diff --git a/CLAUDE.md b/CLAUDE.md index 463bf9ca1..541cf9750 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -185,6 +185,8 @@ deno task binding:audit # comment↔token binding audit: does forma deno task authoring:audit # authoring-independence over Svelte boundary whitespace: every render-equivalent authoring of one document (hug ↔ space ↔ newline at a tag's content boundary; space ↔ newline between siblings) must reach ONE tsv fixed point (pure Rust, no sidecar; gated in `deno task check`) — exits 1 on any non-idempotency, site-level or a base-non-idempotent FILE; see Debug Tooling deno task fuzz:audit # seeded mutational fuzzer over tests/fixtures (fixed --seed 0 --iterations 5000; pure Rust, no sidecar; gated in `deno task check`) — asserts no-panic + idempotency + structural-reparse, on every seed file AS AUTHORED and then on mutated input; see Debug Tooling deno task comments:audit # print-once comment ledger: every comment a document PARSES must be EMITTED exactly once (pure Rust, no sidecar; gated in `deno task check`) — reports DROPPED (silent content loss) and DOUBLE-PRINTED; the structural guard on the detached comment model, tsv's `ensureAllCommentsPrinted`; see Debug Tooling +deno task gaps:audit # gap-injection audit: inject a comment into EVERY gap (five payloads, one per ownership path) and re-run the ledger — the discovery arm `comments:audit` can't be, since it only formats each file AS AUTHORED and no fixture covers most positions (eight such drops were found BY HAND, all green on every gate). Pure Rust, no sidecar; gated in `deno task check` as a RATCHET over a generated shape snapshot (`gap_audit_known.txt`): every line is a known bug and the file shrinking is the goal, so a shape not on the list, one on it that no longer fires, or any PANIC, FAILS. ~17 s. Full reference: ./docs/gap_audit.md +deno task gaps:audit:update # regenerate that snapshot after fixing a shape (or when a new fixture merely REACHES a pre-existing one); refuses a narrowed run deno task idempotency:sweep # F1 (idempotency) sweep over the real-code corpus (the `perf` view — sibling dev repos + upstream framework source). NOT in `deno task check`: machine-dependent corpus, minutes not seconds. Run at conformance cadence or after a printer change; see Debug Tooling ``` @@ -882,6 +884,30 @@ cargo run -p tsv_debug --features comment_check comment_audit ~/dev/zzz/src # # source), so they are outside the model by construction. ``` +**Gap-Injection Audit (dropped-comment discovery):** + +```bash +# gap_audit - inject a comment into EVERY gap and re-run the print-once ledger. The +# DISCOVERY arm `comments:audit` can't be: the ledger only ever sees a document AS +# AUTHORED, so a gap no fixture puts a comment in is one it never checks (eight such +# drops were found BY HAND, all green on every gate). Pure Rust, no sidecar. +cargo run --profile corpus -p tsv_debug --features comment_check gap_audit # tests/fixtures +cargo run --profile corpus -p tsv_debug --features comment_check gap_audit ~/dev/zzz/src +# Also: --json, --jobs N, --limit N, --payload , --all-bytes, --update. +# Build with `--profile corpus` (release + panic=unwind): plain `--release` is +# panic=abort, so a formatter panic kills the process instead of being caught + reported. +# +# GATED as a RATCHET, not a green gate: `gap_audit_known.txt` is a machine-generated +# snapshot of the ~717 shapes tests/fixtures produces, every line a KNOWN BUG, the file +# shrinking is the goal. A shape not on the list, one on it that no longer fires, or any +# PANIC, FAILS. `--limit`/`--payload`/`--all-bytes`/a path narrow a run, so they skip the +# ratchet and refuse `--update`. ~17 s. +``` + +Full reference — flags, the ratchet, reading a finding, triage + re-pin workflow, +scope: ./docs/gap_audit.md. Design rationale (why byte offsets and not tokens, why the +ledger is the oracle, why five payloads) lives in the `gap_audit` module docs. + **Build-Fanout Audit (exponential-rebuild regression guard):** ```bash diff --git a/crates/tsv_debug/src/cli/commands/gap_audit.rs b/crates/tsv_debug/src/cli/commands/gap_audit.rs new file mode 100644 index 000000000..2ece89117 --- /dev/null +++ b/crates/tsv_debug/src/cli/commands/gap_audit.rs @@ -0,0 +1,1937 @@ +//! Gap-injection audit — the mechanized form of hunting the dropped-comment class. +//! +//! ## Why this exists +//! +//! One recurring bug class: **a printer that concatenates fixed pieces without scanning +//! the gaps between them silently DROPS any comment an author wrote in a gap.** A header +//! built as `d.text("import.source(")` scans neither of its two dot gaps, so +//! `import./* c */source(x)` loses the comment — no error, no diff, just gone. +//! +//! The print-once ledger ([`comment_ledger`]) would catch every one of these. It just +//! never sees them: [`comment_audit`](super::comment_audit) formats each file **as +//! authored**, and a gap only becomes a finding once a comment is actually *in* it. Eight +//! such sites were found BY HAND, and every one was green on every gate — `cargo test`, +//! `comments:audit`, `roundtrip:audit`, the corpus diff — purely because no fixture +//! happened to put a comment in that position. The gates were not wrong; the corpus was +//! silent. +//! +//! This audit closes that hole: for each seed file it injects a comment into **every** +//! candidate gap, one at a time, formats, and runs the ledger over the result. +//! +//! ## Design +//! +//! Pure Rust, no sidecar, no new deps — the [`fuzz`](super::fuzz) / +//! [`comment_audit`](super::comment_audit) direction. Deliberately **targeted, not +//! random**: byte mutation would essentially never synthesize a valid comment in a dot +//! gap, which is the whole point of the class. +//! +//! **Sites are byte offsets, not tokens.** A token-stream enumeration would need a flat +//! Svelte token contract that doesn't exist — and `.svelte` is where this class lives, +//! since TS-only syntax is fixtured as `.svelte` + `lang="ts"` (a TS-only audit reaches 53 +//! of 6,689 fixture files). Worse, it would carry exactly the blind spot the class +//! exploits: a punctuator-joined header is a **zero-width** gap, the first thing a +//! "between two tokens" abstraction elides. A byte offset has no such notion, so it cannot +//! miss one. +//! +//! **But an offset must first be somewhere the payload IS a comment**, and tsv's own parser +//! cannot answer that. It is deliberately more permissive than the canonical one, so +//! "tsv accepted it" does not mean "an author could write it": tsv parses +//! `` region), +/// and an `ExpressionTag`'s brace interior (`{ /* c */ x.y }` is legal). +/// +/// TODO: `