Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,24 @@
# For append-only JSONL, `union` is that behaviour and is built in.
.trinity/experience/*.jsonl merge=union

# NOW.md: canonical location is docs/NOW.md. The root file is a symlink, so
# git merges the link target, not the text — and `theirs` is not a built-in
# driver either (same missing .git/config entry as append-log had). Point the
# symlink's rule at the same built-in the real file uses; if the symlink is
# ever replaced by a real file, this keeps working.
# NOW.md: canonical location is docs/NOW.md. The root NOW.md is NOT a symlink
# — `git ls-tree master NOW.md` reports mode 100644, a regular file (a symlink
# would be 120000). It is its own divergent document, last stamped 2026-08-09
# while docs/NOW.md is stamped by every pull request. An earlier version of
# this comment described a symlink and merge-of-the-link-target; no such
# symlink has existed here. Whether the root file should become a real
# symlink, be deleted, or keep its own content is still open — see #2253.
#
# This pattern has no slash, so git matches it by basename at ANY depth: it
# already covers docs/NOW.md (and sub/dir/NOW.md), verifiable with
# `git check-attr merge -- docs/NOW.md`. The rule below is therefore
# redundant, and is kept only because it is the one people look for.
NOW.md merge=union

# The rule above names the SYMLINK, not the file that actually conflicts, and
# `theirs` is not a built-in driver — it needs a per-clone .git/config entry
# that a fresh checkout does not have. So docs/NOW.md conflicted on every
# branch (seven times in one campaign), each resolved identically by hand:
# keep both entries. `union` is built in, so it works in every clone with no
# setup, and it is exactly that resolution.
# docs/NOW.md is the file that actually conflicts: it conflicted on every
# branch (seven times in one campaign), each time resolved identically by hand
# — keep both entries. `theirs` used to be named here, but it is not a
# built-in driver: it needs a merge.theirs.driver entry in .git/config that no
# fresh clone has, so it silently did nothing. `union` is built in, needs no
# setup, and concatenates both sides — exactly that hand resolution.
docs/NOW.md merge=union
61 changes: 59 additions & 2 deletions cli/tri/src/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,13 @@ pub fn now_gate(path: Option<&Path>, today_override: Option<&str>) -> Result<()>
None => Utc::now().format("%Y-%m-%d").to_string(),
};

let re = Regex::new(r"(?m)^\*\*Last updated:\*\*\s*(\d{4}-\d{2}-\d{2})")
// Match the format the producer actually writes. `nownote.rs` emits a
// PLAIN `Last updated: <date>` line, and all 136 stamps in `docs/NOW.md`
// use that form -- zero use the bold one this pattern required before, so
// the gate could never take the `Some` branch on the real document. The
// `**` markers stay optional because archived snapshots (and the entry
// still sitting in root `NOW.md`) predate the switch to plain.
let re = Regex::new(r"(?m)^(?:\*\*)?Last updated:(?:\*\*)?\s*(\d{4}-\d{2}-\d{2})")
.expect("static regex always compiles");
match re.captures(&body) {
Some(caps) => {
Expand All @@ -111,7 +117,7 @@ pub fn now_gate(path: Option<&Path>, today_override: Option<&str>) -> Result<()>
Ok(())
}
None => bail!(
"NOW gate violation: no `**Last updated:** YYYY-MM-DD` line found in {}",
"NOW gate violation: no `Last updated: YYYY-MM-DD` line found in {}",
resolved.display()
),
}
Expand Down Expand Up @@ -188,4 +194,55 @@ mod tests {
std::fs::remove_file(&tmp).ok();
assert!(r.is_err());
}

/// The two tests above write their own fixture in the bold form, so they
/// only ever proved the regex is self-consistent. This one pins the shape
/// `nownote.rs` actually writes (see its `add()`): a PLAIN `Last updated:`
/// line. It fails against the pre-fix bold-only pattern.
#[test]
fn now_gate_accepts_the_plain_form_nownote_writes() {
let date = "2026-05-12";
let body = "# NOW -- some entry (2026-05-12)\n\
\n\
Last updated: 2026-05-12\n\
\n\
## some entry (Closes #1)\n\
\n\
- x\n\n";
let tmp = std::env::temp_dir().join(format!("now_gate_plain_{}.md", std::process::id()));
std::fs::write(&tmp, body).unwrap();
let r = now_gate(Some(&tmp), Some(date));
std::fs::remove_file(&tmp).ok();
assert!(r.is_ok(), "{:?}", r);
}

/// Liveness: run the gate against the real `docs/NOW.md` rather than a
/// fixture. The expected date is re-derived with the *live* gate's own
/// rule (`bootstrap/src/suite.rs` takes the first line containing
/// "Last updated:"), so this asserts the two implementations agree on the
/// actual document. It does not assert freshness, so it cannot go red
/// merely because the file is a day old.
#[test]
fn now_gate_agrees_with_the_live_gate_on_the_real_document() {
let root = match repo_root() {
Ok(r) => r,
Err(_) => return, // not in a git checkout; nothing to check
};
let path = root.join("docs/NOW.md");
let body = match std::fs::read_to_string(&path) {
Ok(b) => b,
Err(_) => return, // file absent (sparse checkout); nothing to check
};
let stamped = body
.lines()
.find(|l| l.contains("Last updated:"))
.and_then(|l| l.split("Last updated:").nth(1))
.map(|s| s.trim().trim_start_matches("**").trim().to_string())
.expect("docs/NOW.md must carry a `Last updated:` line");
let date = stamped
.get(..10)
.expect("`Last updated:` value must start with YYYY-MM-DD");
let r = now_gate(Some(&path), Some(date));
assert!(r.is_ok(), "gate rejected the real docs/NOW.md: {:?}", r);
}
}
86 changes: 86 additions & 0 deletions docs/NOW.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,89 @@
# NOW -- a commit gate that could never pass, a symlink that never existed, and 835 lines of base64 (2026-08-20)

Last updated: 2026-08-20

## hooks: match the NOW stamp format that is actually written (Closes #2299)

Three small defects found while analysing the `docs/NOW.md` bottleneck, all
verified against `origin/master` at `7e8de87b1` before being touched.

### 1. `tri hooks now-gate` required a format nothing produces

`cli/tri/src/hooks.rs` matched the **bold** stamp:

```
r"(?m)^\*\*Last updated:\*\*\s*(\d{4}-\d{2}-\d{2})"
```

The producer disagrees. `cli/tri/src/nownote.rs` writes the plain form, and on
`master` the count is decisive:

```
$ git show origin/master:docs/NOW.md | grep -c '^\*\*Last updated:\*\*'
0
$ git show origin/master:docs/NOW.md | grep -c '^Last updated:'
136
```

Zero bold against 136 plain, so the gate always fell to its `None` branch and
bailed with "no line found". It could not pass on the one document it exists
to check. The pattern now accepts either form; `**` is optional because
archived snapshots and root `NOW.md` still carry the old bold style.

### Why the suite was quiet about it

Both existing `now_gate` tests write their own fixture in the bold form and
then assert the regex reads it back. The fixture matched because the same
commit authored both sides. Neither test ever saw `nownote.rs`'s output or the
real document, so a producer/consumer split was invisible. Two tests added: one
pins the plain shape `nownote.rs` emits, and one runs the gate against the real
`docs/NOW.md`, deriving the expected date with the *live* gate's own rule so
the two implementations are asserted to agree. It checks agreement, not
freshness, so it cannot go red merely because the file is a day old.

### This one is dead code, and that is worth stating

Nothing invokes `tri hooks now-gate` or `tri hooks pre-commit`. The gate that
actually runs is a different implementation -- `.githooks/pre-commit` calls
`scripts/tri check-now` -> `t27c check-now` in `bootstrap/src/suite.rs`, which
matches on `contains("Last updated:")` and therefore accepts the plain form.
No commit is being blocked today. It matters because `MIGRATION_AUDIT.md`
advertises `tri hooks` as the Rust port that replaces the shell gates: the
moment anything wires it up, it rejects every commit.

### 2. `.gitattributes` described a symlink that does not exist

The comment read "The root file is a symlink, so git merges the link target,
not the text". `git ls-tree master NOW.md` reports mode `100644` -- a regular
file; a symlink would be `120000`. Root `NOW.md` is its own divergent document
stamped 2026-08-09 while `docs/NOW.md` is stamped today.

A second claim was backwards too: "the rule above names the SYMLINK, not the
file that actually conflicts". The pattern `NOW.md` has no slash, so git
matches it by basename at any depth -- it already covers `docs/NOW.md`,
confirmed with `git check-attr merge -- docs/NOW.md`. Comments corrected; the
`merge=union` rules themselves are unchanged and `check-attr` returns `union`
for the same three paths before and after.

Whether root `NOW.md` should become a real symlink, be deleted, or keep its own
content is a decision left open deliberately -- #2253 stays open for it.

### 3. `docs/NOW.md.master` was 835 lines of committed base64

Landed in `d063152ad`, a `.master` conflict side-file that got base64'd and
committed. Verified junk before removal: it decodes to 35,328 bytes of an
obsolete NOW snapshot stamped 2026-04-08, and
`git grep "NOW\.md\.master" origin/master` returns zero hits tree-wide. Being
base64, it was unreadable in review, ungreppable and undiffable. Removed.

### A number in my own brief did not survive checking

The task described "137 entries". Measured on `master` it is 136 `Last
updated:` stamps across 74 `^# NOW --` headers. Corrected everywhere rather
than repeated; the load-bearing figure for the defect is the zero, not the 136.
The existing content of this file is untouched -- this entry is a pure prepend,
so `git diff --numstat` reports 0 deleted lines for `docs/NOW.md`.

# NOW -- the workspace was throwing away a release profile, and the brief about it was measured on the wrong branch (2026-08-20)

Last updated: 2026-08-20
Expand Down
Loading
Loading