diff --git a/CLAUDE.md b/CLAUDE.md index 49225a2..0107aaf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -128,172 +128,18 @@ a summary. ## Required checks on `main` -One ruleset protects `main`: **`required-checks`, id `19913330`**. **Read the live set back rather -than trusting any list written down** (`gh api "repos/cosyte/synth/rulesets?includes_parents=true"`): -a hardcoded list goes stale and it is prose no test can check. Snapshot + why every context is pinned -to the GitHub Actions app: `notes#the-one-ruleset-that-protects-main`. - -- **Extend that one ruleset in place; never add a second for the next gate.** An unpinned required - context can be satisfied by any actor with write access posting a commit status of that name, - without the workflow ever running. `notes#extend-that-one-ruleset-in-place`. -- **Never require a context before its workflow has completed on `main`**: every future PR goes - PENDING and unmergeable with nothing saying why. `test-selection`, `smoke (22)`/`smoke (24)` and - `install (22)`/`install (24)` are deliberately not required yet; read real names off a live check - run, never off a workflow's `name:`. `notes#contexts-deliberately-not-required-yet`. -- **Never rename a required job, and never split a step out of one**: both silently un-require it - and leave PRs pending rather than red (why `build` and `smoke` are one job in `smoke.yml`). - `notes#things-that-silently-detach-or-hollow-out-a-required-check`. -- **Never narrow `include` in `vitest.config.ts`.** `pnpm test` takes no path arguments, so that one - glob selects everything `ci / verify` runs, and **coverage does not backstop it** (measured over - `src/**` only, so dropping every `synthetic-safety.property.test.ts` costs 0% and reds nothing). - **For a synthetic-data generator that is the whole safety story.** Gated by - `pnpm check:test-selection`. `notes#things-that-silently-detach-or-hollow-out-a-required-check`. -- **Never replace that gate's exact-match script rule with a parser, and never answer a hole in it - with one more spelling**: analysing a shell string is unbounded, and this is the half a refuter - broke **three times** in `ncpdp`, each time in the remedy for the last. Its four deliberate shapes, - and why **self-test C is NOT covered by self-test A**: - `notes#the-test-selection-gate-and-its-four-deliberate-shapes`. -- **READ THE COUNTS OFF THE GATE, NOT OFF PROSE, AND MOVE THEM WHEN YOU ADD A TEST FILE.** - `pnpm check:test-selection` prints watched / name-only / unwatched every run; they have gone stale - **twice**. The numbers, and what the gate does **not** reach: - `notes#know-the-denominator-and-what-the-gate-does-not-reach`. -- **Deleting a subpath from `exports` shrinks that gate's headline subject and it stays green** - (measured). Not a free escape: it is a breaking change, and `scripts/smoke.mjs` derives its subpath - set from the same map and **refuses** on disagreement. Two gates on one map, on purpose. - `notes#narrowing-the-published-surface`. -- **Never require a workflow with no `pull_request` trigger** (`fuzz`, `scorecard`, `release`): it - strands every PR forever. `notes#requiring-a-workflow-with-no-pull_request-trigger`. -- **RE-MEASURE EVERY PORTED SENTENCE AGAINST THIS REPO: the one failure that repeated.** Two of - three refuter passes found a false number or a false claim of reach carried over from `ncpdp`/`deid` - rather than a hole in the mechanism. **Where a number is asserted, name what produced it; where - reach is asserted, bound it to a route actually seeded.** - `notes#re-measure-every-ported-sentence-against-this-repo`. -- **Nothing inside this repository can observe its own ruleset.** Delete it and every test still - passes, every gate still prints OK, and this file still says `main` is protected. A ruleset makes a - red check block a merge; it does not make the check correct. - `notes#nothing-inside-this-repository-can-observe-its-own-ruleset`. +Moved whole and unchanged to +[documentation/required-checks-on-main.md](documentation/required-checks-on-main.md): read it +before you touch a workflow, a job name or a required context. ## Engineering Guardrails -- No `any`. No unjustified `as` casts. Use `unknown` and narrow. -- JSDoc (with `@example`) on every public export: the JSDoc lint rule is an **error** on public - exports, so this is enforced, not optional. -- Immutable by default. Mutation only via explicit methods. -- No `console.*` in library code. Throw typed errors or return results. -- Short, testable functions over big parsing blobs. -- Postel's Law: parser is liberal (lenient default + warnings), serializer is conservative (always - emits spec-clean output). -- Fatal errors only for unrecoverable structural corruption (Tier-3 codes). Everything else is a - warning with a stable code + positional context. -- **No diagnostic takes a value parameter.** `SynthError(code)` is the only error `src/` constructs - and its message is a fixed entry in the frozen `SYNTH_FATAL_MESSAGES` table, so a caller string has - no position through which to reach `message`, `stack` or a thrown field. **Adding a fatal means - adding a registry entry, never a template string.** `notes#no-diagnostic-takes-a-value-parameter`. -- **Resolve every caller-supplied selector against its closed set** (`resolveKind`/`resolveMix`, - `src/select.ts`) at the entry point, before anything is generated. **Not a style rule**: an - unresolved selector reached an `err.message`/`err.stack` via a peer builder's `TypeError`, became an - `Artifact.kind` and a `manifest.counts` key, and fell out of an exhaustive `switch` as `undefined`. - All three were live. `notes#resolve-every-caller-supplied-selector-against-its-closed-set`. -- **Never claim `SynthError` being the only constructor is the whole guarantee**: `src/` can still - surface a runtime `TypeError` or a peer parser's fatal on forwarded input. Harnesses keep - `String(w.code)`, never a `message` or snippet; `test/phi/diagnostic-surface.test.ts` carries the - proof. `notes#syntherror-being-the-only-constructor-is-not-the-whole-guarantee`. -- **Read the meta-repo's `documentation/repos/phi-audit.md` before touching that.** **A new safety - sentence is worth nothing without a slot in that table behind it.** - `notes#the-phi-free-diagnostics-claim`. -- **▶ `attw` SAYS "does not contain types" AND EXITS 0, SO THE `attw` SCRIPT IS A WRAPPER - (`node scripts/attw.mjs --profile node16`), NOT THE BARE CLI.** For a package that ships types that - sentence means the declarations were **not in the tarball** (a broken publish reported as a pass), - and no `--profile`, `--ignore-rules` or config setting reaches that early return. **The race only - supplies the condition**, so the answer is not a lock, a lease or a build queue: the gate must be - able to say its own inputs were missing, whatever removed them. `notes#the-attw-false-green-and-why-the-script-is-a-wrapper`. -- **Do NOT re-derive the exit-0 condition from the shape of the code: three refuter passes corrected - it, each in the same direction, and "attw misses subpaths" is the plausible, wrong story.** The - residual is a **known limit, filed rather than fixed; weaken the sentence rather than adding a - fifth arm.** All three corrections, each with its measurement, and why a correct red explained by a - falsehood is worse than it looks: `notes#the-false-green-needs-every-entry-point-untyped-at-once`, - `notes#the-first-correction-was-also-wrong-in-the-same-direction`, - `notes#missing-is-a-proxy-not-the-key-a-known-limit`. -- **`scripts/attw.mjs` carries TWO nets that catch different things** (a preflight over the relative - paths `package.json` promises, and a post-check on the untyped sentence), both pinned by - `test/scripts/attw-gate.test.ts` against the real binary plus a negative control. - `notes#the-two-nets-in-scriptsattwmjs`. -- **The post-check reads a string, so the argument guard is an ALLOW-LIST, NOT A DENY-LIST**: - `--profile` and `--no-definitely-typed` forwarded, everything else refused, "harmless" included. - **Do not answer a hole in it with a seventh spelling.** The six measured routes, why a deny-list - could not hold, and why the `.attw.json` refusal stays separate: - `notes#the-argument-guard-is-an-allow-list-not-a-deny-list`. -- **The seven `file:vendor/*.tgz` devDeps are NOT part of the `attw` story**, `npm pack` emits no - `vendor/` and attw does not resolve bare external specifiers. A **stale** vendored tarball makes - this gate neither red nor green; a **missing** one is a different thing this sentence does not - cover. `notes#the-vendored-tarballs-are-not-part-of-the-attw-story`. -- Coverage: per-directory >= 90% (lines/branches/functions/statements), enforced by - `pnpm test:coverage`. +Moved whole and unchanged to +[documentation/engineering-guardrails.md](documentation/engineering-guardrails.md): read it before +you touch the thing a guardrail guards. ## Standing disciplines (every change) -Mirrors the three disciplines in the meta-repo's `documentation/conventions.md`, and they bind here too: - -1. **Documentation follows code**: a change to the public surface/stack/status isn't done until the - docs are: this repo's docs content (`README.md`, `docs-content/`), the meta-repo - `documentation/repos/synth.md` (bump its "last verified" date), and the `ecosystem-map.md` - status table. -2. **Version + changelog**: a Changeset (`patch` on the `0.0.x` ladder) per meaningful change. - **The changeset summary IS the changelog entry**, and `CHANGELOG.md` is generated output above - `## Released before this file was generated`: `.changeset/config.json` names a `changelog` - generator, so the release writes the version heading and the entry itself. **Do not hand-edit - `CHANGELOG.md`, and never reintroduce a hand-maintained `[Unreleased]` heading**: one stood over - this package's whole published history and shipped a tarball calling its own contents unreleased; - with generation on, a release prepends ABOVE such a heading, which is worse. Only the H1 may sit - above the first heading, and Changesets' Prettier pass stays ON here (derived, not copied): - `notes#the-changelog-is-generated-by-the-release`. Gated by - `test/scripts/changelog-generation.test.ts`. Renaming a stable warning code is a **breaking - change**. -3. **Crew + knowledgebase loop**, if this parser's public API or warning codes change, flag/update - the matching `crew` healthcare skill + the KB product doc. -4. **No internal project bookkeeping on a public surface** (founder directive, 2026-07-27). What a - consumer reads (`README.md`, `docs-content/`, the npm `description`, a release body) says what the - software does and what changed; identifiers, phase/wave language, ADR numbers, meta-repo paths and - `roadmap §N` citations belong in the changeset, `CHANGELOG.md`, the commit, the PR and the - roadmap. It is a **translation** at the boundary, not a deletion: **repair the head of a line you - strip an identifier from**. Gated by `pnpm check:no-internal-refs`, which keys on known prefixes - (**a new programme prefix must be added by hand**) and catches identifiers, not English sentences - about our process, so the reviewer still owns half the rule. - `notes#no-internal-project-bookkeeping-on-a-public-surface`. - - **`SYNTH` is BOTH our item prefix AND the marker this package writes into what it generates** - (`SYNTH-FAC`, `SYNTH-LIS`, `SYNTH-ANALYZER`, the `SYNTH-`+10-digit NCPDP message id). The - separation is the explicit `SYNTHETIC_FIXTURE_TOKEN` list in the gate, never a shape rule. - **Never re-key the identifier rule on the `WORD-N` shape**, and never "resync" the prefix list - with a sibling's copy without re-reading why `SYNTH` is PRESENT here and ABSENT from `ncpdp`'s. - `notes#synth-is-both-our-item-prefix-and-a-marker-in-generated-output`. - - **Four surfaces, three different answers.** Browsable Markdown, npm metadata and `/** */` doc - comments are **gated**; string literals are **gated too, and here they matter most: they are the - bytes this package emits**; `//` and plain `/* */` comments are **not gated and identifiers are - welcome in them**, because the convention says source comments are a place identifiers belong. - **Do not justify this boundary from what reaches `dist/`**: effectively all of `src/` does, and - two attempts to argue it that way in a sibling repo were false and both caught by a refuter. The - line is not what reaches a consumer's disk but what a consumer is **shown**. - `notes#four-surfaces-three-different-answers`. - - Two consequences: **removing a doc comment to satisfy the gate is a regression**, not a fix; and - **when a stale claim is what carries the phase number, cut the sentence rather than reword it**: - rewording leaves a falsehood standing in cleaner clothes. The gate cannot read `dist/`, so it - gates the source of the published text, not the published text. - `notes#two-consequences-doc-comments-and-stale-phase-claims`. - -5. **No em dash, anywhere** (founder directive, 2026-07-24), **including commit messages**. Gated by - `pnpm check:no-emdash` over every tracked file, its name, the gate script, and on a PR the title, - body and commit messages. Rewrite with a comma, colon, period or **parentheses**; **never - re-encode it**. Landed 2026-08-06 with the sweep (1,167 rewrites over 135 files). - Full text and the sweep's own traps: `notes#no-em-dash-anywhere`. Two that gate what you change: - - **The job is deliberately NOT a required context**: it reads the PR body, and Dependabot pastes - the dependency's upstream release notes there. **Keep the exemption and its written reason, and - never answer it with an actor `if:` on a required context** (that leaves the check PENDING, not - red). **PR text is also the half no local hook can see, so check your own before you push.** - - **Do not simplify two deliberate shapes.** The binary partition is a **declaration** - (`git check-attr binary`, refused outside `vendor/`), never a NUL test and never `grep -I`, - because a real UTF-8 source here holds a NUL and carried 14. And `CHANGELOG.md` is scanned - **above** `## Released before this file was generated` rather than excluded, the archive below - being a dated ship-log (128 deliberate survivors, and it fails closed). +Moved whole and unchanged to +[documentation/standing-disciplines.md](documentation/standing-disciplines.md): read it before you +open a change. diff --git a/documentation/engineering-guardrails.md b/documentation/engineering-guardrails.md new file mode 100644 index 0000000..e2c9b1b --- /dev/null +++ b/documentation/engineering-guardrails.md @@ -0,0 +1,59 @@ +Split out of `CLAUDE.md` to keep that file inside its byte budget. The text below is unchanged. +A pointer written `notes#
` names a section of `agent-notes.md` beside this file. + +## Engineering Guardrails + +- No `any`. No unjustified `as` casts. Use `unknown` and narrow. +- JSDoc (with `@example`) on every public export: the JSDoc lint rule is an **error** on public + exports, so this is enforced, not optional. +- Immutable by default. Mutation only via explicit methods. +- No `console.*` in library code. Throw typed errors or return results. +- Short, testable functions over big parsing blobs. +- Postel's Law: parser is liberal (lenient default + warnings), serializer is conservative (always + emits spec-clean output). +- Fatal errors only for unrecoverable structural corruption (Tier-3 codes). Everything else is a + warning with a stable code + positional context. +- **No diagnostic takes a value parameter.** `SynthError(code)` is the only error `src/` constructs + and its message is a fixed entry in the frozen `SYNTH_FATAL_MESSAGES` table, so a caller string has + no position through which to reach `message`, `stack` or a thrown field. **Adding a fatal means + adding a registry entry, never a template string.** `notes#no-diagnostic-takes-a-value-parameter`. +- **Resolve every caller-supplied selector against its closed set** (`resolveKind`/`resolveMix`, + `src/select.ts`) at the entry point, before anything is generated. **Not a style rule**: an + unresolved selector reached an `err.message`/`err.stack` via a peer builder's `TypeError`, became an + `Artifact.kind` and a `manifest.counts` key, and fell out of an exhaustive `switch` as `undefined`. + All three were live. `notes#resolve-every-caller-supplied-selector-against-its-closed-set`. +- **Never claim `SynthError` being the only constructor is the whole guarantee**: `src/` can still + surface a runtime `TypeError` or a peer parser's fatal on forwarded input. Harnesses keep + `String(w.code)`, never a `message` or snippet; `test/phi/diagnostic-surface.test.ts` carries the + proof. `notes#syntherror-being-the-only-constructor-is-not-the-whole-guarantee`. +- **Read the meta-repo's `documentation/repos/phi-audit.md` before touching that.** **A new safety + sentence is worth nothing without a slot in that table behind it.** + `notes#the-phi-free-diagnostics-claim`. +- **▶ `attw` SAYS "does not contain types" AND EXITS 0, SO THE `attw` SCRIPT IS A WRAPPER + (`node scripts/attw.mjs --profile node16`), NOT THE BARE CLI.** For a package that ships types that + sentence means the declarations were **not in the tarball** (a broken publish reported as a pass), + and no `--profile`, `--ignore-rules` or config setting reaches that early return. **The race only + supplies the condition**, so the answer is not a lock, a lease or a build queue: the gate must be + able to say its own inputs were missing, whatever removed them. `notes#the-attw-false-green-and-why-the-script-is-a-wrapper`. +- **Do NOT re-derive the exit-0 condition from the shape of the code: three refuter passes corrected + it, each in the same direction, and "attw misses subpaths" is the plausible, wrong story.** The + residual is a **known limit, filed rather than fixed; weaken the sentence rather than adding a + fifth arm.** All three corrections, each with its measurement, and why a correct red explained by a + falsehood is worse than it looks: `notes#the-false-green-needs-every-entry-point-untyped-at-once`, + `notes#the-first-correction-was-also-wrong-in-the-same-direction`, + `notes#missing-is-a-proxy-not-the-key-a-known-limit`. +- **`scripts/attw.mjs` carries TWO nets that catch different things** (a preflight over the relative + paths `package.json` promises, and a post-check on the untyped sentence), both pinned by + `test/scripts/attw-gate.test.ts` against the real binary plus a negative control. + `notes#the-two-nets-in-scriptsattwmjs`. +- **The post-check reads a string, so the argument guard is an ALLOW-LIST, NOT A DENY-LIST**: + `--profile` and `--no-definitely-typed` forwarded, everything else refused, "harmless" included. + **Do not answer a hole in it with a seventh spelling.** The six measured routes, why a deny-list + could not hold, and why the `.attw.json` refusal stays separate: + `notes#the-argument-guard-is-an-allow-list-not-a-deny-list`. +- **The seven `file:vendor/*.tgz` devDeps are NOT part of the `attw` story**, `npm pack` emits no + `vendor/` and attw does not resolve bare external specifiers. A **stale** vendored tarball makes + this gate neither red nor green; a **missing** one is a different thing this sentence does not + cover. `notes#the-vendored-tarballs-are-not-part-of-the-attw-story`. +- Coverage: per-directory >= 90% (lines/branches/functions/statements), enforced by + `pnpm test:coverage`. diff --git a/documentation/required-checks-on-main.md b/documentation/required-checks-on-main.md new file mode 100644 index 0000000..051b873 --- /dev/null +++ b/documentation/required-checks-on-main.md @@ -0,0 +1,49 @@ +Split out of `CLAUDE.md` to keep that file inside its byte budget. The text below is unchanged. +A pointer written `notes#
` names a section of `agent-notes.md` beside this file. + +## Required checks on `main` + +One ruleset protects `main`: **`required-checks`, id `19913330`**. **Read the live set back rather +than trusting any list written down** (`gh api "repos/cosyte/synth/rulesets?includes_parents=true"`): +a hardcoded list goes stale and it is prose no test can check. Snapshot + why every context is pinned +to the GitHub Actions app: `notes#the-one-ruleset-that-protects-main`. + +- **Extend that one ruleset in place; never add a second for the next gate.** An unpinned required + context can be satisfied by any actor with write access posting a commit status of that name, + without the workflow ever running. `notes#extend-that-one-ruleset-in-place`. +- **Never require a context before its workflow has completed on `main`**: every future PR goes + PENDING and unmergeable with nothing saying why. `test-selection`, `smoke (22)`/`smoke (24)` and + `install (22)`/`install (24)` are deliberately not required yet; read real names off a live check + run, never off a workflow's `name:`. `notes#contexts-deliberately-not-required-yet`. +- **Never rename a required job, and never split a step out of one**: both silently un-require it + and leave PRs pending rather than red (why `build` and `smoke` are one job in `smoke.yml`). + `notes#things-that-silently-detach-or-hollow-out-a-required-check`. +- **Never narrow `include` in `vitest.config.ts`.** `pnpm test` takes no path arguments, so that one + glob selects everything `ci / verify` runs, and **coverage does not backstop it** (measured over + `src/**` only, so dropping every `synthetic-safety.property.test.ts` costs 0% and reds nothing). + **For a synthetic-data generator that is the whole safety story.** Gated by + `pnpm check:test-selection`. `notes#things-that-silently-detach-or-hollow-out-a-required-check`. +- **Never replace that gate's exact-match script rule with a parser, and never answer a hole in it + with one more spelling**: analysing a shell string is unbounded, and this is the half a refuter + broke **three times** in `ncpdp`, each time in the remedy for the last. Its four deliberate shapes, + and why **self-test C is NOT covered by self-test A**: + `notes#the-test-selection-gate-and-its-four-deliberate-shapes`. +- **READ THE COUNTS OFF THE GATE, NOT OFF PROSE, AND MOVE THEM WHEN YOU ADD A TEST FILE.** + `pnpm check:test-selection` prints watched / name-only / unwatched every run; they have gone stale + **twice**. The numbers, and what the gate does **not** reach: + `notes#know-the-denominator-and-what-the-gate-does-not-reach`. +- **Deleting a subpath from `exports` shrinks that gate's headline subject and it stays green** + (measured). Not a free escape: it is a breaking change, and `scripts/smoke.mjs` derives its subpath + set from the same map and **refuses** on disagreement. Two gates on one map, on purpose. + `notes#narrowing-the-published-surface`. +- **Never require a workflow with no `pull_request` trigger** (`fuzz`, `scorecard`, `release`): it + strands every PR forever. `notes#requiring-a-workflow-with-no-pull_request-trigger`. +- **RE-MEASURE EVERY PORTED SENTENCE AGAINST THIS REPO: the one failure that repeated.** Two of + three refuter passes found a false number or a false claim of reach carried over from `ncpdp`/`deid` + rather than a hole in the mechanism. **Where a number is asserted, name what produced it; where + reach is asserted, bound it to a route actually seeded.** + `notes#re-measure-every-ported-sentence-against-this-repo`. +- **Nothing inside this repository can observe its own ruleset.** Delete it and every test still + passes, every gate still prints OK, and this file still says `main` is protected. A ruleset makes a + red check block a merge; it does not make the check correct. + `notes#nothing-inside-this-repository-can-observe-its-own-ruleset`. diff --git a/documentation/standing-disciplines.md b/documentation/standing-disciplines.md new file mode 100644 index 0000000..1af360d --- /dev/null +++ b/documentation/standing-disciplines.md @@ -0,0 +1,70 @@ +Split out of `CLAUDE.md` to keep that file inside its byte budget. The text below is unchanged. +A pointer written `notes#
` names a section of `agent-notes.md` beside this file. + +## Standing disciplines (every change) + +Mirrors the three disciplines in the meta-repo's `documentation/conventions.md`, and they bind here too: + +1. **Documentation follows code**: a change to the public surface/stack/status isn't done until the + docs are: this repo's docs content (`README.md`, `docs-content/`), the meta-repo + `documentation/repos/synth.md` (bump its "last verified" date), and the `ecosystem-map.md` + status table. +2. **Version + changelog**: a Changeset (`patch` on the `0.0.x` ladder) per meaningful change. + **The changeset summary IS the changelog entry**, and `CHANGELOG.md` is generated output above + `## Released before this file was generated`: `.changeset/config.json` names a `changelog` + generator, so the release writes the version heading and the entry itself. **Do not hand-edit + `CHANGELOG.md`, and never reintroduce a hand-maintained `[Unreleased]` heading**: one stood over + this package's whole published history and shipped a tarball calling its own contents unreleased; + with generation on, a release prepends ABOVE such a heading, which is worse. Only the H1 may sit + above the first heading, and Changesets' Prettier pass stays ON here (derived, not copied): + `notes#the-changelog-is-generated-by-the-release`. Gated by + `test/scripts/changelog-generation.test.ts`. Renaming a stable warning code is a **breaking + change**. +3. **Crew + knowledgebase loop**, if this parser's public API or warning codes change, flag/update + the matching `crew` healthcare skill + the KB product doc. +4. **No internal project bookkeeping on a public surface** (founder directive, 2026-07-27). What a + consumer reads (`README.md`, `docs-content/`, the npm `description`, a release body) says what the + software does and what changed; identifiers, phase/wave language, ADR numbers, meta-repo paths and + `roadmap §N` citations belong in the changeset, `CHANGELOG.md`, the commit, the PR and the + roadmap. It is a **translation** at the boundary, not a deletion: **repair the head of a line you + strip an identifier from**. Gated by `pnpm check:no-internal-refs`, which keys on known prefixes + (**a new programme prefix must be added by hand**) and catches identifiers, not English sentences + about our process, so the reviewer still owns half the rule. + `notes#no-internal-project-bookkeeping-on-a-public-surface`. + + **`SYNTH` is BOTH our item prefix AND the marker this package writes into what it generates** + (`SYNTH-FAC`, `SYNTH-LIS`, `SYNTH-ANALYZER`, the `SYNTH-`+10-digit NCPDP message id). The + separation is the explicit `SYNTHETIC_FIXTURE_TOKEN` list in the gate, never a shape rule. + **Never re-key the identifier rule on the `WORD-N` shape**, and never "resync" the prefix list + with a sibling's copy without re-reading why `SYNTH` is PRESENT here and ABSENT from `ncpdp`'s. + `notes#synth-is-both-our-item-prefix-and-a-marker-in-generated-output`. + + **Four surfaces, three different answers.** Browsable Markdown, npm metadata and `/** */` doc + comments are **gated**; string literals are **gated too, and here they matter most: they are the + bytes this package emits**; `//` and plain `/* */` comments are **not gated and identifiers are + welcome in them**, because the convention says source comments are a place identifiers belong. + **Do not justify this boundary from what reaches `dist/`**: effectively all of `src/` does, and + two attempts to argue it that way in a sibling repo were false and both caught by a refuter. The + line is not what reaches a consumer's disk but what a consumer is **shown**. + `notes#four-surfaces-three-different-answers`. + + Two consequences: **removing a doc comment to satisfy the gate is a regression**, not a fix; and + **when a stale claim is what carries the phase number, cut the sentence rather than reword it**: + rewording leaves a falsehood standing in cleaner clothes. The gate cannot read `dist/`, so it + gates the source of the published text, not the published text. + `notes#two-consequences-doc-comments-and-stale-phase-claims`. + +5. **No em dash, anywhere** (founder directive, 2026-07-24), **including commit messages**. Gated by + `pnpm check:no-emdash` over every tracked file, its name, the gate script, and on a PR the title, + body and commit messages. Rewrite with a comma, colon, period or **parentheses**; **never + re-encode it**. Landed 2026-08-06 with the sweep (1,167 rewrites over 135 files). + Full text and the sweep's own traps: `notes#no-em-dash-anywhere`. Two that gate what you change: + - **The job is deliberately NOT a required context**: it reads the PR body, and Dependabot pastes + the dependency's upstream release notes there. **Keep the exemption and its written reason, and + never answer it with an actor `if:` on a required context** (that leaves the check PENDING, not + red). **PR text is also the half no local hook can see, so check your own before you push.** + - **Do not simplify two deliberate shapes.** The binary partition is a **declaration** + (`git check-attr binary`, refused outside `vendor/`), never a NUL test and never `grep -I`, + because a real UTF-8 source here holds a NUL and carried 14. And `CHANGELOG.md` is scanned + **above** `## Released before this file was generated` rather than excluded, the archive below + being a dated ship-log (128 deliberate survivors, and it fails closed).