Skip to content
31 changes: 31 additions & 0 deletions .changeset/plenty-donuts-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@cosyte/ccda": patch
---

phi-scan: a target enumerated and never read refuses the scan

`--allow-fixture` withdrew a path from the target list and the run reported on
whatever remained, so the withdrawal never showed up in the verdict: four argv
shapes printed `OK, no hits` at exit 0 over a corpus holding a live, detectable
violator, including a whole-run sweep whose only violator was the withdrawn file.
CI could print a clean verdict over a corpus it never opened.

The scanner now refuses (exit 2), in every mode, over any target this run
enumerated and never read, naming the paths. The comparison is a set difference
rather than a count, because a count counts the targets that did get read. A
bypass naming a path the run does not enumerate refuses too, under its own
message, since such a flag subtracts nothing (in `paths` mode a nonexistent path
is refused earlier still, by `File not found`). `--allow-fixture` no longer
selects the mode and is unioned into the target list in `paths` mode, so it means
the same thing in every argv.

What it costs, stated rather than left to be discovered: `--allow-fixture` can no
longer reach exit 0 in any mode. The flag, `phi-scan-overrides.md` and the log
gate all stay, so an attempt is recorded and then refused rather than silently
honored, and `scripts/phi-allow-list.txt` is now the only mechanism that reaches
a clean run. The hit footer no longer advertises the flag as a remedy.

No published API, warning code or parser behaviour changes; this is the commit
gate only. There was no real unread corpus in this repo: the all-mode sweep
enumerates and reads the same set, and the four states were reproduced with
planted fixtures rather than found.
48 changes: 24 additions & 24 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ immutability + explicit mutation, and the profile system.

- **Published on npm, public, MIT.** **This line names no version on purpose: it was stale every
time anyone checked, so `npm view @cosyte/ccda version` is the only source of truth**, and a
`@cosyte/ccda` version quoted elsewhere in this file is historical, not
current; the `@cosyte/test-utils`, TypeScript and `pnpm` pins below are live
and mean what they say. Pre-alpha, `0.0.x` ladder (ADR 0001). A published version never moves
`@cosyte/ccda` version quoted elsewhere here is historical; every toolchain pin below is live and
means what it says. Pre-alpha, `0.0.x` ladder (ADR 0001). A published version never moves
backwards.
Why: `documentation/agent-notes.md#the-published-version-line-names-no-version`
- **There are no stubs left.** `src/index.ts` exports a working parser (`parseCcda`), serializer
Expand Down Expand Up @@ -268,37 +267,39 @@ immutability + explicit mutation, and the profile system.
exclusions, phase guards and self-test samples are re-derived for C-CDA and must not be inherited
wholesale.** `CHANGELOG.md` is exempt org-wide (founder, 2026-07-29): do not re-litigate it.
Why: `documentation/agent-notes.md#the-public-surface-gate`
- **Em-dash gate present AND BLOCKING.** `U+2014` is banned outright by
founder directive, and **when it goes red the fix is never to re-encode the character**: rewrite
with a period, colon, comma or parentheses. `no-emdash` is a required status check via the
repository-level `emdash-required-check` ruleset; **re-read the rulesets rather than this line.**
It scans every tracked file **except the script itself**, **and** the
PR title, body and commit messages, so **keep the script free of the literal character.** **It is
the text-only variant, and dropping `grep -I` is the load-bearing part**: `src/profiles/merge.ts`
carries raw NULs and would otherwise be **silently exempt** from a ban with no exceptions, which
is not theoretical. **Do not swap in `website`'s variant**, and do not reach for `pathways`'
- **Em-dash gate present AND BLOCKING.** `U+2014` is banned outright by founder directive, and
**when it goes red the fix is never to re-encode the character**: rewrite with a period, colon,
comma or parentheses. `no-emdash` is required via the repository-level `emdash-required-check`
ruleset; **re-read the rulesets rather than this line.** It scans every
tracked file **except the script itself**, **and** the PR title, body and commit messages, so
**keep the script free of the literal character.** **It is the text-only variant, and dropping
`grep -I` is the load-bearing part** (`src/profiles/merge.ts` carries raw NULs and would otherwise
be **silently exempt**). **Do not swap in `website`'s variant**, and do not reach for `pathways`'
`git check-attr binary` without first adding a `.gitattributes`. **Do not trust a copy count
written down anywhere, including here** - enumerate at carry-back time. **The gate covers new
text only and does not rewrite history.**
Why: `documentation/agent-notes.md#the-em-dash-gate`
- **`phi-scan` scans EVERY tracked file now, markdown included; the two exemptions are literal
paths, and writing docs is inside the gate.** **THERE ARE THREE ROUTES, not two**
(`paths` is the third; miscounting them shipped an `INTRODUCED`, twice). **Never widen
`isSourceCode`: it also SUBTRACTS the structured scan in `looksLikeCda`.** A refusal exits **2**.
paths, and writing docs is inside the gate.** **THERE ARE THREE ROUTES, not two** (`paths` is the
third; miscounting them shipped an `INTRODUCED`, twice). **Never widen `isSourceCode`: it also
SUBTRACTS the structured scan in `looksLikeCda`.** A refusal exits **2**.
**Write no count in this area; three drafts wrote one and were wrong.**
Why: `documentation/agent-notes.md#the-corpus-every-phi-scan-route-read-past`
- **`all` mode UNIONS the bytes git carries with the walk, deduped by CONTENT not path (the EOL
axis). Keep every `git` call in `buildTargetsForAll` AFTER the walk and BEFORE the first read; do
not decorate `Target.path`. A non-blob index mode or an EMPTY index refuses. `ccda` had NO real
unscanned corpus: the four states were reproduced, not found.**
Why: `documentation/agent-notes.md#the-all-mode-sweep-reads-the-bytes-git-carries`
- **A target ENUMERATED and never READ refuses (exit 2), any mode; `--allow-fixture` cannot reach
exit 0 anywhere. Assert an exact code AND the tier's message: `not.toBe(0)` accepts a crash.**
Why: `documentation/agent-notes.md#the-completeness-rule`
- **The `CLAUDE.md` / `agent-notes.md` contract is gated, and unlike the public-surface gate above
it BLOCKS** (it runs in the test suite, inside `parser-ci-required-checks`). **It asserts what
THIS repo promises, never a fleet universal**: `config`, `hl7` and `workflow` carry no
`agent-notes.md` at all. **Do not promote it to an umbrella script.** It scans **EVERY tracked
file, no exclusion list: do not re-add a binary/NUL skip** - the first cut had one and silently
exempted `src/profiles/merge.ts`, the file the em-dash trap above names. The bare `` `#anchor` ``
form is confined to `CLAUDE.md` by shape and scope and **must not be widened** (`#id`/`#62` are
exempted `src/profiles/merge.ts`. The bare `` `#anchor` `` form is
confined to `CLAUDE.md` by shape and scope and **must not be widened** (`#id`/`#62` are
XML and C-CDA narrative references). **Never delete an imperative or a section to get green.**
Why: `documentation/agent-notes.md#the-agent-notes-contract-gate`

Expand All @@ -312,8 +313,7 @@ a summary.
`@cosyte/tsconfig`. **Target ES2023**, `NodeNext`. TypeScript 5.9.x, exact-pinned.
- **Build:** dual ESM + CJS + `.d.ts` via `tsup` (`@cosyte/tsup-config`); `attw` is a publish gate
(per-condition types: `.d.ts` for `import`, `.d.cts` for `require`). The `attw` script is
**`node scripts/attw.mjs`, not the bare CLI**: see the guardrail below. The CLI reports a missing
`dist/` as "does not contain types" and **exits 0**.
**`node scripts/attw.mjs`, not the bare CLI**: see the guardrail below.
- **Node:** **>= 22** (CI matrix 22 + 24).
- **Package manager:** `pnpm@10`.
- **Lint/format:** **ESLint 10** + unified `typescript-eslint` (type-checked) via
Expand Down Expand Up @@ -351,11 +351,11 @@ a summary.
token.** **`.npmignore` versus `files` is about the file's DEPTH, not its existence.**
`test/scripts/attw-gate.test.ts` pins two of the three, the upstream exit-0 itself, a real failure
and a negative control; **the printed-nothing backstop is pinned by NO test, a stated gap rather
than an oversight. Do not carry the test file's "16 of 21" figure forward, re-measure it.** **This is a
per-repo script** and a sibling still invoking the CLI directly still has the defect; do not write
a repo count down here, derive it. `scripts/verify.sh` in the meta-repo **must not be touched** for
this. **The guard is described in four committed files and three corrections have landed in some
copies and not others: prefer CUTTING a copy to adding a more careful one.**
than an oversight. Do not carry the test file's "16 of 21" figure forward, re-measure it.**
**Per-repo script**; a sibling still on the bare CLI still has the defect. Write no repo count,
derive it. The meta-repo's `scripts/verify.sh` **must not be touched** for this. **The guard is
described in four committed files and three corrections have landed in some copies and not
others: prefer CUTTING a copy to adding a more careful one.**
Why: `documentation/agent-notes.md#the-attw-wrapper-script`

## Standing disciplines (every change)
Expand Down
92 changes: 92 additions & 0 deletions documentation/agent-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2118,6 +2118,98 @@ non-regular branch, not on `isFile()`). It is harmless - the file holds a `gitdi
direction is more scanning than claimed, never less. Reproduced on this repo's checkout under the
meta-repo; a standalone clone has a real `.git` directory and never reaches it.

## The completeness rule

`PHI-SCAN`'s second half, closed here 2026-08-11. It is independent of the union half above and was
still live after it: **CI could print a clean verdict over a corpus it never opened.** **The rule
itself, the four argv shapes it closes, what it costs and why the comparison is a set difference are
stated ONCE, in `scripts/phi-scan.ts`'s docblock.** This section carries what a docblock cannot: what
was measured here, and which claims were checked rather than inherited.

**THE DEFECT WAS REPRODUCED IN THIS FILE, NOT PORTED AS A STORY.** `--allow-fixture` withdrew a path
from the target list and the run reported on whatever remained, so the withdrawal was invisible in
the verdict. Four argv shapes reached `OK, no hits` at exit 0 over a corpus holding a live, detectable
violator, and the subtlest of them is the one a reader will miss: the seed read
`paths.length > 0 ? paths : [...allowFixtures]`, so with any positional path present the flag was a
silent no-op and the named file was never ADMITTED to the run rather than withdrawn from it. All four
are enumerated in the docblock and all four are pinned by test.

**AND AGAIN, THE HONEST ANSWER IS A NULL RESULT: THERE WAS NO REAL UNREAD CORPUS HERE.** Measured at
`1978bf7` with the rule in place: `all` mode enumerates 141 paths and reads all 141, nothing
tolerated and nothing unread, so the sweep exits 0 on its own corpus. **The figures are DATED TO THAT
COMMIT rather than asserted as current**, for the reason the section above gives: the `.changeset/`
entry that ships this raises the tracked figure by one until a release consumes it. The arithmetic is
written out because 141 looks like the tracked count wearing a different hat and is not: 141 tracked,
minus the one declared literal exclusion, is 140 in-scope tracked paths reached by both the walk and
the union, plus the `.git` **gitfile** this submodule checkout carries in place of a directory (the
pre-existing, filed observation in the section above). The tracked figure was taken with three tools
(`wc -l`, `rg -c` and a stage-0 filter over `ls-files -s`), because `grep -c` has mis-reported a zero
on this class three times. **`ncpdp` reproduced the same null result on the union half, so the fleet
premise that "every repo is hiding something" is now broken twice. Do not manufacture a gap to match
a sibling.**

**WHAT IT COSTS THIS REPO, STATED RATHER THAN LEFT TO BE DISCOVERED. `--allow-fixture` CAN NO LONGER
REACH EXIT 0 IN ANY MODE.** The flag, `phi-scan-overrides.md` and the log gate all stay, so an attempt
is RECORDED AND REFUSED rather than silently honored, and `scripts/phi-allow-list.txt` is now the only
mechanism that reaches a clean run. The hit footer stopped advertising the flag as a remedy for the
same reason: a printed remedy that walks a developer from exit 1 into exit 2 is the same defect as one
that reaches a false green, with the sign flipped. **The log gate did not become decorative and the
test that used to assert "honors a logged bypass, exit 0" now asserts it reaches a DIFFERENT refusal**,
which is the boundary between the two tiers.

**THE VANISH TOLERANCE IS THE ONE ACCOUNTED-FOR NON-READ, AND SUBTRACTING IT IS A DECISION.** A target
skipped under `Target.tolerateVanish` is excluded from the unread difference, or the `ccda@0.0.5`
publish-time refusal would come straight back. It cannot launder a bypass: an `--allow-fixture` path is
skipped BEFORE `scanTarget` is ever called, so it can never enter the vanished set, and the tolerance
is bounded to an untracked file the walk enumerated itself that failed with `ENOENT`. **If you widen
that tolerance, you widen this hole in the same edit.**

**⚖️ THE POSITIVE CONTROL IS THE POINT, AND IT IS BUILT AGAINST THIS REPO'S OWN CAUTIONARY LINEAGE.**
`#110`'s final fix shipped ungraded and `#111` found that its branch-and-merge fixture had gone red on
a **committer identity** (`git merge` dying at exit 128 before it touched the index) while an
`expect(...).not.toBe(0)` waved it through. **A premise assertion that accepts any non-zero exit
accepts a crash**, and this scanner has three distinct exit-2 tiers, so every assertion in the new
block pins an exact code AND the message that names the tier. The control itself runs the same argv
against a copy of the scanner with the rule's one line mutated out, asserts the mutation landed (so it
cannot go vacuous when the rule is reworded), and asserts the mutant reproduces both pre-fix results
exactly: the HITS code where the corpus still holds a readable violator, and `OK, no hits` at exit 0
where it does not. Re-run against the pre-change scanner, seven of the nine new cases red and the two
that pass are the anti-vacuity premise and the no-false-refusal case, which must hold on both trees.

**🛑 THE GRADE FOUND THREE PROSE DEFECTS AND NO CODE DEFECT, WHICH IS THIS LINEAGE'S SIGNATURE, SO
THEY ARE RECORDED RATHER THAN QUIETLY FIXED.** All three were `INTRODUCED` by this slice and all three
are corrected on it:

1. **A draft docblock listed the WALK's filters as if they bounded the whole sweep.** Only
`EXCLUDED_PATHS` binds both halves of `all` mode. A **tracked** path under a `WALK_SKIP_DIRS` name
IS enumerated, by the union half, and read from the index: a committed `dist/leaked.txt` with a
clean working copy reports `HIT: dist/leaked.txt (as git carries it)` at exit 1. Measured twice,
independently. The direction is more scanning than the sentence claimed, never less, and it cannot
produce a false refusal because the union both enumerates AND reads it. **Do not read the walk's
filter list as the sweep's.** The gitignore half of that sentence is only VACUOUSLY true of the
union, and `unionCandidatePaths` says so where it declines to apply it.
2. **The `CLAUDE.md` byte budget forced five compressions and one of them widened a scoped sentence
into a false universal:** "a `@cosyte/ccda` version quoted elsewhere is historical" became "any
version", which declares the live enforced toolchain baseline historical. **Compressing to fit a
budget is an edit like any other and can introduce a claim defect.** No imperative was deleted by
any of the five; that was checked one by one.
3. **The positive control's first mutant assertion accepted a crash.** It pinned exit 1, and node
exits 1 on an uncaught throw, so a mutant that failed to RUN satisfied it; only the second mutant
run (exit 0 plus `OK, no hits`) was crash-proof. It now asserts the payload marker too. **This is
the `#111` shape again, in the very block written to avoid it.**

**A BOUND WORTH KNOWING: the rule is PATH-LEVEL.** "Read" means the bytes went through `scanTarget`,
not that every detector ran over them, so `CHANGELOG.md` counts as read while
`STRUCTURED_EXEMPT_PATHS` keeps the five structured detectors off it. That exemption is pre-existing
and argued where it is declared; this rule does not re-litigate it, and what it adds is that a file
nothing opened at all can no longer pass.

**THE EXTERNAL DEFINITION OF DONE.** `config`'s `pnpm drift` runs a capability PROBE, not a regex over
this file's prose, and it named this repo's drift as "reported only its HITS code (1) over a run that
withdrew `test/fixtures/phi-scan-probe-decoy.txt` after enumerating it". It is `ok` here now. **The
probe grades behaviour on purpose: this class has produced defects that lived entirely in a prose
carrier while the code was right, so matching prose would grade the comment.**

## The agent-notes contract gate

`CLAUDE-MD-AUDIT` (2026-08-04) split this file out of `CLAUDE.md` and nothing checked the result.
Expand Down
19 changes: 14 additions & 5 deletions phi-scan-overrides.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
# phi-scan bypass log

This file logs every `--allow-fixture <path>` bypass invocation of
`scripts/phi-scan.ts`. The scanner refuses to honor a `--allow-fixture <path>`
flag UNLESS this file contains an entry referencing the same path. The committed
log is intentionally annoying: it discourages bypass and creates an audit
trail. Prefer extending `scripts/phi-allow-list.txt` (a token-level, reviewed
declaration) over a whole-file bypass.
`scripts/phi-scan.ts`. The scanner refuses a `--allow-fixture <path>` flag
UNLESS this file contains an entry referencing the same path. The committed log
is intentionally annoying: it discourages bypass and creates an audit trail.

**A LOGGED BYPASS IS NOW RECORDED AND THEN REFUSED, NEVER HONORED, so
`--allow-fixture` CANNOT REACH EXIT 0 IN ANY MODE.** A bypass withdraws a file
from the read set, and a scan that did not open a file has no clean verdict to
give about it, so the completeness rule refuses (exit 2) over any target this run
enumerated and never read. The two tiers are separate and stay separate: this
log is what admits the flag to the run at all, and the completeness rule is what
then refuses the run. **`scripts/phi-allow-list.txt` (a token-level, reviewed
declaration) is the ONLY mechanism that reaches a clean run.** The rule, the four
argv shapes it closes, and what the vanish tolerance is allowed to subtract from
it are stated once, in the docblock of `scripts/phi-scan.ts`; read them there.

## How the scanner detects PHI

Expand Down
Loading
Loading