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
4 changes: 3 additions & 1 deletion .claude/jit-context/paths/00-manual/tooling.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ The half that was never a judgement call: **a newline in a name forged a whole r

**A withheld name must not cost the reader the row.** `jit-dry-run.sh` keeps every refused row identifiable without printing the name: a `REFUSED`/`WARN` pattern row still prints its pattern verbatim on its own `untrusted>` line, and `check_entry_file()` — the one report whose whole class is unprintable, since a name carrying a separator is never a plain name either — closes with the position, worded exactly as `jit_row_id()` words it for the hooks. A fix that withheld the name and said nothing else would have replaced a disclosure with a dead end.

One line here prints a whole path rather than a name, and it needed a second fix: `truncate_index()`'s `FATAL`. Its `: > "$tsv" 2>/dev/null` applied the redirections **left to right**, so the failing one was set up while stderr was still the terminal — bash printed its own diagnostic carrying the absolute path, and the `2>/dev/null` that followed silenced nothing. A clone reaches that branch on purpose by shipping a *directory* named `00-index.tsv`. The redirection is now `: 2>/dev/null > "$tsv"`, and the reason bash would have given is replaced by the one case that is constructible. The `FATAL` then names `JIT_BASE` on its own line: the display string is `dimension/layer/leaf`, so the absolute path went with the withheld component, and the *ordinary* failure here — a read-only tree, a full disk — is the one where the reader needs a path they can act on.
One line here prints a whole path rather than a name, and it needed a second fix: `truncate_index()`'s `FATAL`. Its `: > "$tsv" 2>/dev/null` once applied the redirections **left to right**, so the failing one was set up while stderr was still the terminal — bash printed its own diagnostic carrying the absolute path, and the `2>/dev/null` that followed silenced nothing. A clone reaches that branch on purpose by shipping a *directory* named `00-index.tsv`. The `FATAL` names `JIT_BASE` on its own line: the display string is `dimension/layer/leaf`, so the absolute path went with the withheld component, and the *ordinary* failure here — a read-only tree, a full disk — is the one where the reader needs a path they can act on.

**`truncate_index()` no longer truncates anything in place (#480).** The bash-leak this paragraph describes was about a REAL write to `$tsv` that could fail; since #480 that write never happens at all. Every index is built into a **temp file in the same directory** and `commit_index()` swaps it into place with one `mv`, and only when nothing crashed while that index was being built — a helper process dying partway through the loop (macOS 27's built-in bash killing forked `awk` children with `Bus error: 10`, not reproducible under Homebrew bash) used to leave the committed index truncated or half-written, because the truncation happened before the loop even started. Now a crash still exits the whole run non-zero, but the previously committed index for the layer that crashed is left on disk, untouched. The one consequence worth knowing if you touch this code: `mv`'s rename() only needs write permission on the **directory**, never on the file it replaces, so a `00-index.tsv` that is itself `chmod 444` no longer blocks a rebuild the way it used to — the genuine "cannot write" case is now the directory, not the file.

**A check here has to agree with the hooks, because the hooks send people here.** The refusal notice they inject ends with `bash scripts/jit-dry-run.sh --base <tree>`, so a class the hooks refuse and this tool reports `ok` for makes that advice false — this repository's own defect class, in the tool written to report it. The byte checks (`#77`, `#78`) are read off the same `common.sh` functions the hooks use rather than a second copy in bash. Two traps came with them: `read -r` in the row loops truncates at a NUL, so that pass is one `awk` per index file rather than a bash loop; and that `awk` must be invoked `LC_ALL=C`, because the byte range it builds is a decode failure in a UTF-8 locale — unpinned, one-true-awk aborted the whole program on the first row of a tree carrying the byte the check exists to find.

Expand Down
13 changes: 13 additions & 0 deletions changelog.d/480.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- **A crashed helper process no longer truncates a committed index** (#480). On macOS 27,
the built-in `/bin/bash` 3.2.57 can kill forked `awk` children with `Bus error: 10`
partway through `rebuild-tsv.sh` -- not reproducible under Homebrew bash, and too rare in
this plugin's own short test runs to ever show up there. `rebuild-tsv.sh` already
detected one shape of that crash and reported it, but it had already truncated the REAL,
committed `00-index.tsv` before the rebuild loop even started, so the run kept writing
into the file a fresh clone and the hooks read as this tree's rules -- in the reporter's
project, 7 indexes ended up empty and the largest went from 3,714 rows to 1,430.

Every index is now built into a temp file in the same directory and swapped into place
with a single `mv` only when that index's own build finished clean; a crashed helper
still exits the whole rebuild non-zero, but the previously committed index for the layer
it crashed in is left on disk, untouched, byte-identical to what it was before the run.
Loading
Loading