From 81ea7b95c47b244412a240fab34df068403d30ba Mon Sep 17 00:00:00 2001 From: Noah Schatz Date: Tue, 4 Aug 2026 21:54:03 +0000 Subject: [PATCH] docs(claude-md): relocate the narrative to documentation/agent-notes.md CLAUDE.md was 51,276 bytes and is always-read by every worker that enters this repo. The per-incident write-ups, the shipped-phase histories and the long rationales move to documentation/agent-notes.md verbatim, under headings that name where they came from. Nothing is deleted. What stays in CLAUDE.md is the cursor, the rules, and every trap, each compressed to a one-line imperative with a link to the section that proves it. 51,276 -> 29,451 bytes. Two things recorded rather than silently changed, both in the new file's "Corrections made during the relocation" section: - The Tech Stack runtime-deps bullet was pre-swap and contradicted Status (it listed @cosyte/fhir and @cosyte/transform as hard vendored deps). It now states the shipped shape, verified against package.json; the stale wording is quoted in agent-notes.md rather than dropped. - The "FIXED at 0.0.3" version literal is not carried across: the registry and package.json both read 0.0.4 on 2026-08-04. The trap itself is live and unchanged and is stated without a number, per ADR 0023. Per the 2026-08-04 amendment to the meta-repo's ADR 0023, which bounds /CLAUDE.md and names /documentation/agent-notes.md as the relocation target. The bound itself is deliberately not quoted anywhere in either file: it is a per-repo ratchet in REPO_CLAUDE in doc-budget.mjs that is lowered as relocations land, and the first number quoted for it went stale within a day. documentation/agent-notes.md is deliberately outside the format:check glob. Prettier renormalises list indentation and would rewrite the archive whose verbatim preservation this slice rests on. Do not add it. Docs only: no published surface changes, so no changeset and no version bump. --- CLAUDE.md | 801 +++++++++++++---------------------- documentation/agent-notes.md | 621 +++++++++++++++++++++++++++ 2 files changed, 911 insertions(+), 511 deletions(-) create mode 100644 documentation/agent-notes.md diff --git a/CLAUDE.md b/CLAUDE.md index f7145e1..49999a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,15 @@ # @cosyte/cli: Project Guide for Claude +> **The narrative lives in [`documentation/agent-notes.md`](documentation/agent-notes.md). Read it +> before you touch anything a rule below tells you not to touch.** On 2026-08-04 this file was 51,276 +> bytes and it is always-read by every worker that enters this repo, so the per-incident write-ups, +> the shipped-phase histories and the long rationales were relocated there **verbatim**, under +> headings that say where they came from. Nothing was deleted. +> +> What stays here is the cursor, the rules, and **every** trap, each compressed to a one-line +> imperative with a link to the section that proves it. **"I did not read the reason" is not a licence +> to discount the rule.** Every one of these lines cost a defect to learn. + ## Project **`@cosyte/cli`**: the **cosyte CLI**: a **`bin` package** (the `cosyte` command), not a parser and @@ -23,345 +33,189 @@ subpath still exports a small programmatic `core` API (`detectFormat`, `EXIT`, ` ## Status -- **▶ `docs-content/sidebars.json` IS BOUND BY AN IA SPINE THAT NOTHING IN THIS REPO CHECKS, AND - BREAKING IT STOPS THE WHOLE DOCS SITE DEPLOYING.** This package shipped two off-spine top-level - categories, **"MCP server"** and **"Reference"**. The docs site lints the sidebar out of each - package's released `docs-content.tar.gz`, and in strict mode a non-canonical top-level label is an - **error**, so `cli` was one of two packages holding a site that had not deployed for four days. - It was pre-existing and masked: every build died earlier, on memory, before reaching the gate. - **The canonical top-level order is `Overview` (the `intro` DOC REFERENCE, not a category), - `Installation`, `Quickstart`, `Core Concepts`, `Guides`, `API Reference`, `Troubleshooting`.** - Categories are OPTIONAL; the rule is that whatever you have is labelled and ordered canonically, so - `{"docs":["intro"]}` is fully compliant. `mcp` and `reference-commands` now sit under **Guides**, - `limitations` under **Troubleshooting** (the spine's item 7 explicitly houses "Known Limitations", - and `mllp`/`astm`/`deid` all keep it there; `synth` is the lone counter-example, so do not copy - `synth` here); nothing was orphaned. - **🔴 NEVER AUTHOR AN `API Reference` CATEGORY.** It is injected by the docs site when the package - ships API sources, and a hand-authored one is a distinct, harder **error** than the off-spine - label it would be replacing. Renaming "Reference" to "API Reference" trades one failing check for - another. - **▶ AND DO NOT REPEAT THE CLAIM THAT IT LANDS "JUST BEFORE Troubleshooting". A refuter falsified - that here.** Two different code paths inject it and they DISAGREE: - `docs/scripts/sidebar-resolver.ts` inserts at the canonical position, but it governs the - UNVERSIONED `content//` instance, which is not served once a slug is versioned - (`includeCurrentVersion: false`). The served path for a released package is - `docs/scripts/versioning/sidebar-augment.ts`, which **appends** (`[...value, apiCategoryEntry()]`), - so the rendered nav ends `..., Guides, Troubleshooting, API Reference`. The augmenter's own header - claims it mirrors the resolver; it does not. **This is a `docs` defect, not a `cli` one**, it is - cosmetic ordering, and it affects every released package. **The IA linter cannot see it** (it - refuses `versioned_sidebars/`), so "verified against the linter" is NOT evidence about placement. - State only what this package controls: it does not author or position that category. - **Nothing in `verify.sh` or this repo's CI can catch this** (`pack:docs` checks only that - `intro.md` and `sidebars.json` EXIST), so verify a sidebar edit against the site's own linter, - `docs/scripts/check-ia-conformance.ts`. It self-executes on import, so `NODE_ENV=test` is required - to call `lintSidebar` directly. **Use the previously shipped sidebar as a negative control**: if it - does not report errors, the probe is wrong, not the sidebar. - **Only a NEW RELEASE clears the gate**, because it reads the shipped artifact and releases are - immutable, so every already-published version keeps its sidebar forever. Archived versions are - reported at `info` and never gate. Do not try to fix history. - -- **▶ THE PRE-COMMIT PHI GATE WAS BLIND TO `git mv`, AND THE HOLE WAS NOT LIMITED TO LINKS.** - `scripts/phi-scan.ts --staged` is the `pre-commit` hook (`simple-git-hooks`). It listed the index - with `git diff --cached --name-only --diff-filter=AM`. Rename detection is **on by default**, so - `git mv test/__fixtures__/` stages as a **two-path `R100`** record, and `R`/`C` are in - neither `AM` nor `AMT`, so the status filter **deleted the record** and the destination was never - enumerated. Measured here, both at **exit 0** through the hook: an ordinary regular file carrying a - value the scanner's own floor catches (`:100644 100644 R100`) and a link - (`:120000 120000 R100`, index mode `120000` under the scan root). - **`--no-renames` is the whole remedy and it is a strict SUPERSET, not a narrowing**: the - destination arrives as a single-path `A`, the source as a `D` the filter drops. Verified under - `diff.renames` = `true` / `copies` / `false` / `1` and `diff.renameLimit=1`: **no `R` or `C` - survives any of them**, which is what makes the two-field `--raw -z` stride **structural rather - than conditional**. `copies` is not hypothetical here, it emits a live `C100`. - **Do NOT re-derive this as "needs the two-path record shape, a scope decision".** That framing was - ported from a sibling and is false; `dicom` measured it and `cli` re-measured it. - Three more shapes in the same route, each measured at exit 0 on `a7a92f8` and each closed: the - **destination mode was never read** (`--name-only` gives none, and `git show :` answers a - link with its **target path as though it were content**, so the scan read path text and never the - target's bytes, and this is why the route now lists `--raw -z` and refuses a non-blob mode with - **exit 2**); **`T` was missing from the filter**, so replacing a _tracked_ fixture with a link was - neither `A` nor `M` and the record was gone before a mode could be read; and **each scan root's own - path** is in scope now, because git records no index entry for a directory, so an entry at exactly - `test/__fixtures__` or `src` is that root replaced by a blob or a link. - **A refusal NEVER prints the link target.** It is working-tree text and a target path of the shape - `../patients/--.txt` is the whole reason: a diagnostic about a PHI leak is - itself a PHI surface. The entry's **own** path is printed deliberately, and every offender is - named, not just the first. **That guarantee is about a REFUSAL and does not extend to a hit** (see - the linked-scan-root residual below, where the values printed come from the far side of a link). - **The all-mode walk got the same refusal**, because a scanner whose pre-commit half refuses a link - while its CI half silently drops one cannot be reasoned about. The `.md` exemption deliberately - does not reach a link. - **Two exit-code defects fixed with it, and the reason matters more than the codes:** a missing or - unreadable allow-list, and an unreadable scan root, both threw past every handler and exited **1** - with a stack trace. **`1` is this contract's code for "hits found"**, so a broken invocation read - as a PHI finding. Both are exit **2** now. - **▶ THE REFUSAL RULE IS SCOPED TO AN _ENUMERATED_ ENTRY, AND THE UNQUALIFIED VERSION IS FALSE.** - A refuter caught the first draft of this note asserting "neither route follows such an entry" while - the same file falsifies it. The rule covers an entry the walk reached **beneath a root it had - already opened**, and a staged record **in scope per the boundary rule** (NOT "at or under a scan - root": measured, a staged link at `src/notes.json` is under a scan root and `--staged` exits 0 over - it, because the `src/` half of that scope is `.ts` only. The all-mode sweep refuses it, so nothing - escapes the gate as a whole). Three shapes escape it, all - **PRE-EXISTING** (identical on `a7a92f8` and in `dicom`), all measured, **none closed here**: - (1) **a scan root that is itself a LIVE link is followed** by the all-mode walk, because - `existsSync`/`readdirSync` both resolve, so the walk reads files no commit contains and prints their - values under a **fabricated in-repo path** that holds no such file, which is a confident wrong - provenance on the channel this repo calls a PHI surface. The **dangling** direction is the mirror - image: it prints OK over a corpus it never opened; (2) **an ancestor** of a scan root is in neither - route's scope, so staging `test` as a link is exit 0 on `--staged` and the walk then follows it; - (3) **paths mode follows an explicitly named link** (`statSync` resolves). The `--staged` half of - shape (1) **is** closed here. **Do not "fix" this by growing the guard inside a rename slice**: the - remedy taken was to correct the claim, and closing it needs a refuse-a-scan-that-observed-nothing - rule plus a decision about how far above a root to look. - **Other residuals, all PRE-EXISTING and none closed:** `D` and `U` are still not enumerated (`U` - costs nothing that can reach a commit, **measured**: `git commit` refuses an unmerged index - outright, exit 128); under `src/` the staged route covers only `.ts` while the all-mode walk covers - every non-`.md` file, so **the two routes disagree there** and the CI sweep is the cover. - **16 of `test/scripts/phi-scan.test.ts`'s 34 tests run red on `a7a92f8`.** The 18 that stay green - are the floor tests, the deliberate controls (one of which asserts the payload under test is - something this scanner would otherwise catch), and the four that **pin the residuals above** and - are green on both trees by design. **Give these tests explicit timeouts**: each spawns `tsx` cold, - measured at 0.5s idle and **3.7s under contention** against a shared 10s default. - **Two vacuity traps this suite has already sprung, both worth knowing before you add to it:** a - fixture built its merge conflict with a bare `git merge` and DISCARDED the result, which on a - runner with no git identity refuses before touching the index, so every later assertion held over - an empty one (real CI caught it; local runs passed against an ambient global identity). And the - `diff.renames` loop asserted only the detection-OFF side, so it would have passed just as happily - if git had stopped emitting the record shape the whole change is about. **Assert the premise, not - only the remedy.** - -- **Em-dash brand gate armed, and unlike most siblings this repo was NOT clean when it landed.** - `scripts/check-no-emdash.sh` (`pnpm check:no-emdash`) plus `.github/workflows/no-emdash.yml` enforce - the founder directive banning `U+2014` outright (`knowledgebase/06-brand/voice-and-tone.md`, "No em - dashes. Ever."). It scans **both** halves the rule covers: every tracked file, **and** the PR title, - body, and commit messages, on the non-default `edited` trigger so retitling a PR re-checks it. - **Measured byte-level over all 124 tracked files, not over markdown alone** (a markdown-only count is - what wrongly cleared `dicom`): **659 occurrences across 87 files**, all literal, none encoded. **61 of - the 87 are not markdown** (26 are), and one is `package.json`, whose `description` publishes to npm. All 659 - were rewritten in the same commit, so the gate arrives green over a tree that was cleared rather than - green over a tree nobody measured. - **What lands on `main` here is a repo setting, read rather than assumed, and it differs from `mllp`: - all THREE merge methods are enabled** (`allow_squash_merge`, `allow_merge_commit`, - `allow_rebase_merge`). Squash uses `squash_merge_commit_title: COMMIT_OR_PR_TITLE` and - `squash_merge_commit_message: COMMIT_MESSAGES`; merge and rebase land the branch commit messages - verbatim. So **the branch commit messages are the one text that lands under every enabled method**, - the PR title lands under two of three, and **the PR body lands under none**. It is scanned anyway as - deliberate over-strictness. Do not repeat `ncpdp`'s copy, which says "the PR title and body are what - lands": the body never lands anywhere. - **The script is composed from three copies, and the composition is the thing to understand before - editing it.** Base: `website`'s **NUL-exclusion** shape. Plus `ncpdp`'s two route fixes (a tracked - file named exactly `-` was read as **standard input** and never opened, so the gate printed OK over a - live em dash; `-d skip` silently passed a tracked **symlink to a directory**). Plus `dicom`'s - **binary-match diagnostic branch**. The `./` prefix is applied in the list-building **loop**, not via - `sed -z`, so the scan is a single command with the stderr capture bound to all of it, and there is no - GNU-only `sed` dependency that has no self-test. - **Why the NUL-exclusion shape here, and unlike `mllp` the reason is not a hypothetical.** This repo - tracks **eleven** binaries: the ten `vendor/cosyte-*.tgz` packed siblings (an `npx` bin cannot - peer-depend, so the parsers are hard vendored deps) and `test/__fixtures__/sample.dcm`. Measured on - this tree: **`vendor/cosyte-hl7-0.0.0.tgz` already contains the byte sequence `E2 80 94`** (one - occurrence, offset 50217 of 665534 bytes, in a DEFLATE stream). A text-only port therefore reds on - this repo **today**, naming a compressed byte stream nobody wrote, and **that red has no - remediation**: you cannot rewrite compressed bytes with a period. A gate whose red state has no fix - is a gate someone disables. **Do not add `grep -I` instead**: measured on GNU grep 3.8, a text file - whose bad byte sits on the same line as the em dash is skipped by `-I` in total silence (no stdout, - no stderr, exit 0) and the gate prints OK. - **The disclosed cost, said plainly: a tracked TEXT file holding a NUL byte is silently exempt, and - seeding a vendored tarball with a live em dash leaves this gate green. That is a miss, not a pass.** - Verified, along with the control that proves the NUL rule causes it (the same tarball bytes with - every NUL stripped, carrying the same em dash, goes RED). `cli` has **no** NUL-bearing text file - today, checked over all 124 tracked files rather than assumed, so the exclusion currently exempts - exactly eleven files and all eleven are genuine binaries. **Do not round that off to "hypothetical - here."** `git ls-files --eol` calls **thirteen** files binary, not eleven: the other two are - `test/__fixtures__/adt-a01.hl7` and `test/__fixtures__/minimal.astm`, which git classifies on its - lone-CR branch because an HL7 v2 and an ASTM segment terminator is `CR` with no `LF`. Those two hold - **zero** NUL bytes, so they stay in scope, and that was **proved, not assumed**: each was seeded with - a live em dash in turn and the gate went red naming the file. `cli` wraps eight parsers and its - `test/__fixtures__/` corpus is the obvious place a NUL-bearing text fixture would arrive. **The tell - is the excluded count on the OK line: it reads 11 today.** If one lands, revisit the partition (the - `.gitattributes` declaration `pathways` prefers, which this repo cannot use because it declares no - attributes at all), never the ban. - One disclosed property of a red on those two fixtures: the hit echoes the matching _line_, and a - CR-delimited frame is one line, so a whole message lands in a public CI log. Acceptable and - deliberately un-truncated, because those fixtures are synthetic by policy and `pnpm phi-scan` gates - that policy over the same files. Remaining known limits (encoded-form matching is literal, so - `%e2%80%94`, `—` and `—` pass; the scan reads contents and never file names) are in the - script header and are **shared across every copy, so fix them there, not here**: a divergent copy is - worse than a shared known limit. - **▶ READ THIS BEFORE PORTING THIS GATE ANYWHERE ELSE. AN EM DASH IS SOMETIMES A VALUE, NOT - PUNCTUATION, AND A BULK SWEEP WILL SILENTLY EDIT A CAPABILITY CLAIM.** The refuter caught it here and - it is the single most valuable thing this port produced. In `docs-content/limitations.md` the - per-(format, operation) support matrix used a bare em dash (`U+2014`) **as a cell value meaning - "not supported"**. - The sweep rewrote it as punctuation, so `dicom`.`fmt`, `mllp`.`fmt` and `mllp`.`validate` rendered as - a stray colon. **That converts "support absent" into "support unstated", on the one page whose entire - job is honest capability disclosure**, and it reads as a rendering artifact rather than as a claim, so - a reader does not even know something is missing. It is the same family as this repo's own rule for - the public-surface gate: **CUT, do not rewrite; softening a stated limit into an implied capability - while tidying a sentence is a worse defect than the thing being removed.** - **Nothing in this repo's CI could have caught it**, and that is the part to carry: `test/docs-content.test.ts` - only executes ` ```ts runnable ` blocks, and `prettier`'s glob covers `src/**`, `test/**`, `scripts/**` - and root `*.{json,md,yml}` but **NOT `docs-content/`**. Fixed by replacing the marker with the words - `not supported`. **Before sweeping any repo, grep for an em dash used as a table cell or a list marker - first** (`\|\s*\x{2014}\s*[\|\(]` found exactly the three lines here) and convert those to a WORD, - by hand, before running any bulk transform. A marker that means "no" must survive as a word, never as - punctuation. - **A second measurement lesson from the same review, because this item's history is wrong counts - propagating through briefs:** the first draft of these notes said "37 of the 87 are not markdown", - asserted as measured fact inside the sentence arguing for measurement rigor. It was wrong. **Correct - and final: 659 occurrences across 87 of 124 tracked files; 26 of those files are markdown and 61 are - not; 37 is the non-markdown AND non-`test/` subset.** Re-derive numbers before writing them down, and - never quote them from a sibling repo's copy of this note. - **Pre-existing and correctly not retroactive:** commit subjects already on `main` may carry `U+2014`. - The message half only runs on `pull_request`. History is not rewritten. - -- **The published manifest is installable again, and `VERSION` no longer lies.** `0.0.1` and `0.0.2` - are permanently broken on npm (`file:vendor/*.tgz` specifiers in the published manifest, `ENOENT` on - every install route) and both shipped `VERSION = "0.0.0"` against a `0.0.2` manifest, which - `cosyte --version` and the MCP `serverInfo.version` both printed. Fixed together: - **`scripts/sync-version.mjs`** now runs inside the `version` script and rewrites **two** targets, - `src/core/version.ts` and the asserted literal in `docs-content/installation.md`; `test/sanity.test.ts` - compares the export against `package.json` **and** pins the declaration's `: string` shape, which the - script's pattern keys on. **Fix the ASSERTION, not just the value:** the docs block asserted - `typeof VERSION`, which is true of every wrong value and green-lit both bad releases. - **The dep swap is done except for one dependency, and the exception is the interesting part.** - `@cosyte/hl7` (`^0.0.7`) and `@cosyte/terminology` (`^0.0.9`) are hard deps at real ranges; the six - breadth parsers and `@cosyte/transform` (`^0.0.4`) are `optionalDependencies` at real ranges. - **`@cosyte/fhir` is not declared at all**, because it is not on the registry and, measured here, - declaring it in ANY form alongside `@cosyte/transform` (whose `@cosyte/fhir` peer is mandatory) - fails the whole install with `ERESOLVE`: optional dep and optional _peer_ both. Either one alone - installs clean; the pair does not. **Do not explain this with a missing - `peerDependenciesMeta.optional` flag** - measured across the suite, that flag does not decide the - outcome, and the mechanism is unexplained. `@cosyte/fhir` is kept as a **`devDependency`** on the - vendored tarball so this repo's own FHIR/`convert` tests run and so `transform`'s peer resolves in - the dev tree. Consequence, stated on every consumer surface rather than discovered: **an installed - copy has no FHIR support**, and FHIR `parse`/`inspect`/`fmt`/`validate` plus `convert` degrade to a - value-free `CLI_PARSER_UNAVAILABLE` (69). That required `loadOptionalPackage(detail, load)` beneath - `loadOptional` (which takes a `CosyteFormat` and hardcodes the word "parser", wrong for both cases) - plus `loadFhir()`; `test/absent-sibling.test.ts` includes a **static guard** that reds on a new - **single-line, unwrapped** `import("@cosyte/fhir")` / `import("@cosyte/transform")` in `src/`, which - is the shape the defect took. **Do not write "any new call site": a refuter falsified that wording - by adding a thunk assigned to a variable, and the suite stayed 10/10 green.** It also misses a - multi-line import and a **static** `import … from "@cosyte/fhir"` - and this repo now HAS the first - static reference to that package (`src/core/parsers.ts`, `import type`, erased at build, verified - absent from `dist/`). Dropping the word `type` loads it eagerly and breaks every command in an - installed copy, unseen by the guard. Also note the two diagnostics deliberately do NOT say - "install it": `npm install @cosyte/transform` fails `E404` on its own `fhir` peer, so that advice - would send a user at a command that cannot succeed. `loadOptional()`'s stock wording says exactly - that, which is why neither goes through it. - **Verified by installing, which a `--dry-run` cannot do**: pack, `npm install` the tarball in a clean - directory outside the repo (exit 0), run both bins, import `.` under ESM and CJS. Negative control: - the published `0.0.2` still `ENOENT`s. Keep that step; it is checklist step 6 in `RELEASING.md`. - **A THIRD fault is real, pre-existing, and NOT fixed by any of this: `npx @cosyte/cli …` fails with - `could not determine executable to run`.** `npx` runs the bin matching the package name's last - segment (`cli`); this package ships `cosyte` and `cosyte-mcp`. The `bin` block is byte-identical to - the published `0.0.2`, so the swap cannot have changed it. Docs now say - `npx --package @cosyte/cli cosyte …` (measured working). **A `cli` bin alias would fix it and is - deliberately not added** - `npm install -g` would then claim the name `cli` on the user's `PATH`. - That trade is a founder call. -- **Phase 7 shipped (CLI-7): release hardening: the final roadmap phase. The CLI is feature-complete.** - No new runtime command surface; this phase is publish-readiness. **Fuzz** over the CLI's two input - boundaries: the terminal (`run`, over argv plus stdin bytes) and the agent surface (`dispatchTool`, - over a tool name plus arguments): proving neither ever throws or leaks a stack frame - (`test/fuzz.property.test.ts`, scaled by `CLI_FUZZ_RUNS`, run nightly by a scheduled **Fuzz** workflow - and on demand via `pnpm test:fuzz`). The **exit-code golden matrix** (`test/exit-code-matrix.test.ts`) - locks one representative invocation for every code in the `0/1/2/65/66/69/70` contract as a stability - surface. **Publish dry-run proven:** `attw` green, a new `smoke` gate (`scripts/smoke.mjs`, in - `verify.sh`) exercising the built dual ESM/CJS `.` and `./mcp` subpaths **and both bins** under `node`, - and a clean `npm publish --dry-run` tarball. **Honesty docs:** `docs-content/limitations.md` - (wraps-not-implements, the non-goals, the per-(format, op) matrix, the PHI posture) and a - man-page-style `docs-content/reference-commands.md`; plus `RELEASING.md` (the two-bin publish, - provenance/OIDC, the vendor→npm swap, the two founder stops). **Founder-gated tail (NOT crossed):** the - real `npm publish` and the repo public-flip remain the two standing human stops, and, unique to this - `bin`, the vendored `file:` sibling deps must become real `@cosyte/*` npm ranges at `PUB-FLIP` (a - published package cannot ship a `file:` dep). Everything up to those is done. -- **Phase 6 shipped** (`operations/roadmaps/cli.md` §Phase 6). **Six more formats + streaming + shell - completion** (ADR 0025). The CLI now wraps **all eight** cosyte formats through a single lazy - **per-format adapter registry** (`src/core/parsers.ts`) that replaced the per-command `hl7 ? : fhir` - branches and makes support **per (format, operation)** via `OP_SUPPORT`: an unsupported (format, op) - is a value-free `CLI_FORMAT_UNSUPPORTED`, never a fake. Capabilities: `x12`/`astm`/`ncpdp` → - parse+inspect+fmt+validate; `ccda` → inspect+fmt(XML)+validate (parse deferred, no library JSON - model); `dicom` → inspect+validate (parse/fmt deferred: binary model); `mllp` → parse+inspect (a - transport container de-framed to its enclosed HL7). Autodetection covers all eight (conservative + - fail-safe: a co-match is a _detected_ ambiguity, never a mis-route). **Streaming:** `parse` emits - **NDJSON** with per-record isolation for MLLP frames and the - new **`--ndjson`** input mode (a failed record is a value-free `{record,error}` line; any failure → - exit `65`). **`cosyte completion `** prints a static completion script. The six breadth - parsers are **`optionalDependencies`** (vendored, lazy per format, outside the hard-dep closure: ADR - 0025), so the umbrella `verify-policy` `cli` cap **stays 4**; an absent optional parser degrades to a - value-free **`CLI_PARSER_UNAVAILABLE`** (exit `69`). New diagnostic `CLI_PARSER_UNAVAILABLE`; the public - `WIRED_FORMATS` set is replaced by the per-op `OP_SUPPORT` matrix. -- **Phase 5 shipped** (`operations/roadmaps/cli.md` §Phase 5). Adds the **`cosyte-mcp` MCP server**: the - **agent front door** and the _second adapter_ over the one shared `core` (ADR 0022). A **stdio** Model - Context Protocol server on `@modelcontextprotocol/sdk`, reachable three ways: the new **`cosyte-mcp`** - bin, the **`cosyte mcp`** subcommand, and the **`@cosyte/cli/mcp`** subpath export. It exposes four - tools (**`parse`/`validate`/`inspect`/`convert`**) each a thin wrapper that calls the same command - handler the terminal uses (with `--json`), so `cosyte parse` and the MCP `parse` tool agree by - construction; the CLI re-implements nothing. Every tool runs **value-free** (no `--unsafe-show-values` - door on the agent surface): a tool _result_ carries the requested data, a tool _error_ carries only the - value-free diagnostic, and a parsed-but-invalid `validate` verdict is a **successful** call reporting - the verdict: only a hard failure sets `isError`. The **SDK is isolated and runtime-optional** (ADR - 0024): it is the CLI's first and only third-party runtime dep, declared in **`optionalDependencies`** - (pinned `1.29.0`), imported only in `src/mcp/server.ts`, and reachable solely via the `./mcp` - boundary: a `cosyte parse` invocation never loads it (proven by `test/mcp-isolation.test.ts`), and the core - works with the SDK absent. Because it is not in the hard runtime closure, the umbrella `verify-policy` - runtime-dep cap on `cli` stays **4**. `redact`/`deid` and `map-codes` are deliberately not yet exposed - as tools. New exports (on `./mcp`): `createMcpServer`, `startStdioServer`, `dispatchTool`, `TOOL_DEFS`. -- **Phase 4 shipped** (`operations/roadmaps/cli.md` §Phase 4). Adds the two **consumer-of-consumers** - commands, each a thin wrapper that re-implements no library logic: **`convert --to fhir`** - (HL7 v2 → FHIR R4 via **`@cosyte/transform`**: parse with `hl7`, `toFhir`, serialize with `fhir`; - the `Bundle` on stdout, value-free issues on stderr, an **error-severity issue drives exit `1`**, a - non-HL7 source is `CLI_FORMAT_UNSUPPORTED`/`65`) and **`map-codes --code … [--system -…]`** (ConceptMap `$translate` via **`@cosyte/terminology`**, BYO ConceptMap: a match → target - coding(s) + exit `0`; unmapped → `TERM_TRANSLATE_UNMAPPED` + exit `1`; an unloadable map → the new - **`CLI_MAP_INVALID`**/`65`). Both siblings are **hard, first-party, lazy-loaded** runtime deps - (vendored tarballs; the umbrella dep cap was raised **2 → 4**: ADR 0023). New exports: - `convertCommand`, `convertOutcome`, `mapCodesCommand`. -- **Phase 3 shipped** (`operations/roadmaps/cli.md` §Phase 3). Adds three commands over the two wired - parsers: **`validate`** (parse + the wrapped parser's own validation surface, **verdict in the exit - code**: `0` valid / `1` invalid / `65` unparseable; findings value-free; `--profile` gated to an - honest `CLI_NOT_IMPLEMENTED`/`69`; verdict never invented: FHIR = `validateResource().valid`, HL7 = - parseable), **`inspect`** (a value-free structural summary: message/resource type, segment/entry - counts, warning/issue count), and **`fmt`** (canonical re-serialization via the library's serializer; - stdout is the data channel; no partial emit on unparseable input). Adds **`EXIT.INVALID` (`1`)**: the - exit-code contract is now `0/1/2/65/66/69/70`. All four commands share one input+format front door - (`core/input.ts` `resolveInput`) and one value-free parser-failure boundary (`core/wrap.ts`), so the - value-free posture + `--unsafe-show-values` chokepoint stay uniform (`parse` refactored onto them, - behavior-preserving). -- **Phase 2 shipped** (`operations/roadmaps/cli.md` §Phase 2). Pre-alpha `0.0.x`, unpublished. On top - of Phase 1's `cosyte parse` (HL7 v2 + FHIR R4, content autodetection, exit-code contract, value-free - `CLI_*` diagnostics), Phase 2 hardens the PHI posture: the global opt-in **`--unsafe-show-values`** - (the single door to a value on a secondary surface, funnelled through one chokepoint in - `core/phi.ts`), a proven **never-a-PHI-temp-file** guarantee, and the **`redact`/`deid`** command as - an honest, `@cosyte/deid`-gated `CLI_NOT_IMPLEMENTED` (exit `69`), never a built-in partial scrub - that would risk a false-safety impression. Exit-code contract is now `0/2/65/66/69/70`. -- **Phase 1 shipped** (§Phase 1). `cosyte parse ` for **HL7 v2** + **FHIR R4**, **content - format autodetection** (conservative, fail-safe, never a guessed parser), the documented - **exit-code contract**, and the **value-free diagnostic** channel with stable `CLI_*` codes. -- **Hard runtime deps (ADR 0021 + 0023), as they stand AFTER the vendor → npm swap:** only - **`@cosyte/hl7` (`^0.0.7`) + `@cosyte/terminology` (`^0.0.9`)** are hard `dependencies` now, both - real registry ranges (an `npx` bin can't peer-depend). `@cosyte/transform` moved to - `optionalDependencies` and **`@cosyte/fhir` is undeclared** - see the swap note above for why, and - do not "restore" either without reading it. That is **2** hard runtime deps against an umbrella - `verify-policy.json` cap of **4**, so it is under the cap, not at it. **Lazy-loaded per command.** - On the `0.0.x` ladder `^0.0.7` permits no other version, so these are effectively exact pins, and - Dependabot now sees them (it never could while they were `file:` specs). `vendor/` survives only to - supply `@cosyte/fhir` as a **`devDependency`**; the other nine tarballs are refreshed by - `pnpm vendor:refresh` but wired to nothing, and removing them is a deliberate separate cleanup. - Third-party CLI-core runtime deps: **zero**. The MCP server's **`@modelcontextprotocol/sdk`** is the - CLI's only third-party runtime dep: declared in **`optionalDependencies`** (not `dependencies`), - isolated behind `./mcp`, so it is outside the hard-closure cap (ADR 0024). -- **Deferred:** the roadmap's build phases are complete (P7 release hardening shipped). Per-(format, op) - cells remain deferred honestly (never faked): `dicom` - `parse`/`fmt` (binary model), `ccda` `parse` (XML is the canonical `fmt` surface), `mllp` `fmt`/`validate`. - The MCP tool set covers `parse`/`validate`/`inspect`/`convert`; `redact`/`map-codes` tools and - remote/HTTP MCP are later. `redact`'s real de-identification is deferred to when `@cosyte/deid` ships - (P2 landed the gated stub + seam). `validate --profile` is reserved but gated (`CLI_NOT_IMPLEMENTED`/`69`) - until the CLI can load a profile, no profiles are bundled. -- **ADRs:** `documentation/decisions/0021` (dependency-tier: a `bin` hard-deps first-party siblings), - `0022` (one-repo-two-bins: CLI + MCP over one core; web playground out of scope), `0023` (wire - `transform` + `terminology` for `convert`/`map-codes`; the deliberate 2 → 4 dep-cap raise), `0024` - (the Phase-5 MCP server; the SDK as an isolated, runtime-optional dependency: hard-dep cap stays 4), - and `0025` (the Phase-6 breadth parsers as runtime-optional lazy deps outside the cap; the cap stays 4). +**Feature-complete.** Phase 7 (release hardening) shipped and was the roadmap's final phase; no new +runtime command surface is planned. The CLI wraps **all eight** cosyte formats through one lazy +per-format adapter registry (`src/core/parsers.ts`), exposes the same `core` through a terminal bin +(`cosyte`), an MCP server bin (`cosyte-mcp`) and the `.` / `./mcp` subpath exports, and states support +**per (format, operation)** via `OP_SUPPORT`: an unsupported cell is a value-free +`CLI_FORMAT_UNSUPPORTED`, never a fake. Exit-code contract: `0/1/2/65/66/69/70`. Per-phase histories: +[agent-notes § Shipped phases](documentation/agent-notes.md#shipped-phases). + +**Deferred, honestly and never faked:** `dicom` `parse`/`fmt` (binary model), `ccda` `parse` (XML is +the canonical `fmt` surface), `mllp` `fmt`/`validate`; `redact`/`deid` and `map-codes` MCP tools and +remote/HTTP MCP, deliberately not yet exposed; `redact`'s real de-identification (gated stub + seam +landed, waiting on `@cosyte/deid`, and **never a built-in partial scrub**, which would risk a +false-safety impression); `validate --profile` (reserved, `CLI_NOT_IMPLEMENTED`/`69`, no profiles +bundled). Detail: +[agent-notes § Deferred](documentation/agent-notes.md#deferred). + +**ADRs:** `documentation/decisions/0021` (a `bin` hard-deps first-party siblings), `0022` (one repo, +two bins over one core), `0023` (wire `transform` + `terminology`; the 2 → 4 dep-cap raise), `0024` +(the MCP SDK as an isolated runtime-optional dep), `0025` (breadth parsers optional, outside the cap). +Summaries: [agent-notes § ADRs](documentation/agent-notes.md#adrs). + +### The published package, and the FHIR hole (live, unresolved) + +Why: [agent-notes § The vendor to npm dependency swap](documentation/agent-notes.md#the-vendor-to-npm-dependency-swap). + +- **`@cosyte/fhir` is deliberately NOT in the manifest, in any form, and you must not add it.** + `fhir` cannot publish at all (`FHIR-NPM-NAME`), and, measured here, declaring it alongside + `@cosyte/transform` fails the whole install with `ERESOLVE`. **This is not a manifest bug to fix.** +- **Do not explain that `ERESOLVE` with a missing `peerDependenciesMeta.optional` flag.** Measured + across the suite: that flag does not decide the outcome, and the mechanism is unexplained. +- **An installed copy therefore has NO FHIR support**, stated on every consumer surface rather than + discovered: FHIR `parse`/`inspect`/`fmt`/`validate` and `convert` degrade to a value-free + `CLI_PARSER_UNAVAILABLE` (`69`). `@cosyte/fhir` survives only as a **`devDependency`** on the + vendored tarball, so this repo's own FHIR tests run. +- **Never quote the published version in this file.** Derive it: `npm view @cosyte/cli version`. A + `0.0.3` pin was still being quoted elsewhere on 2026-08-04 after `0.0.4` had shipped, which is the + exact defect ADR 0023 deleted the umbrella's version list over. +- **`0.0.1` and `0.0.2` are permanently broken on npm** (`file:vendor/*.tgz` specifiers, `ENOENT` on + every install route) and both printed `VERSION = "0.0.0"`. Never move a published version backwards. +- **Fix the ASSERTION, not just the value.** The installation doc asserted `typeof VERSION`, true of + every wrong value, and green-lit both bad releases. `scripts/sync-version.mjs` rewrites two targets + and `test/sanity.test.ts` pins the `: string` declaration shape the script keys on. +- **Never write "any new call site" of the `absent-sibling` static guard: a refuter falsified that + wording.** It reds only on a NEW **single-line, unwrapped** dynamic `import()`; it misses a + multi-line import and a static one. `src/core/parsers.ts` holds an `import type` of `@cosyte/fhir`: + **dropping the word `type` loads it eagerly and breaks every command in an installed copy, unseen.** +- **The two unavailable-parser diagnostics deliberately do NOT say "install it"** (and so do not go + through `loadOptional()`'s stock wording): `npm install @cosyte/transform` fails `E404` on its own + `fhir` peer, so that advice sends a user at a command that cannot succeed. +- **Verify a release by INSTALLING the packed tarball outside the repo, never by `--dry-run`.** It is + checklist step 6 in `RELEASING.md`; keep it. +- **`npx @cosyte/cli …` fails** with `could not determine executable to run` (the bins are `cosyte` + and `cosyte-mcp`). Use `npx --package @cosyte/cli cosyte …`. **A `cli` bin alias would fix it and is + deliberately not added** (`npm install -g` would claim the name `cli` on the user's `PATH`): founder + call, not an oversight. +- **The public-flip stop is not yours to cross, and both original stops are already behind this + package.** It is + **public** (`gh repo view cosyte/cli --json visibility`) and it has published, so the original + "founder-gated tail (NOT crossed)" note is superseded, not still pending. **Flipping a repo's + visibility is never waived**, so an agent still does not touch it; the `npm publish` half is + covered by a standing founder directive. The vendored `file:` sibling deps that gated a publish (a + published package cannot ship a `file:` dep) are already real npm ranges, except `@cosyte/fhir`, + which stays a `file:` **devDependency** and never reaches a consumer's install: see the swap note. + +### Hard runtime deps + +- **Two hard `dependencies`: `@cosyte/hl7` (`^0.0.7`) + `@cosyte/terminology` (`^0.0.9`)**, real + registry ranges, lazy-loaded per command. That is **2** against the umbrella `verify-policy.json` + cap of **4**: under it, not at it. On the `0.0.x` ladder those ranges are effectively exact pins. +- **Do not "restore" `@cosyte/transform` to `dependencies` or declare `@cosyte/fhir`** without reading + the swap note first. Everything else (`transform`, the six breadth parsers, the MCP SDK) is an + `optionalDependency` outside the cap. Third-party CLI-core runtime deps: **zero**. +- `vendor/` survives only to supply `@cosyte/fhir` as a devDependency; the other nine tarballs are + refreshed by `pnpm vendor:refresh` but wired to nothing. Removing them is a separate cleanup. + Detail: [agent-notes § Hard runtime deps](documentation/agent-notes.md#hard-runtime-deps). + +### The docs sidebar is bound by an IA spine nothing here checks + +Why: [agent-notes § The docs sidebar and the IA spine](documentation/agent-notes.md#the-docs-sidebar-and-the-ia-spine). + +- **An off-spine top-level label in `docs-content/sidebars.json` stops the WHOLE docs site + deploying**, and this package once held it down for four days. Canonical top-level order: + `Overview` (the `intro` **doc reference**, not a category), `Installation`, `Quickstart`, + `Core Concepts`, `Guides`, `API Reference`, `Troubleshooting`. Categories are **optional**; the rule + is that whatever you have is labelled and ordered canonically, so `{"docs":["intro"]}` is compliant. +- **🔴 NEVER AUTHOR AN `API Reference` CATEGORY.** The docs site injects it. A hand-authored one is a + distinct, **harder** error than the off-spine label it would be replacing. +- **Never claim where that injected category lands.** A refuter falsified "just before + Troubleshooting" here: two code paths disagree and the served one **appends**. It is a `docs` + defect, cosmetic, affecting every released package. State only that this package neither authors nor + positions it. **"Verified against the linter" is not evidence about placement**, because the linter + refuses `versioned_sidebars/`. +- **Verify a sidebar edit against the site's own linter**, `docs/scripts/check-ia-conformance.ts` + (self-executes on import, so `NODE_ENV=test` is required to call `lintSidebar`). **Nothing in + `verify.sh` or this repo's CI can catch this**: `pack:docs` checks only that the files EXIST. + **Use the previously shipped sidebar as a negative control**: if it reports no errors, the probe is + wrong, not the sidebar. +- Keep `limitations` under `Troubleshooting` and `mcp`/`reference-commands` under `Guides`. **Do not + copy `synth`**, which is the lone counter-example. +- **Only a NEW RELEASE clears the gate.** It reads the shipped artifact and releases are immutable, so + every published version keeps its sidebar forever. Archived versions are reported at `info` and + never gate, so **do not try to fix history.** + +### The pre-commit PHI scanner + +Why: [agent-notes § The pre-commit PHI gate and git mv](documentation/agent-notes.md#the-pre-commit-phi-gate-and-git-mv). + +- **Never re-introduce rename detection into `scripts/phi-scan.ts --staged`.** Detection is on by + default, a `git mv` into a fixture directory stages as a two-path `R100`/`C100` that the status + filter drops, and both a regular file and a link passed the hook at **exit 0**. `--no-renames` is + the whole remedy and is a strict **SUPERSET**, verified under `diff.renames` = `true` / `copies` / + `false` / `1` and `diff.renameLimit=1` (no `R` or `C` survives any of them, which is what makes the + `--raw -z` stride structural rather than conditional; `copies` is not hypothetical here and emits a + live `C100`). +- **Do NOT re-derive this as "needs the two-path record shape, a scope decision".** That framing was + ported from a sibling, is false, and has now been measured twice (`dicom`, then here). +- **Keep the `--raw -z` two-field stride, the `T` status, the destination-mode read, and the + **exit 2** refusal of a non-blob mode.** `git show :` answers a link with its **target path as + though it were content**, so a mode-blind scan reads path text and never the target's bytes. +- **Each scan root's own path is in scope**, because git records no index entry for a directory. +- **Keep the two routes' refusals identical.** The all-mode walk got the same link refusal as + `--staged`, because a scanner whose pre-commit half refuses a link while its CI half silently drops + one cannot be reasoned about. **The `.md` exemption deliberately does not reach a link.** +- **A refusal NEVER prints the link target** (a target path is itself a PHI surface); the entry's own + path is printed deliberately and every offender is named. **That guarantee is about a REFUSAL and + does not extend to a hit.** +- **Exit `1` means "hits found", so a broken invocation must never exit `1`.** An unreadable + allow-list or scan root is **exit 2**; both used to throw past every handler and read as a finding. +- **Never state the refusal rule unqualified.** It is scoped to an **enumerated** entry; a refuter + falsified "neither route follows such an entry" using this very file. +- **Three escapes are PRE-EXISTING, measured, and deliberately NOT closed** (a scan root that is + itself a live link is followed by the all-mode walk and prints values under a fabricated in-repo + path; an **ancestor** of a scan root is in neither route's scope; paths mode follows a named link). + **Do not "fix" them inside an unrelated slice**: closing them needs a + refuse-a-scan-that-observed-nothing rule plus a decision about how far above a root to look. +- **Other residuals, also not closed:** `D` and `U` are unenumerated (`U` costs nothing that can reach + a commit: `git commit` refuses an unmerged index, exit 128); under `src/` the staged route covers + `.ts` only while the all-mode walk covers every non-`.md` file, and the CI sweep is the cover. +- **Give `test/scripts/phi-scan.test.ts` explicit timeouts.** Each case spawns `tsx` cold: 0.5s idle, + **3.7s under contention**, against a shared 10s default. +- **Assert the premise, not only the remedy.** Two vacuity traps already sprang in this suite: a + fixture whose `git merge` refused on an identity-less runner so every later assertion held over an + empty result, and a loop that asserted only the detection-OFF side. + +### The em-dash brand gate + +Why: [agent-notes § The em-dash brand gate](documentation/agent-notes.md#the-em-dash-brand-gate). + +- **The `U+2014` ban is absolute** (founder directive; `knowledgebase/06-brand/voice-and-tone.md`). + `scripts/check-no-emdash.sh` + `.github/workflows/no-emdash.yml` gate **both** halves: every tracked + file, **and** the PR title, body and commit messages, on the `edited` trigger. +- **▶ READ THIS BEFORE PORTING THE GATE OR SWEEPING ANY REPO: AN EM DASH IS SOMETIMES A VALUE, NOT + PUNCTUATION.** `docs-content/limitations.md` used a bare `U+2014` as a support-matrix **cell value + meaning "not supported"**; the sweep rewrote it as punctuation and turned **"support absent" into + "support unstated"**, on the one page whose entire job is honest capability disclosure, reading as a + rendering artifact rather than a claim. **Grep for a cell or list-marker em dash first** + (`\|\s*\x{2014}\s*[\|\(]`) and convert each to a **WORD**, by hand, before any bulk transform. +- **CUT, do not rewrite.** Softening a stated limit into an implied capability while tidying a + sentence is a worse defect than the thing being removed. Revert a rewrite verbatim rather than + repair it. (Same rule as the public-surface gate below.) +- **Nothing in this repo's CI could have caught that**: `test/docs-content.test.ts` executes only + ` ```ts runnable ` blocks, and prettier's glob does **not** cover `docs-content/`. +- **Never drop the NUL exclusion and never add `grep -I` instead.** A vendored tarball already + contains the bytes `E2 80 94` inside a DEFLATE stream, so a text-only port reds today with **no + remediation**, and a gate whose red has no fix is a gate someone disables. `grep -I` skips a + qualifying text file in **total silence** and prints OK. +- **The disclosed cost is a miss, not a pass, and is not hypothetical here:** a tracked **text** file + holding a NUL byte is silently exempt. **The tell is the excluded count on the OK line: it reads 11 + today.** If it moves, revisit the partition, **never the ban**. +- **`test/__fixtures__/adt-a01.hl7` and `minimal.astm` stay in scope**: git calls them binary on its + lone-CR branch (an HL7 v2 / ASTM terminator is `CR` with no `LF`) but they hold zero NULs, proved by + seeding each with a live em dash. A red on those echoes a whole synthetic message into a public CI + log: deliberate and acceptable. +- **Fix the script's shared known limits in the shared copies, not here.** A divergent copy is worse + than a shared known limit. The script is composed from three siblings' fixes; understand the + composition before editing it. +- **Re-derive every number before writing it down and never quote one from a sibling's copy.** The + first draft of these notes stated a wrong count _inside the sentence arguing for measurement rigor_. +- **What lands on `main` here differs from `mllp`: all three merge methods are enabled**, so the + **branch commit messages** are the one text that lands under every method, the PR title lands under + two of three, and **the PR body lands under none** (it is scanned anyway, as deliberate + over-strictness). **Do not repeat `ncpdp`'s copy** claiming the title and body are what lands. +- **Not retroactive:** commit subjects already on `main` may carry `U+2014`. History is not rewritten. ## Tech Stack (the shared `@cosyte/*` standard) @@ -381,90 +235,50 @@ a summary. - **Testing:** **Vitest 4** + v8 coverage (`@cosyte/vitest-config`), per-directory >= 90 gates on `src/core` + `src/commands`. Command-contract snapshots, an autodetection corpus + a fast-check property, the `parse == library-parse` equivalence, and a **PHI-leak matrix** (sentinel values - never on stderr, across `--json`/`--quiet`/verbose). CLI-7 adds an **argv+stdin+MCP fuzz** gate + never on stderr, across `--json`/`--quiet`/verbose). Plus an **argv+stdin+MCP fuzz** gate (`test:fuzz`, nightly-scaled by `CLI_FUZZ_RUNS`), an **exit-code golden matrix**, and a built-package **`smoke`** (dual ESM/CJS `.` + `./mcp` subpaths and both bins under `node`). The thin `bin/` process adapter is coverage-excluded at source (a `/* v8 ignore */` block over the argv/stdin/exit glue). - **CI/CD:** thin callers of the reusable `cosyte/.github` workflows. **The checks BIND**; see "Branch protection" below. -- **Runtime deps:** `@cosyte/hl7` + `@cosyte/fhir` + `@cosyte/transform` + `@cosyte/terminology` - (first-party, hard, vendored: ADR 0021 + 0023), capped at **4**. The six CLI-6 breadth parsers - (`dicom`/`x12`/`ccda`/`ncpdp`/`astm`/`mllp`) are vendored **`optionalDependencies`**, lazy-loaded per - format and **outside** that cap (ADR 0025); the MCP `@modelcontextprotocol/sdk` is likewise optional - (ADR 0024). **Zero third-party** in the CLI core (`util.parseArgs`, no framework). +- **Runtime deps:** **`@cosyte/hl7` + `@cosyte/terminology` only** as hard deps (ADR 0021 + 0023), + against a cap of **4**. `@cosyte/transform`, the six breadth parsers and the MCP + `@modelcontextprotocol/sdk` are `optionalDependencies`, lazy-loaded and **outside** that cap (ADR + 0024 + 0025); **`@cosyte/fhir` is undeclared**, see Status. **Zero third-party** in the CLI core + (`util.parseArgs`, no framework). - **License:** MIT. ## Branch protection (and the limits of this claim) -`main` is protected by the repository ruleset **`ci-required-checks`** (id `19907924`, -`source_type: Repository`, `enforcement: active`, conditions `~DEFAULT_BRANCH`). Rules: `deletion`, -`non_fast_forward`, `required_status_checks`. Before it existed, every check this repo ran was -advisory: `ci`, `codeql`, `scorecard` and `fuzz` could all be red and the merge still landed on -`main`, and `main` is the branch that publishes. - -Required contexts, each pinned to **`integration_id: 15368`** (the `github-actions` app) so that a -commit status of the same name posted by any other actor with write access cannot satisfy it: - -| context | -| ------------------------------------------ | -| `ci / verify (22, ubuntu-latest)` | -| `ci / verify (24, ubuntu-latest)` | -| `ci / actionlint` | -| `codeql / analyze (javascript-typescript)` | -| `no-internal-refs` | -| `no-emdash` | - -These are the names GitHub actually reports, read off real check runs, **not** off a workflow's -`name:` field. Requiring a context nothing emits does not fail a PR; it leaves it pending and -unmergeable forever. None of `ci.yml`, `codeql.yml`, `no-internal-refs.yml` or `no-emdash.yml` -carries a `paths:` filter, so no PR can skip one. - -**`no-internal-refs` is the one that is NOT ` / `, and the shape is worth knowing.** -`ci / verify (22, ubuntu-latest)` is prefixed because `verify` runs inside a _called_ reusable -workflow, so the context is ` / (matrix)`. `no-internal-refs` is an -ordinary job in this repo's own workflow, so its check-run name is just the **job id**. That means -**renaming the job silently detaches the required check**: the ruleset keeps naming a context nothing -emits, every PR goes pending forever, and nothing errors or warns. Rename the job and the ruleset -together, or neither. It was added to the ruleset on 2026-07-28, after the first real check run -existed and its name was read back off that run. - -**`no-emdash` is the second of that shape, added 2026-07-28 the same way.** Its workflow is titled -`Em-dash gate` and a PR's checks list renders it as `Em-dash gate / no-emdash`, but **the context -GitHub reports is the bare job id `no-emdash`**, which is what the ruleset names. Requiring either of -the other two strings would leave every PR pending forever while the ruleset looked configured. The -name was read off a real `pull_request` check run on `#20` before the ruleset was written, never off -the workflow file, and writability was confirmed with the `PUT` itself rather than a `GET` (an -Organization-sourced ruleset returns `200` to a `GET` and `404` to an identical-payload `PUT`; -`19907924` is `source_type: Repository`, and it is the only ruleset this repo has). - -**What is deliberately NOT required, and why each would be a defect:** - -- **`scorecard / analysis`** runs on `push` to `main` and on a schedule, never on `pull_request`. - Requiring it would strand every PR pending forever. -- **`fuzz`** is `schedule` + `workflow_dispatch` only, for the same reason. The same property suite - runs inside `ci / verify` at a lower case count, so the PR path is covered by a context that - does arrive. -- **`release / release`** runs on `push` to `main`. It is not a PR gate. -- The **`CodeQL`** check posted by the Advanced Security app (id `57789`) reports **alert state**, - not whether the analysis ran. `codeql / analyze` already gates that. - -**A required job gates all of its steps.** Splitting a step out of `ci / verify` into its own job -silently un-requires it, with no error and no warning. There is a banner on `ci.yml` where someone -would trip it. - -**▶ Scope of the claim, stated plainly: a ruleset makes a red check BLOCK a merge. It does not make -the check correct, and nothing inside this repository can observe its own ruleset.** Delete the -ruleset and this test suite stays green while this section keeps asserting protection. It is not -verifiable from inside the repo, by `verify.sh`, or by any gate here. Verify it the only way that -works: - -```bash -gh api repos/cosyte/cli/rulesets -``` - -Two things recorded as **unproven** rather than fine: no fork PR has ever run here, so neither the -first-time-contributor approval gate nor whether `codeql / analyze` can report on a fork token -(which cannot hold `security-events: write`) has been observed. +Full ruleset, the required-context table and the per-check reasoning: +[agent-notes § Branch protection](documentation/agent-notes.md#branch-protection-and-the-limits-of-this-claim). + +- `main` is protected by the repository ruleset **`ci-required-checks`** (id `19907924`). Before it + existed every check here was advisory, on the branch that publishes. Six required contexts, each + pinned to **`integration_id: 15368`** so a same-named status from another actor cannot satisfy it. +- **Read a required context off a REAL check run, never off a workflow's `name:` field.** Requiring a + context nothing emits does not fail a PR: it leaves it **pending and unmergeable forever**, with no + error and no warning. +- **`no-internal-refs` and `no-emdash` are bare JOB IDS**, not ` / `, because they are + ordinary jobs in this repo's own workflows. **Renaming the job silently detaches the required + check.** Rename the job and the ruleset together, or neither. +- **A required job gates all of its steps.** Splitting a step out of `ci / verify` into its own job + silently un-requires it. There is a banner on `ci.yml` where someone would trip it. +- **Never add a `paths:` filter to `ci.yml`, `codeql.yml`, `no-internal-refs.yml` or + `no-emdash.yml`.** None carries one, which is what stops a PR skipping a required check. +- **Confirm a ruleset write with the `PUT` itself, never a `GET`.** An Organization-sourced ruleset + returns `200` to a `GET` and `404` to an identical-payload `PUT`. `19907924` is + `source_type: Repository` and is the only ruleset this repo has. +- **Never require `scorecard / analysis`, `fuzz`, or `release / release`**: none runs on + `pull_request`, so each would strand every PR pending forever. **Never require the Advanced Security + `CodeQL` check** (app id `57789`): it reports **alert state**, not whether the analysis ran. +- **▶ Scope of the claim: a ruleset makes a red check BLOCK a merge. It does not make the check + correct, and nothing inside this repository can observe its own ruleset.** Delete it and this suite + stays green while this section keeps asserting protection. Verify the only way that works: + `gh api repos/cosyte/cli/rulesets`. +- Recorded **unproven** rather than fine: no fork PR has ever run here, so neither the + first-time-contributor gate nor whether `codeql / analyze` can report on a fork token has been + observed. ## Engineering Guardrails @@ -483,65 +297,37 @@ first-time-contributor approval gate nor whether `codeql / analyze` can report o caught exception's message is discarded, never echoed. The CLI writes no temp file, logs to no file. - Coverage: per-directory >= 90% (lines/branches/functions/statements), enforced by `pnpm test:coverage`. -- **▶ `attw` SAYS "does not contain types" AND EXITS 0, SO THE `attw` SCRIPT IS A WRAPPER, NOT THE - BARE CLI.** `getExitCode.js` in `@arethetypeswrong/cli@0.18.4` opens with - `if (!analysis.types) return 0`, so the problem list is never consulted and no `--profile`, - `--ignore-rules` or config setting reaches that early return. For a package that ships types it - means the declarations were **not in the tarball**, which is a broken publish reported as a pass, - and `verify.sh` propagates the 0. **The race only supplies the condition.** Reproduced here with - **zero concurrency**, under the real `--pack . --profile node16`: `rm -rf dist` and, separately, - deleting all ten of a completed build's declaration files both print the sentence and exit 0. - `tsup` writes JS in one pass and declarations in a later one; instrumented on one build here at a - 10ms poll, all eight `.mjs`/`.cjs` files appeared on a single poll at 4.92s and all ten - declarations on a single later poll at 11.82s, a **6.90s window**. **Do not quote that interval as - a constant**: it moves with load (7.85s on a busier box). What does not move is the ordering, and - that the ten declarations land together. So the answer is **not** a lock, a lease or a build queue. - `scripts/attw.mjs` carries **two nets**: a preflight that every relative path `package.json` - promises exists and is non-empty (which names the missing file), and a post-check on the untyped - sentence (which catches declarations on disk but excluded from the tarball by `files`; no instance - of that is on record here). - **Three things specific to this repo, each measured, none inherited from the sibling this was - ported from:** - 1. **The preflight walks `bin` as well as `exports`.** `attw` never reads `bin`. With - `dist/bin/cosyte.mjs` deleted and everything else built, it printed every subpath green and - exited 0 over a tarball with no `cosyte` command in it. - 2. **`--profile node16` is load-bearing and is forwarded, never reinterpreted.** Without it, - `@cosyte/cli/mcp` fails `node10` resolution and `attw` exits 1. The wrapper passes every - argument straight through. - 3. **SIX PACKED-BUT-UNDECLARED DECLARATIONS DECIDE WHICH SILENCE YOU GET, AND THE OBVIOUS - TWO-LINE VERSION OF THIS IS FALSE.** `files: ["dist"]` packs all **ten** declarations `tsup` - emits; `package.json` names only **four** (`dist/index.d.*`, `dist/mcp.d.*`). The other six ride - along unnamed: `dist/io-.d.ts`/`.d.cts` **and the four `dist/bin/*.d.ts`/`.d.cts`**. - `analysis.types` is true if the tarball carries **any** declaration, so **any one of the six** is - enough. Measured, JS intact: the four declared gone with io-\* and bin/\* packed, **exit 1**; - also without io-\*, **exit 1**; also without bin/\* but with io-\* back, **exit 1**; all ten - gone, the untyped sentence and **exit 0**. So a **partial** loss is caught by `attw` itself and - only a **total** one is the false green, and the build window above is the total case. The - preflight therefore reports both outcomes and **must not assert the exit 0**. A first draft - named only the io chunk as the deciding file, measured it on a `bin`-less throwaway fixture, and - wrote the fixture's result down as this tree's; a refuter falsified it in one run. **Re-measure - before you shorten this.** - The post-check reads a string, so what would hide that string is **refused** rather than - tolerated: `--quiet`, `-q`, `--format`, `-f`, `--config-path`, and a `.attw.json` setting `quiet` - or `format` (`readConfig()` applies it after argv). Every one was measured here to remove the - sentence and still exit 0, `--config-path` included. **The refusal matches an EXACT ARGV TOKEN** - (or the part before an `=`), by option name and not by value. **Two disclosed holes, measured, - deliberately left open:** commander's attached and clustered short forms `-fjson` and `-Pf json` - get through and exit 0 over an untyped pack (`-qP` does not, the empty-transcript net catches - it), and a declared path not starting with `.` is skipped by the preflight. Neither is closed, - because the bare invocation this replaced exited 0 on that pack with **no** arguments at all, so - the gate is strictly better either way, and a short-option parser is a moving part the guard does - not need. **Of the ARGV refusal say "exact argv token", never "wholesale":** the stronger wording - was live in both the header and the printed message, and was refuted. The `.attw.json` refusal - **is** wholesale (key presence, any value) and that word is correct there; the two messages - differ on purpose. `test/scripts/attw-gate.test.ts` pins both nets, the upstream exit 0 itself, a - negative control on a well-formed package, and that a real `attw` failure still fails with attw's - own status. - **The vendored `file:` deps do not touch this.** `npm pack --dry-run` on a clean build lists - **30** entries and zero from `vendor/`, and no emitted declaration carries a `@cosyte/*` module - specifier. (A draft said 70. It was read off a stale `dist/`; a clean `pnpm clean && pnpm build` - gives 30, and 26 of those are `dist/`.) The published manifest being uninstallable is a real, - separate condition; this gate neither addresses it nor is shaped by it. + +### The `attw` gate + +Why, with every measurement: [agent-notes § The attw wrapper](documentation/agent-notes.md#the-attw-wrapper). + +- **▶ `attw` SAYS "does not contain types" AND EXITS 0, so `pnpm attw` is `node scripts/attw.mjs +--profile node16` and NEVER the bare CLI.** `getExitCode.js` opens with `if (!analysis.types) +return 0`, so no `--profile`, `--ignore-rules` or config setting reaches that early return, and a + broken publish is reported as a pass that `verify.sh` propagates. +- **The race only supplies the condition; the answer is NOT a lock, a lease or a build queue.** + `tsup` writes JS in one pass and declarations in a later one, so every build has the window. + **Do not quote the measured interval as a constant** (it moves with load); the ordering does not. +- **Keep the preflight walking `bin` as well as `exports`**: `attw` never reads `bin`, and with + `dist/bin/cosyte.mjs` deleted it printed every subpath green over a tarball with no `cosyte` in it. +- **`--profile node16` is load-bearing and is forwarded, never reinterpreted.** Without it + `@cosyte/cli/mcp` fails `node10` resolution. +- **Only a TOTAL loss of declarations is the false green; a PARTIAL one `attw` catches itself**, so + the preflight must report both outcomes and **must not assert the exit 0**. Six packed-but-undeclared + declarations decide which silence you get, and the obvious two-line version of this is false: a + first draft named one file, measured it on a throwaway fixture, wrote the fixture's result down as + this tree's, and a refuter falsified it in one run. **Re-measure before you shorten it.** +- **The post-check reads a string, so what would hide that string is refused**: `--quiet`, `-q`, + `--format`, `-f`, `--config-path`, and a `.attw.json` setting `quiet` or `format`. **Say "exact argv + token" of the ARGV refusal, never "wholesale"** (the stronger wording was live and was refuted); the + `.attw.json` refusal **is** wholesale, and the two messages differ on purpose. +- **Two holes are disclosed and deliberately left open**: clustered short forms `-fjson` / `-Pf json`, + and a declared path not starting with `.`. The bare invocation this replaced exited 0 with **no** + arguments at all, so the gate is strictly better either way and a short-option parser is a moving + part the guard does not need. +- `test/scripts/attw-gate.test.ts` pins both nets, the upstream exit 0 itself, a negative control, and + that a real `attw` failure still fails with `attw`'s own status. ## Standing disciplines (every change) @@ -561,41 +347,34 @@ Mirrors the three disciplines in the meta-repo's `documentation/conventions.md`. their editor renders on hover, and the diagnostic their terminal prints**) says what the software does and what changed. Item identifiers (`CLI-6`), phase and wave language, ADR numbers, roadmap citations (`cli roadmap §7`), meta-repo paths and "how this got built" commentary belong in the - changeset, `CHANGELOG.md`, the commit, the PR and the roadmap. It is a **translation** at the - boundary, not a deletion. Gated by `pnpm check:no-internal-refs`. - - **Four surfaces, three different answers.** `/** */` doc comments compile into `dist/*.d.ts` and - `dist/*.d.cts` and render on hover, so they are **gated**, and in this repo they were by far the - largest violating surface. String literals reach a consumer as terminal diagnostic text, so they - are **gated too**: this package printed an internal work item inside `CLI_NOT_IMPLEMENTED` and an - ADR number inside `CLI_PARSER_UNAVAILABLE` before that pass existed. `//` and plain `/* */` - comments are **not gated** and identifiers are **welcome** in them, because **the convention says - source comments are a place identifiers belong**. That is the whole reason. **Do not justify that - boundary from what reaches `dist/`**: two drafts of the `ncpdp` copy tried, a refuter proved both - false, and two drafts of this paragraph made the same mistake again. Measured on this tree, at - `06abc86`: `dist` is `files[0]`, there is no `.npmignore`, and **24 of the 27** tracked `src/` - files appear whole in a build map's `sourcesContent` (`src/index.ts`, `src/core/result.ts` and - `src/mcp/index.ts` contribute only re-exports and types, so the bundler erases them). Two - sentences that read well and are **false**, so do not write them: "everything in `src/` ships", - and "the bundles carry `//` comments verbatim" (measured: of the 43 whole-line `//` comments in - tracked `src/*.ts`, exactly **one** survives into any emitted `.mjs`/`.cjs`). **The boundary rests - on the convention, not on either fact.** The line is not what a consumer receives; it is what a - consumer is **shown**. - - **This repo is where the `WORD-N` trap is widest**, because the CLI wraps all eight formats and its - pages reach for every one of their vocabularies at once. `CLI-6` is ours; `HL7-V2`, `FHIR-R4`, - `DICOM-SR`, `NCPDP-SCRIPT`, `X12-837P`, `CCDA-R2.1`, `MSH-2`, `NM1-03`, `ST-01`, `439-E4` and - `ICD-10-CM` are reference material a consumer came here for. Never re-key a rule on the `WORD-N` - shape; the negative self-tests exist to make that attempt red. - - **Two remediation rules that matter more here than anywhere else.** (1) **Repair the head**: a - sentence with an identifier stripped off the front reads worse than the text it replaced. (2) - **CUT, do not rewrite.** This package's whole posture is honesty about what it _cannot_ do: gated - stubs that exit `69` and never fake a scrub, value-free stderr, the per-(format, operation) - `OP_SUPPORT` matrix. Softening a stated limit into an implied capability while tidying a sentence - is a worse defect than the bookkeeping being removed. Delete the claim rather than replace it, and - revert a rewrite verbatim rather than repair it. - - **What the gate cannot do:** it catches identifiers, not English sentences about our process, and - it reads `src/`, never `dist/` (untracked build output it cannot see without building). A new - programme prefix has to be added by hand. So the reviewer still owns half the rule. + changeset, + `CHANGELOG.md`, the commit, the PR and the roadmap. It is a **translation** at the boundary, not a + deletion. Gated by `pnpm check:no-internal-refs`. + + Why, in full: [agent-notes § No internal project bookkeeping on a public + surface](documentation/agent-notes.md#no-internal-project-bookkeeping-on-a-public-surface). + - **Doc comments and string literals ARE gated.** A `/** */` comment compiles into `dist/*.d.ts` + and renders on a consumer's hover; a string literal reaches their terminal (this package printed + a work item inside `CLI_NOT_IMPLEMENTED` and an ADR number inside `CLI_PARSER_UNAVAILABLE` + before that pass existed). **Line comments and plain block comments are NOT gated and + identifiers are welcome in them** (those two shapes are `//` and `/* */`), because the + convention says source comments are a place identifiers belong. + - **Do not justify that boundary from what reaches `dist/`.** Two drafts of a sibling's copy tried + and a refuter proved both false; two drafts of ours made the same mistake again. **The boundary + rests on the convention.** The line is not what a consumer _receives_; it is what a consumer is + **shown**. Two sentences that read well and are **false**: "everything in `src/` ships", and + "the bundles carry `//` comments verbatim". + - **Never re-key the gate on the `WORD-N` shape.** This repo is where that trap is widest, because + the CLI reaches for all eight formats' vocabularies at once: `HL7-V2`, `FHIR-R4`, `DICOM-SR`, + `NCPDP-SCRIPT`, `X12-837P`, `CCDA-R2.1`, `MSH-2`, `NM1-03`, `ST-01`, `439-E4`, `ICD-10-CM` are + reference material a consumer came here for. The negative self-tests exist to make that attempt + red. + - **Repair the head**: a sentence with an identifier stripped off the front reads worse than the + text it replaced. + - **CUT, do not rewrite.** This package's whole posture is honesty about what it _cannot_ do: + gated stubs that exit `69`, value-free stderr, the `OP_SUPPORT` matrix. Softening a stated limit + into an implied capability while tidying a sentence is a worse defect than the bookkeeping being + removed. Delete the claim rather than replace it, and revert a rewrite verbatim. + - **The gate catches identifiers, not English sentences about our process**, and it reads `src/`, + never `dist/`. A new programme prefix has to be added by hand. **The reviewer still owns half the + rule.** diff --git a/documentation/agent-notes.md b/documentation/agent-notes.md new file mode 100644 index 0000000..71c7dfc --- /dev/null +++ b/documentation/agent-notes.md @@ -0,0 +1,621 @@ +# @cosyte/cli: agent notes + +**This is the narrative half of `CLAUDE.md`.** On 2026-08-04 that file was 51,276 bytes and it is +always-read by every worker that `cd`s into this repo, so the per-incident write-ups, the +shipped-phase histories and the long rationales were relocated here **verbatim**, under headings +that match where they lived. Nothing was deleted, softened, or summarised on the way over. + +`CLAUDE.md` keeps the cursor, the rules, and **every** trap, each compressed to a one-line +imperative that links back to its section here. That is the contract between the two files: if a +line there tells you not to do something and you do not understand why, the reason is here, and +**"I did not read the reason" is not a licence to discount the rule.** + +Governed by the meta-repo's `documentation/decisions/0023-doc-budgets.md`, amendment of 2026-08-04, +which bounds `/CLAUDE.md` and names `/documentation/agent-notes.md` as the +relocation target. **Read the bound off `REPO_CLAUDE` in `.claude/hooks/doc-budget.mjs`, never off a +number written down here.** The mechanism is a per-repo ratchet that is LOWERED as relocations land, +and the first number quoted for it went stale within a day, which is the same defect this whole +audit exists to fix. That amendment's own warning is the one that binds here: +*"These files are where the traps that cost a defect to learn are written down. Relocate the +narrative; keep the cursor, the rule, and every trap."* + +--- + +## Status + +The per-incident sections below were the bulk of `CLAUDE.md`'s `## Status` list. Each is reproduced +exactly as it stood, including its leading `- ` bullet. + +### The docs sidebar and the IA spine + +- **▶ `docs-content/sidebars.json` IS BOUND BY AN IA SPINE THAT NOTHING IN THIS REPO CHECKS, AND + BREAKING IT STOPS THE WHOLE DOCS SITE DEPLOYING.** This package shipped two off-spine top-level + categories, **"MCP server"** and **"Reference"**. The docs site lints the sidebar out of each + package's released `docs-content.tar.gz`, and in strict mode a non-canonical top-level label is an + **error**, so `cli` was one of two packages holding a site that had not deployed for four days. + It was pre-existing and masked: every build died earlier, on memory, before reaching the gate. + **The canonical top-level order is `Overview` (the `intro` DOC REFERENCE, not a category), + `Installation`, `Quickstart`, `Core Concepts`, `Guides`, `API Reference`, `Troubleshooting`.** + Categories are OPTIONAL; the rule is that whatever you have is labelled and ordered canonically, so + `{"docs":["intro"]}` is fully compliant. `mcp` and `reference-commands` now sit under **Guides**, + `limitations` under **Troubleshooting** (the spine's item 7 explicitly houses "Known Limitations", + and `mllp`/`astm`/`deid` all keep it there; `synth` is the lone counter-example, so do not copy + `synth` here); nothing was orphaned. + **🔴 NEVER AUTHOR AN `API Reference` CATEGORY.** It is injected by the docs site when the package + ships API sources, and a hand-authored one is a distinct, harder **error** than the off-spine + label it would be replacing. Renaming "Reference" to "API Reference" trades one failing check for + another. + **▶ AND DO NOT REPEAT THE CLAIM THAT IT LANDS "JUST BEFORE Troubleshooting". A refuter falsified + that here.** Two different code paths inject it and they DISAGREE: + `docs/scripts/sidebar-resolver.ts` inserts at the canonical position, but it governs the + UNVERSIONED `content//` instance, which is not served once a slug is versioned + (`includeCurrentVersion: false`). The served path for a released package is + `docs/scripts/versioning/sidebar-augment.ts`, which **appends** (`[...value, apiCategoryEntry()]`), + so the rendered nav ends `..., Guides, Troubleshooting, API Reference`. The augmenter's own header + claims it mirrors the resolver; it does not. **This is a `docs` defect, not a `cli` one**, it is + cosmetic ordering, and it affects every released package. **The IA linter cannot see it** (it + refuses `versioned_sidebars/`), so "verified against the linter" is NOT evidence about placement. + State only what this package controls: it does not author or position that category. + **Nothing in `verify.sh` or this repo's CI can catch this** (`pack:docs` checks only that + `intro.md` and `sidebars.json` EXIST), so verify a sidebar edit against the site's own linter, + `docs/scripts/check-ia-conformance.ts`. It self-executes on import, so `NODE_ENV=test` is required + to call `lintSidebar` directly. **Use the previously shipped sidebar as a negative control**: if it + does not report errors, the probe is wrong, not the sidebar. + **Only a NEW RELEASE clears the gate**, because it reads the shipped artifact and releases are + immutable, so every already-published version keeps its sidebar forever. Archived versions are + reported at `info` and never gate. Do not try to fix history. + +### The pre-commit PHI gate and git mv + +- **▶ THE PRE-COMMIT PHI GATE WAS BLIND TO `git mv`, AND THE HOLE WAS NOT LIMITED TO LINKS.** + `scripts/phi-scan.ts --staged` is the `pre-commit` hook (`simple-git-hooks`). It listed the index + with `git diff --cached --name-only --diff-filter=AM`. Rename detection is **on by default**, so + `git mv test/__fixtures__/` stages as a **two-path `R100`** record, and `R`/`C` are in + neither `AM` nor `AMT`, so the status filter **deleted the record** and the destination was never + enumerated. Measured here, both at **exit 0** through the hook: an ordinary regular file carrying a + value the scanner's own floor catches (`:100644 100644 R100`) and a link + (`:120000 120000 R100`, index mode `120000` under the scan root). + **`--no-renames` is the whole remedy and it is a strict SUPERSET, not a narrowing**: the + destination arrives as a single-path `A`, the source as a `D` the filter drops. Verified under + `diff.renames` = `true` / `copies` / `false` / `1` and `diff.renameLimit=1`: **no `R` or `C` + survives any of them**, which is what makes the two-field `--raw -z` stride **structural rather + than conditional**. `copies` is not hypothetical here, it emits a live `C100`. + **Do NOT re-derive this as "needs the two-path record shape, a scope decision".** That framing was + ported from a sibling and is false; `dicom` measured it and `cli` re-measured it. + Three more shapes in the same route, each measured at exit 0 on `a7a92f8` and each closed: the + **destination mode was never read** (`--name-only` gives none, and `git show :` answers a + link with its **target path as though it were content**, so the scan read path text and never the + target's bytes, and this is why the route now lists `--raw -z` and refuses a non-blob mode with + **exit 2**); **`T` was missing from the filter**, so replacing a _tracked_ fixture with a link was + neither `A` nor `M` and the record was gone before a mode could be read; and **each scan root's own + path** is in scope now, because git records no index entry for a directory, so an entry at exactly + `test/__fixtures__` or `src` is that root replaced by a blob or a link. + **A refusal NEVER prints the link target.** It is working-tree text and a target path of the shape + `../patients/--.txt` is the whole reason: a diagnostic about a PHI leak is + itself a PHI surface. The entry's **own** path is printed deliberately, and every offender is + named, not just the first. **That guarantee is about a REFUSAL and does not extend to a hit** (see + the linked-scan-root residual below, where the values printed come from the far side of a link). + **The all-mode walk got the same refusal**, because a scanner whose pre-commit half refuses a link + while its CI half silently drops one cannot be reasoned about. The `.md` exemption deliberately + does not reach a link. + **Two exit-code defects fixed with it, and the reason matters more than the codes:** a missing or + unreadable allow-list, and an unreadable scan root, both threw past every handler and exited **1** + with a stack trace. **`1` is this contract's code for "hits found"**, so a broken invocation read + as a PHI finding. Both are exit **2** now. + **▶ THE REFUSAL RULE IS SCOPED TO AN _ENUMERATED_ ENTRY, AND THE UNQUALIFIED VERSION IS FALSE.** + A refuter caught the first draft of this note asserting "neither route follows such an entry" while + the same file falsifies it. The rule covers an entry the walk reached **beneath a root it had + already opened**, and a staged record **in scope per the boundary rule** (NOT "at or under a scan + root": measured, a staged link at `src/notes.json` is under a scan root and `--staged` exits 0 over + it, because the `src/` half of that scope is `.ts` only. The all-mode sweep refuses it, so nothing + escapes the gate as a whole). Three shapes escape it, all + **PRE-EXISTING** (identical on `a7a92f8` and in `dicom`), all measured, **none closed here**: + (1) **a scan root that is itself a LIVE link is followed** by the all-mode walk, because + `existsSync`/`readdirSync` both resolve, so the walk reads files no commit contains and prints their + values under a **fabricated in-repo path** that holds no such file, which is a confident wrong + provenance on the channel this repo calls a PHI surface. The **dangling** direction is the mirror + image: it prints OK over a corpus it never opened; (2) **an ancestor** of a scan root is in neither + route's scope, so staging `test` as a link is exit 0 on `--staged` and the walk then follows it; + (3) **paths mode follows an explicitly named link** (`statSync` resolves). The `--staged` half of + shape (1) **is** closed here. **Do not "fix" this by growing the guard inside a rename slice**: the + remedy taken was to correct the claim, and closing it needs a refuse-a-scan-that-observed-nothing + rule plus a decision about how far above a root to look. + **Other residuals, all PRE-EXISTING and none closed:** `D` and `U` are still not enumerated (`U` + costs nothing that can reach a commit, **measured**: `git commit` refuses an unmerged index + outright, exit 128); under `src/` the staged route covers only `.ts` while the all-mode walk covers + every non-`.md` file, so **the two routes disagree there** and the CI sweep is the cover. + **16 of `test/scripts/phi-scan.test.ts`'s 34 tests run red on `a7a92f8`.** The 18 that stay green + are the floor tests, the deliberate controls (one of which asserts the payload under test is + something this scanner would otherwise catch), and the four that **pin the residuals above** and + are green on both trees by design. **Give these tests explicit timeouts**: each spawns `tsx` cold, + measured at 0.5s idle and **3.7s under contention** against a shared 10s default. + **Two vacuity traps this suite has already sprung, both worth knowing before you add to it:** a + fixture built its merge conflict with a bare `git merge` and DISCARDED the result, which on a + runner with no git identity refuses before touching the index, so every later assertion held over + an empty one (real CI caught it; local runs passed against an ambient global identity). And the + `diff.renames` loop asserted only the detection-OFF side, so it would have passed just as happily + if git had stopped emitting the record shape the whole change is about. **Assert the premise, not + only the remedy.** + +### The em-dash brand gate + +- **Em-dash brand gate armed, and unlike most siblings this repo was NOT clean when it landed.** + `scripts/check-no-emdash.sh` (`pnpm check:no-emdash`) plus `.github/workflows/no-emdash.yml` enforce + the founder directive banning `U+2014` outright (`knowledgebase/06-brand/voice-and-tone.md`, "No em + dashes. Ever."). It scans **both** halves the rule covers: every tracked file, **and** the PR title, + body, and commit messages, on the non-default `edited` trigger so retitling a PR re-checks it. + **Measured byte-level over all 124 tracked files, not over markdown alone** (a markdown-only count is + what wrongly cleared `dicom`): **659 occurrences across 87 files**, all literal, none encoded. **61 of + the 87 are not markdown** (26 are), and one is `package.json`, whose `description` publishes to npm. All 659 + were rewritten in the same commit, so the gate arrives green over a tree that was cleared rather than + green over a tree nobody measured. + **What lands on `main` here is a repo setting, read rather than assumed, and it differs from `mllp`: + all THREE merge methods are enabled** (`allow_squash_merge`, `allow_merge_commit`, + `allow_rebase_merge`). Squash uses `squash_merge_commit_title: COMMIT_OR_PR_TITLE` and + `squash_merge_commit_message: COMMIT_MESSAGES`; merge and rebase land the branch commit messages + verbatim. So **the branch commit messages are the one text that lands under every enabled method**, + the PR title lands under two of three, and **the PR body lands under none**. It is scanned anyway as + deliberate over-strictness. Do not repeat `ncpdp`'s copy, which says "the PR title and body are what + lands": the body never lands anywhere. + **The script is composed from three copies, and the composition is the thing to understand before + editing it.** Base: `website`'s **NUL-exclusion** shape. Plus `ncpdp`'s two route fixes (a tracked + file named exactly `-` was read as **standard input** and never opened, so the gate printed OK over a + live em dash; `-d skip` silently passed a tracked **symlink to a directory**). Plus `dicom`'s + **binary-match diagnostic branch**. The `./` prefix is applied in the list-building **loop**, not via + `sed -z`, so the scan is a single command with the stderr capture bound to all of it, and there is no + GNU-only `sed` dependency that has no self-test. + **Why the NUL-exclusion shape here, and unlike `mllp` the reason is not a hypothetical.** This repo + tracks **eleven** binaries: the ten `vendor/cosyte-*.tgz` packed siblings (an `npx` bin cannot + peer-depend, so the parsers are hard vendored deps) and `test/__fixtures__/sample.dcm`. Measured on + this tree: **`vendor/cosyte-hl7-0.0.0.tgz` already contains the byte sequence `E2 80 94`** (one + occurrence, offset 50217 of 665534 bytes, in a DEFLATE stream). A text-only port therefore reds on + this repo **today**, naming a compressed byte stream nobody wrote, and **that red has no + remediation**: you cannot rewrite compressed bytes with a period. A gate whose red state has no fix + is a gate someone disables. **Do not add `grep -I` instead**: measured on GNU grep 3.8, a text file + whose bad byte sits on the same line as the em dash is skipped by `-I` in total silence (no stdout, + no stderr, exit 0) and the gate prints OK. + **The disclosed cost, said plainly: a tracked TEXT file holding a NUL byte is silently exempt, and + seeding a vendored tarball with a live em dash leaves this gate green. That is a miss, not a pass.** + Verified, along with the control that proves the NUL rule causes it (the same tarball bytes with + every NUL stripped, carrying the same em dash, goes RED). `cli` has **no** NUL-bearing text file + today, checked over all 124 tracked files rather than assumed, so the exclusion currently exempts + exactly eleven files and all eleven are genuine binaries. **Do not round that off to "hypothetical + here."** `git ls-files --eol` calls **thirteen** files binary, not eleven: the other two are + `test/__fixtures__/adt-a01.hl7` and `test/__fixtures__/minimal.astm`, which git classifies on its + lone-CR branch because an HL7 v2 and an ASTM segment terminator is `CR` with no `LF`. Those two hold + **zero** NUL bytes, so they stay in scope, and that was **proved, not assumed**: each was seeded with + a live em dash in turn and the gate went red naming the file. `cli` wraps eight parsers and its + `test/__fixtures__/` corpus is the obvious place a NUL-bearing text fixture would arrive. **The tell + is the excluded count on the OK line: it reads 11 today.** If one lands, revisit the partition (the + `.gitattributes` declaration `pathways` prefers, which this repo cannot use because it declares no + attributes at all), never the ban. + One disclosed property of a red on those two fixtures: the hit echoes the matching _line_, and a + CR-delimited frame is one line, so a whole message lands in a public CI log. Acceptable and + deliberately un-truncated, because those fixtures are synthetic by policy and `pnpm phi-scan` gates + that policy over the same files. Remaining known limits (encoded-form matching is literal, so + `%e2%80%94`, `—` and `—` pass; the scan reads contents and never file names) are in the + script header and are **shared across every copy, so fix them there, not here**: a divergent copy is + worse than a shared known limit. + **▶ READ THIS BEFORE PORTING THIS GATE ANYWHERE ELSE. AN EM DASH IS SOMETIMES A VALUE, NOT + PUNCTUATION, AND A BULK SWEEP WILL SILENTLY EDIT A CAPABILITY CLAIM.** The refuter caught it here and + it is the single most valuable thing this port produced. In `docs-content/limitations.md` the + per-(format, operation) support matrix used a bare em dash (`U+2014`) **as a cell value meaning + "not supported"**. + The sweep rewrote it as punctuation, so `dicom`.`fmt`, `mllp`.`fmt` and `mllp`.`validate` rendered as + a stray colon. **That converts "support absent" into "support unstated", on the one page whose entire + job is honest capability disclosure**, and it reads as a rendering artifact rather than as a claim, so + a reader does not even know something is missing. It is the same family as this repo's own rule for + the public-surface gate: **CUT, do not rewrite; softening a stated limit into an implied capability + while tidying a sentence is a worse defect than the thing being removed.** + **Nothing in this repo's CI could have caught it**, and that is the part to carry: `test/docs-content.test.ts` + only executes ` ```ts runnable ` blocks, and `prettier`'s glob covers `src/**`, `test/**`, `scripts/**` + and root `*.{json,md,yml}` but **NOT `docs-content/`**. Fixed by replacing the marker with the words + `not supported`. **Before sweeping any repo, grep for an em dash used as a table cell or a list marker + first** (`\|\s*\x{2014}\s*[\|\(]` found exactly the three lines here) and convert those to a WORD, + by hand, before running any bulk transform. A marker that means "no" must survive as a word, never as + punctuation. + **A second measurement lesson from the same review, because this item's history is wrong counts + propagating through briefs:** the first draft of these notes said "37 of the 87 are not markdown", + asserted as measured fact inside the sentence arguing for measurement rigor. It was wrong. **Correct + and final: 659 occurrences across 87 of 124 tracked files; 26 of those files are markdown and 61 are + not; 37 is the non-markdown AND non-`test/` subset.** Re-derive numbers before writing them down, and + never quote them from a sibling repo's copy of this note. + **Pre-existing and correctly not retroactive:** commit subjects already on `main` may carry `U+2014`. + The message half only runs on `pull_request`. History is not rewritten. + +### The vendor to npm dependency swap + +- **The published manifest is installable again, and `VERSION` no longer lies.** `0.0.1` and `0.0.2` + are permanently broken on npm (`file:vendor/*.tgz` specifiers in the published manifest, `ENOENT` on + every install route) and both shipped `VERSION = "0.0.0"` against a `0.0.2` manifest, which + `cosyte --version` and the MCP `serverInfo.version` both printed. Fixed together: + **`scripts/sync-version.mjs`** now runs inside the `version` script and rewrites **two** targets, + `src/core/version.ts` and the asserted literal in `docs-content/installation.md`; `test/sanity.test.ts` + compares the export against `package.json` **and** pins the declaration's `: string` shape, which the + script's pattern keys on. **Fix the ASSERTION, not just the value:** the docs block asserted + `typeof VERSION`, which is true of every wrong value and green-lit both bad releases. + **The dep swap is done except for one dependency, and the exception is the interesting part.** + `@cosyte/hl7` (`^0.0.7`) and `@cosyte/terminology` (`^0.0.9`) are hard deps at real ranges; the six + breadth parsers and `@cosyte/transform` (`^0.0.4`) are `optionalDependencies` at real ranges. + **`@cosyte/fhir` is not declared at all**, because it is not on the registry and, measured here, + declaring it in ANY form alongside `@cosyte/transform` (whose `@cosyte/fhir` peer is mandatory) + fails the whole install with `ERESOLVE`: optional dep and optional _peer_ both. Either one alone + installs clean; the pair does not. **Do not explain this with a missing + `peerDependenciesMeta.optional` flag** - measured across the suite, that flag does not decide the + outcome, and the mechanism is unexplained. `@cosyte/fhir` is kept as a **`devDependency`** on the + vendored tarball so this repo's own FHIR/`convert` tests run and so `transform`'s peer resolves in + the dev tree. Consequence, stated on every consumer surface rather than discovered: **an installed + copy has no FHIR support**, and FHIR `parse`/`inspect`/`fmt`/`validate` plus `convert` degrade to a + value-free `CLI_PARSER_UNAVAILABLE` (69). That required `loadOptionalPackage(detail, load)` beneath + `loadOptional` (which takes a `CosyteFormat` and hardcodes the word "parser", wrong for both cases) + plus `loadFhir()`; `test/absent-sibling.test.ts` includes a **static guard** that reds on a new + **single-line, unwrapped** `import("@cosyte/fhir")` / `import("@cosyte/transform")` in `src/`, which + is the shape the defect took. **Do not write "any new call site": a refuter falsified that wording + by adding a thunk assigned to a variable, and the suite stayed 10/10 green.** It also misses a + multi-line import and a **static** `import … from "@cosyte/fhir"` - and this repo now HAS the first + static reference to that package (`src/core/parsers.ts`, `import type`, erased at build, verified + absent from `dist/`). Dropping the word `type` loads it eagerly and breaks every command in an + installed copy, unseen by the guard. Also note the two diagnostics deliberately do NOT say + "install it": `npm install @cosyte/transform` fails `E404` on its own `fhir` peer, so that advice + would send a user at a command that cannot succeed. `loadOptional()`'s stock wording says exactly + that, which is why neither goes through it. + **Verified by installing, which a `--dry-run` cannot do**: pack, `npm install` the tarball in a clean + directory outside the repo (exit 0), run both bins, import `.` under ESM and CJS. Negative control: + the published `0.0.2` still `ENOENT`s. Keep that step; it is checklist step 6 in `RELEASING.md`. + **A THIRD fault is real, pre-existing, and NOT fixed by any of this: `npx @cosyte/cli …` fails with + `could not determine executable to run`.** `npx` runs the bin matching the package name's last + segment (`cli`); this package ships `cosyte` and `cosyte-mcp`. The `bin` block is byte-identical to + the published `0.0.2`, so the swap cannot have changed it. Docs now say + `npx --package @cosyte/cli cosyte …` (measured working). **A `cli` bin alias would fix it and is + deliberately not added** - `npm install -g` would then claim the name `cli` on the user's `PATH`. + That trade is a founder call. + +### Shipped phases + +The roadmap's build phases, newest first, exactly as each was recorded on landing. The CLI is +feature-complete: Phase 7 was the final phase. + +- **Phase 7 shipped (CLI-7): release hardening: the final roadmap phase. The CLI is feature-complete.** + No new runtime command surface; this phase is publish-readiness. **Fuzz** over the CLI's two input + boundaries: the terminal (`run`, over argv plus stdin bytes) and the agent surface (`dispatchTool`, + over a tool name plus arguments): proving neither ever throws or leaks a stack frame + (`test/fuzz.property.test.ts`, scaled by `CLI_FUZZ_RUNS`, run nightly by a scheduled **Fuzz** workflow + and on demand via `pnpm test:fuzz`). The **exit-code golden matrix** (`test/exit-code-matrix.test.ts`) + locks one representative invocation for every code in the `0/1/2/65/66/69/70` contract as a stability + surface. **Publish dry-run proven:** `attw` green, a new `smoke` gate (`scripts/smoke.mjs`, in + `verify.sh`) exercising the built dual ESM/CJS `.` and `./mcp` subpaths **and both bins** under `node`, + and a clean `npm publish --dry-run` tarball. **Honesty docs:** `docs-content/limitations.md` + (wraps-not-implements, the non-goals, the per-(format, op) matrix, the PHI posture) and a + man-page-style `docs-content/reference-commands.md`; plus `RELEASING.md` (the two-bin publish, + provenance/OIDC, the vendor→npm swap, the two founder stops). **Founder-gated tail (NOT crossed):** the + real `npm publish` and the repo public-flip remain the two standing human stops, and, unique to this + `bin`, the vendored `file:` sibling deps must become real `@cosyte/*` npm ranges at `PUB-FLIP` (a + published package cannot ship a `file:` dep). Everything up to those is done. +- **Phase 6 shipped** (`operations/roadmaps/cli.md` §Phase 6). **Six more formats + streaming + shell + completion** (ADR 0025). The CLI now wraps **all eight** cosyte formats through a single lazy + **per-format adapter registry** (`src/core/parsers.ts`) that replaced the per-command `hl7 ? : fhir` + branches and makes support **per (format, operation)** via `OP_SUPPORT`: an unsupported (format, op) + is a value-free `CLI_FORMAT_UNSUPPORTED`, never a fake. Capabilities: `x12`/`astm`/`ncpdp` → + parse+inspect+fmt+validate; `ccda` → inspect+fmt(XML)+validate (parse deferred, no library JSON + model); `dicom` → inspect+validate (parse/fmt deferred: binary model); `mllp` → parse+inspect (a + transport container de-framed to its enclosed HL7). Autodetection covers all eight (conservative + + fail-safe: a co-match is a _detected_ ambiguity, never a mis-route). **Streaming:** `parse` emits + **NDJSON** with per-record isolation for MLLP frames and the + new **`--ndjson`** input mode (a failed record is a value-free `{record,error}` line; any failure → + exit `65`). **`cosyte completion `** prints a static completion script. The six breadth + parsers are **`optionalDependencies`** (vendored, lazy per format, outside the hard-dep closure: ADR + 0025), so the umbrella `verify-policy` `cli` cap **stays 4**; an absent optional parser degrades to a + value-free **`CLI_PARSER_UNAVAILABLE`** (exit `69`). New diagnostic `CLI_PARSER_UNAVAILABLE`; the public + `WIRED_FORMATS` set is replaced by the per-op `OP_SUPPORT` matrix. +- **Phase 5 shipped** (`operations/roadmaps/cli.md` §Phase 5). Adds the **`cosyte-mcp` MCP server**: the + **agent front door** and the _second adapter_ over the one shared `core` (ADR 0022). A **stdio** Model + Context Protocol server on `@modelcontextprotocol/sdk`, reachable three ways: the new **`cosyte-mcp`** + bin, the **`cosyte mcp`** subcommand, and the **`@cosyte/cli/mcp`** subpath export. It exposes four + tools (**`parse`/`validate`/`inspect`/`convert`**) each a thin wrapper that calls the same command + handler the terminal uses (with `--json`), so `cosyte parse` and the MCP `parse` tool agree by + construction; the CLI re-implements nothing. Every tool runs **value-free** (no `--unsafe-show-values` + door on the agent surface): a tool _result_ carries the requested data, a tool _error_ carries only the + value-free diagnostic, and a parsed-but-invalid `validate` verdict is a **successful** call reporting + the verdict: only a hard failure sets `isError`. The **SDK is isolated and runtime-optional** (ADR + 0024): it is the CLI's first and only third-party runtime dep, declared in **`optionalDependencies`** + (pinned `1.29.0`), imported only in `src/mcp/server.ts`, and reachable solely via the `./mcp` + boundary: a `cosyte parse` invocation never loads it (proven by `test/mcp-isolation.test.ts`), and the core + works with the SDK absent. Because it is not in the hard runtime closure, the umbrella `verify-policy` + runtime-dep cap on `cli` stays **4**. `redact`/`deid` and `map-codes` are deliberately not yet exposed + as tools. New exports (on `./mcp`): `createMcpServer`, `startStdioServer`, `dispatchTool`, `TOOL_DEFS`. +- **Phase 4 shipped** (`operations/roadmaps/cli.md` §Phase 4). Adds the two **consumer-of-consumers** + commands, each a thin wrapper that re-implements no library logic: **`convert --to fhir`** + (HL7 v2 → FHIR R4 via **`@cosyte/transform`**: parse with `hl7`, `toFhir`, serialize with `fhir`; + the `Bundle` on stdout, value-free issues on stderr, an **error-severity issue drives exit `1`**, a + non-HL7 source is `CLI_FORMAT_UNSUPPORTED`/`65`) and **`map-codes --code … [--system +…]`** (ConceptMap `$translate` via **`@cosyte/terminology`**, BYO ConceptMap: a match → target + coding(s) + exit `0`; unmapped → `TERM_TRANSLATE_UNMAPPED` + exit `1`; an unloadable map → the new + **`CLI_MAP_INVALID`**/`65`). Both siblings are **hard, first-party, lazy-loaded** runtime deps + (vendored tarballs; the umbrella dep cap was raised **2 → 4**: ADR 0023). New exports: + `convertCommand`, `convertOutcome`, `mapCodesCommand`. +- **Phase 3 shipped** (`operations/roadmaps/cli.md` §Phase 3). Adds three commands over the two wired + parsers: **`validate`** (parse + the wrapped parser's own validation surface, **verdict in the exit + code**: `0` valid / `1` invalid / `65` unparseable; findings value-free; `--profile` gated to an + honest `CLI_NOT_IMPLEMENTED`/`69`; verdict never invented: FHIR = `validateResource().valid`, HL7 = + parseable), **`inspect`** (a value-free structural summary: message/resource type, segment/entry + counts, warning/issue count), and **`fmt`** (canonical re-serialization via the library's serializer; + stdout is the data channel; no partial emit on unparseable input). Adds **`EXIT.INVALID` (`1`)**: the + exit-code contract is now `0/1/2/65/66/69/70`. All four commands share one input+format front door + (`core/input.ts` `resolveInput`) and one value-free parser-failure boundary (`core/wrap.ts`), so the + value-free posture + `--unsafe-show-values` chokepoint stay uniform (`parse` refactored onto them, + behavior-preserving). +- **Phase 2 shipped** (`operations/roadmaps/cli.md` §Phase 2). Pre-alpha `0.0.x`, unpublished. On top + of Phase 1's `cosyte parse` (HL7 v2 + FHIR R4, content autodetection, exit-code contract, value-free + `CLI_*` diagnostics), Phase 2 hardens the PHI posture: the global opt-in **`--unsafe-show-values`** + (the single door to a value on a secondary surface, funnelled through one chokepoint in + `core/phi.ts`), a proven **never-a-PHI-temp-file** guarantee, and the **`redact`/`deid`** command as + an honest, `@cosyte/deid`-gated `CLI_NOT_IMPLEMENTED` (exit `69`), never a built-in partial scrub + that would risk a false-safety impression. Exit-code contract is now `0/2/65/66/69/70`. +- **Phase 1 shipped** (§Phase 1). `cosyte parse ` for **HL7 v2** + **FHIR R4**, **content + format autodetection** (conservative, fail-safe, never a guessed parser), the documented + **exit-code contract**, and the **value-free diagnostic** channel with stable `CLI_*` codes. + +### Hard runtime deps + +- **Hard runtime deps (ADR 0021 + 0023), as they stand AFTER the vendor → npm swap:** only + **`@cosyte/hl7` (`^0.0.7`) + `@cosyte/terminology` (`^0.0.9`)** are hard `dependencies` now, both + real registry ranges (an `npx` bin can't peer-depend). `@cosyte/transform` moved to + `optionalDependencies` and **`@cosyte/fhir` is undeclared** - see the swap note above for why, and + do not "restore" either without reading it. That is **2** hard runtime deps against an umbrella + `verify-policy.json` cap of **4**, so it is under the cap, not at it. **Lazy-loaded per command.** + On the `0.0.x` ladder `^0.0.7` permits no other version, so these are effectively exact pins, and + Dependabot now sees them (it never could while they were `file:` specs). `vendor/` survives only to + supply `@cosyte/fhir` as a **`devDependency`**; the other nine tarballs are refreshed by + `pnpm vendor:refresh` but wired to nothing, and removing them is a deliberate separate cleanup. + Third-party CLI-core runtime deps: **zero**. The MCP server's **`@modelcontextprotocol/sdk`** is the + CLI's only third-party runtime dep: declared in **`optionalDependencies`** (not `dependencies`), + isolated behind `./mcp`, so it is outside the hard-closure cap (ADR 0024). + +### Deferred + +- **Deferred:** the roadmap's build phases are complete (P7 release hardening shipped). Per-(format, op) + cells remain deferred honestly (never faked): `dicom` + `parse`/`fmt` (binary model), `ccda` `parse` (XML is the canonical `fmt` surface), `mllp` `fmt`/`validate`. + The MCP tool set covers `parse`/`validate`/`inspect`/`convert`; `redact`/`map-codes` tools and + remote/HTTP MCP are later. `redact`'s real de-identification is deferred to when `@cosyte/deid` ships + (P2 landed the gated stub + seam). `validate --profile` is reserved but gated (`CLI_NOT_IMPLEMENTED`/`69`) + until the CLI can load a profile, no profiles are bundled. + +### ADRs + +- **ADRs:** `documentation/decisions/0021` (dependency-tier: a `bin` hard-deps first-party siblings), + `0022` (one-repo-two-bins: CLI + MCP over one core; web playground out of scope), `0023` (wire + `transform` + `terminology` for `convert`/`map-codes`; the deliberate 2 → 4 dep-cap raise), `0024` + (the Phase-5 MCP server; the SDK as an isolated, runtime-optional dependency: hard-dep cap stays 4), + and `0025` (the Phase-6 breadth parsers as runtime-optional lazy deps outside the cap; the cap stays 4). + +--- + +## Branch protection (and the limits of this claim) + +`main` is protected by the repository ruleset **`ci-required-checks`** (id `19907924`, +`source_type: Repository`, `enforcement: active`, conditions `~DEFAULT_BRANCH`). Rules: `deletion`, +`non_fast_forward`, `required_status_checks`. Before it existed, every check this repo ran was +advisory: `ci`, `codeql`, `scorecard` and `fuzz` could all be red and the merge still landed on +`main`, and `main` is the branch that publishes. + +Required contexts, each pinned to **`integration_id: 15368`** (the `github-actions` app) so that a +commit status of the same name posted by any other actor with write access cannot satisfy it: + +| context | +| ------------------------------------------ | +| `ci / verify (22, ubuntu-latest)` | +| `ci / verify (24, ubuntu-latest)` | +| `ci / actionlint` | +| `codeql / analyze (javascript-typescript)` | +| `no-internal-refs` | +| `no-emdash` | + +These are the names GitHub actually reports, read off real check runs, **not** off a workflow's +`name:` field. Requiring a context nothing emits does not fail a PR; it leaves it pending and +unmergeable forever. None of `ci.yml`, `codeql.yml`, `no-internal-refs.yml` or `no-emdash.yml` +carries a `paths:` filter, so no PR can skip one. + +**`no-internal-refs` is the one that is NOT ` / `, and the shape is worth knowing.** +`ci / verify (22, ubuntu-latest)` is prefixed because `verify` runs inside a _called_ reusable +workflow, so the context is ` / (matrix)`. `no-internal-refs` is an +ordinary job in this repo's own workflow, so its check-run name is just the **job id**. That means +**renaming the job silently detaches the required check**: the ruleset keeps naming a context nothing +emits, every PR goes pending forever, and nothing errors or warns. Rename the job and the ruleset +together, or neither. It was added to the ruleset on 2026-07-28, after the first real check run +existed and its name was read back off that run. + +**`no-emdash` is the second of that shape, added 2026-07-28 the same way.** Its workflow is titled +`Em-dash gate` and a PR's checks list renders it as `Em-dash gate / no-emdash`, but **the context +GitHub reports is the bare job id `no-emdash`**, which is what the ruleset names. Requiring either of +the other two strings would leave every PR pending forever while the ruleset looked configured. The +name was read off a real `pull_request` check run on `#20` before the ruleset was written, never off +the workflow file, and writability was confirmed with the `PUT` itself rather than a `GET` (an +Organization-sourced ruleset returns `200` to a `GET` and `404` to an identical-payload `PUT`; +`19907924` is `source_type: Repository`, and it is the only ruleset this repo has). + +**What is deliberately NOT required, and why each would be a defect:** + +- **`scorecard / analysis`** runs on `push` to `main` and on a schedule, never on `pull_request`. + Requiring it would strand every PR pending forever. +- **`fuzz`** is `schedule` + `workflow_dispatch` only, for the same reason. The same property suite + runs inside `ci / verify` at a lower case count, so the PR path is covered by a context that + does arrive. +- **`release / release`** runs on `push` to `main`. It is not a PR gate. +- The **`CodeQL`** check posted by the Advanced Security app (id `57789`) reports **alert state**, + not whether the analysis ran. `codeql / analyze` already gates that. + +**A required job gates all of its steps.** Splitting a step out of `ci / verify` into its own job +silently un-requires it, with no error and no warning. There is a banner on `ci.yml` where someone +would trip it. + +**▶ Scope of the claim, stated plainly: a ruleset makes a red check BLOCK a merge. It does not make +the check correct, and nothing inside this repository can observe its own ruleset.** Delete the +ruleset and this test suite stays green while this section keeps asserting protection. It is not +verifiable from inside the repo, by `verify.sh`, or by any gate here. Verify it the only way that +works: + +```bash +gh api repos/cosyte/cli/rulesets +``` + +Two things recorded as **unproven** rather than fine: no fork PR has ever run here, so neither the +first-time-contributor approval gate nor whether `codeql / analyze` can report on a fork token +(which cannot hold `security-events: write`) has been observed. + +--- + +## Engineering Guardrails + +### The attw wrapper + +- **▶ `attw` SAYS "does not contain types" AND EXITS 0, SO THE `attw` SCRIPT IS A WRAPPER, NOT THE + BARE CLI.** `getExitCode.js` in `@arethetypeswrong/cli@0.18.4` opens with + `if (!analysis.types) return 0`, so the problem list is never consulted and no `--profile`, + `--ignore-rules` or config setting reaches that early return. For a package that ships types it + means the declarations were **not in the tarball**, which is a broken publish reported as a pass, + and `verify.sh` propagates the 0. **The race only supplies the condition.** Reproduced here with + **zero concurrency**, under the real `--pack . --profile node16`: `rm -rf dist` and, separately, + deleting all ten of a completed build's declaration files both print the sentence and exit 0. + `tsup` writes JS in one pass and declarations in a later one; instrumented on one build here at a + 10ms poll, all eight `.mjs`/`.cjs` files appeared on a single poll at 4.92s and all ten + declarations on a single later poll at 11.82s, a **6.90s window**. **Do not quote that interval as + a constant**: it moves with load (7.85s on a busier box). What does not move is the ordering, and + that the ten declarations land together. So the answer is **not** a lock, a lease or a build queue. + `scripts/attw.mjs` carries **two nets**: a preflight that every relative path `package.json` + promises exists and is non-empty (which names the missing file), and a post-check on the untyped + sentence (which catches declarations on disk but excluded from the tarball by `files`; no instance + of that is on record here). + **Three things specific to this repo, each measured, none inherited from the sibling this was + ported from:** + 1. **The preflight walks `bin` as well as `exports`.** `attw` never reads `bin`. With + `dist/bin/cosyte.mjs` deleted and everything else built, it printed every subpath green and + exited 0 over a tarball with no `cosyte` command in it. + 2. **`--profile node16` is load-bearing and is forwarded, never reinterpreted.** Without it, + `@cosyte/cli/mcp` fails `node10` resolution and `attw` exits 1. The wrapper passes every + argument straight through. + 3. **SIX PACKED-BUT-UNDECLARED DECLARATIONS DECIDE WHICH SILENCE YOU GET, AND THE OBVIOUS + TWO-LINE VERSION OF THIS IS FALSE.** `files: ["dist"]` packs all **ten** declarations `tsup` + emits; `package.json` names only **four** (`dist/index.d.*`, `dist/mcp.d.*`). The other six ride + along unnamed: `dist/io-.d.ts`/`.d.cts` **and the four `dist/bin/*.d.ts`/`.d.cts`**. + `analysis.types` is true if the tarball carries **any** declaration, so **any one of the six** is + enough. Measured, JS intact: the four declared gone with io-\* and bin/\* packed, **exit 1**; + also without io-\*, **exit 1**; also without bin/\* but with io-\* back, **exit 1**; all ten + gone, the untyped sentence and **exit 0**. So a **partial** loss is caught by `attw` itself and + only a **total** one is the false green, and the build window above is the total case. The + preflight therefore reports both outcomes and **must not assert the exit 0**. A first draft + named only the io chunk as the deciding file, measured it on a `bin`-less throwaway fixture, and + wrote the fixture's result down as this tree's; a refuter falsified it in one run. **Re-measure + before you shorten this.** + The post-check reads a string, so what would hide that string is **refused** rather than + tolerated: `--quiet`, `-q`, `--format`, `-f`, `--config-path`, and a `.attw.json` setting `quiet` + or `format` (`readConfig()` applies it after argv). Every one was measured here to remove the + sentence and still exit 0, `--config-path` included. **The refusal matches an EXACT ARGV TOKEN** + (or the part before an `=`), by option name and not by value. **Two disclosed holes, measured, + deliberately left open:** commander's attached and clustered short forms `-fjson` and `-Pf json` + get through and exit 0 over an untyped pack (`-qP` does not, the empty-transcript net catches + it), and a declared path not starting with `.` is skipped by the preflight. Neither is closed, + because the bare invocation this replaced exited 0 on that pack with **no** arguments at all, so + the gate is strictly better either way, and a short-option parser is a moving part the guard does + not need. **Of the ARGV refusal say "exact argv token", never "wholesale":** the stronger wording + was live in both the header and the printed message, and was refuted. The `.attw.json` refusal + **is** wholesale (key presence, any value) and that word is correct there; the two messages + differ on purpose. `test/scripts/attw-gate.test.ts` pins both nets, the upstream exit 0 itself, a + negative control on a well-formed package, and that a real `attw` failure still fails with attw's + own status. + **The vendored `file:` deps do not touch this.** `npm pack --dry-run` on a clean build lists + **30** entries and zero from `vendor/`, and no emitted declaration carries a `@cosyte/*` module + specifier. (A draft said 70. It was read off a stale `dist/`; a clean `pnpm clean && pnpm build` + gives 30, and 26 of those are `dist/`.) The published manifest being uninstallable is a real, + separate condition; this gate neither addresses it nor is shaped by it. + +--- + +## Standing disciplines (every change) + +### No internal project bookkeeping on a public surface + +The long-form half of standing discipline 4. The rule itself, and the founder directive it comes +from, stay in `CLAUDE.md`. + + **Four surfaces, three different answers.** `/** */` doc comments compile into `dist/*.d.ts` and + `dist/*.d.cts` and render on hover, so they are **gated**, and in this repo they were by far the + largest violating surface. String literals reach a consumer as terminal diagnostic text, so they + are **gated too**: this package printed an internal work item inside `CLI_NOT_IMPLEMENTED` and an + ADR number inside `CLI_PARSER_UNAVAILABLE` before that pass existed. `//` and plain `/* */` + comments are **not gated** and identifiers are **welcome** in them, because **the convention says + source comments are a place identifiers belong**. That is the whole reason. **Do not justify that + boundary from what reaches `dist/`**: two drafts of the `ncpdp` copy tried, a refuter proved both + false, and two drafts of this paragraph made the same mistake again. Measured on this tree, at + `06abc86`: `dist` is `files[0]`, there is no `.npmignore`, and **24 of the 27** tracked `src/` + files appear whole in a build map's `sourcesContent` (`src/index.ts`, `src/core/result.ts` and + `src/mcp/index.ts` contribute only re-exports and types, so the bundler erases them). Two + sentences that read well and are **false**, so do not write them: "everything in `src/` ships", + and "the bundles carry `//` comments verbatim" (measured: of the 43 whole-line `//` comments in + tracked `src/*.ts`, exactly **one** survives into any emitted `.mjs`/`.cjs`). **The boundary rests + on the convention, not on either fact.** The line is not what a consumer receives; it is what a + consumer is **shown**. + + **This repo is where the `WORD-N` trap is widest**, because the CLI wraps all eight formats and its + pages reach for every one of their vocabularies at once. `CLI-6` is ours; `HL7-V2`, `FHIR-R4`, + `DICOM-SR`, `NCPDP-SCRIPT`, `X12-837P`, `CCDA-R2.1`, `MSH-2`, `NM1-03`, `ST-01`, `439-E4` and + `ICD-10-CM` are reference material a consumer came here for. Never re-key a rule on the `WORD-N` + shape; the negative self-tests exist to make that attempt red. + + **Two remediation rules that matter more here than anywhere else.** (1) **Repair the head**: a + sentence with an identifier stripped off the front reads worse than the text it replaced. (2) + **CUT, do not rewrite.** This package's whole posture is honesty about what it _cannot_ do: gated + stubs that exit `69` and never fake a scrub, value-free stderr, the per-(format, operation) + `OP_SUPPORT` matrix. Softening a stated limit into an implied capability while tidying a sentence + is a worse defect than the bookkeeping being removed. Delete the claim rather than replace it, and + revert a rewrite verbatim rather than repair it. + + **What the gate cannot do:** it catches identifiers, not English sentences about our process, and + it reads `src/`, never `dist/` (untracked build output it cannot see without building). A new + programme prefix has to be added by hand. So the reviewer still owns half the rule. + +--- + +## Corrections made during the relocation (2026-08-04) + +Everything above was moved, not edited. The few claims that were **changed** rather than moved +are all listed here, with the superseded wording quoted, so nothing is lost. Do not treat this +list as closed: add to it rather than restating a count. + +**This file is deliberately NOT run through prettier.** It is outside the repo's `format:check` +glob (`"*.{json,md,yml}"`, top level only), and reformatting it would rewrap the relocated text +and destroy the verbatim guarantee this file exists to provide. The leftover 3-space paragraph +indents are below the 4-space code-block threshold, so they render normally and hide nothing. + +**The `Tech Stack` runtime-deps bullet was stale and contradicted `Status`.** It read: + +> - **Runtime deps:** `@cosyte/hl7` + `@cosyte/fhir` + `@cosyte/transform` + `@cosyte/terminology` +> (first-party, hard, vendored: ADR 0021 + 0023), capped at **4**. The six CLI-6 breadth parsers +> (`dicom`/`x12`/`ccda`/`ncpdp`/`astm`/`mllp`) are vendored **`optionalDependencies`**, lazy-loaded per +> format and **outside** that cap (ADR 0025); the MCP `@modelcontextprotocol/sdk` is likewise optional +> (ADR 0024). **Zero third-party** in the CLI core (`util.parseArgs`, no framework). + +That is the **pre-swap** shape. The `Hard runtime deps` section above records what actually shipped: +after the vendor to npm swap only `@cosyte/hl7` and `@cosyte/terminology` are hard `dependencies`, +`@cosyte/transform` moved to `optionalDependencies`, and `@cosyte/fhir` is **undeclared**. Verified +against `package.json` on `origin/main` on 2026-08-04. `CLAUDE.md`'s bullet now states the shipped +shape and points at the swap note; the stale wording is preserved above rather than deleted. + +**One further wording edit, recorded for completeness.** The `Tech Stack` testing bullet read +"CLI-7 adds an **argv+stdin+MCP fuzz** gate ..."; it now reads "Plus an **argv+stdin+MCP fuzz** +gate ...". The phase attribution survives verbatim in `### Shipped phases` above; only the two +words of attribution changed. + +**The Phase 7 entry's "Founder-gated tail (NOT crossed)" is superseded and is preserved as the +record of what was true on landing.** Measured 2026-08-04: `cosyte/cli` is **public** +(`gh repo view cosyte/cli --json visibility`) and the package has published. `CLAUDE.md` carries +the current state. Flipping visibility is never waived and is not an agent's to cross; the +`npm publish` half is covered by the standing founder directive. + +**One number was NOT carried across as written.** The umbrella described this package as "FIXED at +`0.0.3`". On 2026-08-04 `npm view @cosyte/cli version` read **`0.0.4`** and `package.json` on +`origin/main` read `0.0.4`. The trap itself is live and unchanged and is stated in `CLAUDE.md` +without a number: **`@cosyte/fhir` is still absent from the manifest because `fhir` cannot publish +(`FHIR-NPM-NAME`), and an installed copy therefore has no FHIR support.** Only the version literal +was dropped, for exactly the reason ADR 0023 deleted the umbrella's version list: a quoted version +here is a number that goes stale between the write and the read.