Skip to content

style(brand): sweep U+2014 out of the tree and land the CI gate with it (EMDASH-CONFORMANCE) - #33

Merged
NSchatz merged 1 commit into
mainfrom
emdash-conformance-sweep-and-gate
Aug 7, 2026
Merged

style(brand): sweep U+2014 out of the tree and land the CI gate with it (EMDASH-CONFORMANCE)#33
NSchatz merged 1 commit into
mainfrom
emdash-conformance-sweep-and-gate

Conversation

@NSchatz

@NSchatz NSchatz commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The em-dash ban is a founder directive of 2026-07-24 and it covers every surface
this repository has, including commit messages, PR titles and PR bodies. This
package carried 659 occurrences across 75 of its 98 tracked files, and had no
gate at all.

The sweep and the gate are one commit on purpose. A gate arriving before its
sweep reds main on arrival; a sweep arriving before its gate grows the character
back on the next session.

THE CENSUS WAS RE-DERIVED, NOT INHERITED

The umbrella backlog recorded 660 across 76 files. Counted here in Python over
raw bytes, this tree reads 659 across 75, and the two agree: the npm description
fix that landed as #30 removed exactly one occurrence from exactly one file. The
count matters because the original org-wide census was taken with a broken
scanner: in the agent containers grep is a shell function forcing -I, and under
xargs the real binary fails at exit 2 and prints nothing in the empty locale, so
a piped count reads zero. Every other spelling the rule names (the named HTML
entity, the decimal and hex character references, the percent-encoding, both
JavaScript escapes) was searched for before and after and is absent.

WHAT MOVED

609 occurrences rewritten across 73 files, each by what the sentence wanted
rather than by one substitution: a colon where the dash introduced an appositive,
a comma before a conjunction or a negation, parentheses where the mark scoped an
aside. Consumer-visible surfaces in that count: README.md, the seven docs-content
pages that publish to the documentation site, and the src/ JSDoc that compiles
into dist/index.d.ts and dist/index.d.cts and renders on hover.

Three things were done BY HAND, before any bulk pass, because a bulk pass gets
them wrong in a way a green suite does not show:

  • The one runtime string. scripts/phi-scan.ts's clean-run line is quoted in
    prose in two docblocks and asserted by regex in three tests. All five sites
    moved together. It now reads "[phi-scan] OK: no hits". The check-agent-notes
    R5 orphan message moved the same way; its test asserts on the token ORPHAN,
    so nothing else had to.
  • The one semantic value. diagnostic-report.ts's OBR mapping table used a bare
    dash cell to mean "nothing here", which a bulk rewrite turns into a stray
    mark that reads as a rendering artefact. OBR-8 goes through toFhirDateTime at
    the call site, so the cell now names that converter.
  • The slug fixtures in the agent-notes contract suite, which prove GitHub gives
    each space its own hyphen using a heading whose dropped mark has a space on
    either side. They now use an en dash. slug() drops it identically and both
    surrounding spaces still survive as two hyphens, so the behaviour under test
    and the bite of the two cases are unchanged. The trap did not go away: every
    dropped mark with a space either side reproduces it.

No exported name, type, issue code, fatal code or documented behaviour changed.

AND THE RULE THIS CHANGE STATES IS THE ONE IT KEPT

"A comma where the clause already carried a colon" is the rule, and a sibling
repository put a colon into thirteen clauses that already had one while stating
it. Measured here by joining wrapped prose into sentences, stripping backticked
spans and URLs, and counting colons per sentence on both sides of the diff:
nineteen sites were caught and corrected by hand, in README.md, three
docs-content pages, agent-notes.md, six src/ modules, two test files and two
scripts. The ones left are sound, and the test was applied rather than assumed:
the second colon is inside parentheses, inside a code span, or in a different
sentence.

Two of those nineteen were more than punctuation and are called out here because
they read as claims. src/messages/orders.ts had "incorporated into that same
request: OBR -> ServiceRequest", where the colon read as a definition of the
preceding phrase rather than a consequence of it; it now reads "so". And
src/messages/service-request.ts's priority bullet had a colon in its own label
AND in the sentence introducing the map.

TWO EXEMPTIONS, EACH WITH A WRITTEN REASON, AND NOTHING SKIPPED SILENTLY

50 occurrences remain. 49 are in CHANGELOG.md below its "Released before this
file was generated" heading: a dated ship-log whose entries are byte identical to
the tarballs they shipped in, measured by test/scripts/changelog-generation.test.ts.
The gate scans the generated half above the heading, because a changeset summary
becomes the published release body and a line in the tarball's changelog, and it
FAILS CLOSED: if the heading goes, the whole file is in scope. That archive is
also the gate's on-disk canary.

The last one is inside vendor/cosyte-hl7-0.0.0.tgz, a third-party pnpm pack
tarball whose DEFLATE stream holds the bytes by coincidence. It is declared
binary in a new .gitattributes, alongside the fhir tarball. Both blobs are
byte identical to base. That file cannot become a silencer: the gate REFUSES any
binary declaration outside vendor/, and it scans tracked FILENAMES whatever the
declaration says.

Everything else was swept, including CLAUDE.md and documentation/agent-notes.md.
The banner on the notes protects its claims from being softened, not its bytes
from being repunctuated, and an exemption there would grow because that file is
appended to. The one passage reproduced verbatim was swept too, and the sentence
introducing it now says so: the wording, every claim and every qualifier are
untouched and only the dash punctuation moved.

THE GATE

scripts/check-no-emdash.mjs shells out for nothing. It reads bytes with node:fs;
the only child processes are git ls-files and git check-attr, and both have their
exit status checked. It excludes nothing by path, because every banned spelling is
assembled at runtime from the codepoint, so the script holds itself to its own
rule rather than needing the self-exclusion that let an em dash scan green in a
sibling. Six probe checks refuse rather than report a clean tree it cannot prove
it read, and the enumeration floor is 80 paths, deliberately below the 103 tracked
today so an ordinary deletion is not mistaken for a filtered scan.

.github/workflows/no-emdash.yml runs two jobs, and the split is the design.
no-emdash scans tracked files and filenames and is safe to require, once it has
run on main, folded into ruleset 19914044 like every other context here.
no-emdash-messages scans the PR title, body and commit range and MUST NEVER be
required: Dependabot pastes the dependency's upstream release notes into a PR
body, and requiring it would block a dependency bump on prose nobody here wrote.
An actor if: on a required context is not the fix either; that leaves the check
pending, which is worse than red.

41 tests pin all of it: every banned spelling reds, every near miss stays green,
the gate holds its own source, the CHANGELOG boundary is proved on both sides and
with the heading removed, and every refusal is driven through a real scan of a
synthetic repository.

Three of those 41 exist because a refuter refuted this commit twice, and the
third is the one that closes the class.
The probe checked only that scanned + binary == tracked, and that arithmetic
balances for a path exclusion which ACCOUNTS for what it skips: push the skipped
paths onto the declared-binary list and the sum still adds up, while the
outside-vendor refusal reads .gitattributes rather than the skip list and never
sees them. Adding || rel.startsWith("docs-content/") to the skip condition, 34
characters, hid seven pages that publish to the documentation site, printed a
clean banner and left all 38 tests green. probe() check 5b now reconciles the two
sets path for path, the real-tree case pins the banner at "2 declared binary",
and a mutation case reproduces that exclusion against a copy of the gate in a
throwaway repository and requires the refusal, with the unmutated fixture beside
it as the near miss. Proven red-before, green-after: the pre-fix gate exits 0 on
that fixture.

Its second pass showed that fix closed one bucket of two. Every partition check
counts FILES, so an exclusion that pushes what it skips onto the SCANNED list
rather than the declared-binary one leaves all of them byte identical to an
honest run: the path really is classified and really is counted, it is just never
opened. With a live em dash planted on a page that publishes to the documentation
site it printed the same banner, exited 0, and left all 40 tests green. The one
quantity such a mutation cannot fake is bytesRead, which the gate already prints,
so the suite now recomputes the expected total INDEPENDENTLY, from git ls-files
plus git check-attr plus statSync, and asserts the banner against it. That
assertion has to live in the test rather than in probe(), because an in-script
invariant can always be satisfied by the same edit that breaks the property. Both
surviving mutations red on it and on nothing else, measured.

DOCS

CLAUDE.md gains standing discipline 5 and stays inside its byte budget, which
took relocating narrative that documentation/agent-notes.md already carries: the
Dependabot limits, the older publish-state wording, and the provenance of the
third PHI-scanner residual. No trap was deleted to make room. The long form of
the sweep, both counts, both exemptions and every trap above is a new
"No em dash, anywhere" section in documentation/agent-notes.md.

Verified: typecheck, lint, format:check, phi-scan, check-agent-notes,
check-no-internal-refs, check-no-emdash, 388 tests, coverage, build, attw.
Refuter verdict on the fixed commit: pending re-check of the one finding above.

…it (EMDASH-CONFORMANCE)

The em-dash ban is a founder directive of 2026-07-24 and it covers every surface
this repository has, including commit messages, PR titles and PR bodies. This
package carried 659 occurrences across 75 of its 98 tracked files, and had no
gate at all.

The sweep and the gate are one commit on purpose. A gate arriving before its
sweep reds main on arrival; a sweep arriving before its gate grows the character
back on the next session.

THE CENSUS WAS RE-DERIVED, NOT INHERITED

The umbrella backlog recorded 660 across 76 files. Counted here in Python over
raw bytes, this tree reads 659 across 75, and the two agree: the npm description
fix that landed as #30 removed exactly one occurrence from exactly one file. The
count matters because the original org-wide census was taken with a broken
scanner: in the agent containers grep is a shell function forcing -I, and under
xargs the real binary fails at exit 2 and prints nothing in the empty locale, so
a piped count reads zero. Every other spelling the rule names (the named HTML
entity, the decimal and hex character references, the percent-encoding, both
JavaScript escapes) was searched for before and after and is absent.

WHAT MOVED

609 occurrences rewritten across 73 files, each by what the sentence wanted
rather than by one substitution: a colon where the dash introduced an appositive,
a comma before a conjunction or a negation, parentheses where the mark scoped an
aside. Consumer-visible surfaces in that count: README.md, the seven docs-content
pages that publish to the documentation site, and the src/ JSDoc that compiles
into dist/index.d.ts and dist/index.d.cts and renders on hover.

Three things were done BY HAND, before any bulk pass, because a bulk pass gets
them wrong in a way a green suite does not show:

  * The one runtime string. scripts/phi-scan.ts's clean-run line is quoted in
    prose in two docblocks and asserted by regex in three tests. All five sites
    moved together. It now reads "[phi-scan] OK: no hits". The check-agent-notes
    R5 orphan message moved the same way; its test asserts on the token ORPHAN,
    so nothing else had to.
  * The one semantic value. diagnostic-report.ts's OBR mapping table used a bare
    dash cell to mean "nothing here", which a bulk rewrite turns into a stray
    mark that reads as a rendering artefact. OBR-8 goes through toFhirDateTime at
    the call site, so the cell now names that converter.
  * The slug fixtures in the agent-notes contract suite, which prove GitHub gives
    each space its own hyphen using a heading whose dropped mark has a space on
    either side. They now use an en dash. slug() drops it identically and both
    surrounding spaces still survive as two hyphens, so the behaviour under test
    and the bite of the two cases are unchanged. The trap did not go away: every
    dropped mark with a space either side reproduces it.

No exported name, type, issue code, fatal code or documented behaviour changed.

AND THE RULE THIS CHANGE STATES IS THE ONE IT KEPT

"A comma where the clause already carried a colon" is the rule, and a sibling
repository put a colon into thirteen clauses that already had one while stating
it. Measured here by joining wrapped prose into sentences, stripping backticked
spans and URLs, and counting colons per sentence on both sides of the diff:
nineteen sites were caught and corrected by hand, in README.md, three
docs-content pages, agent-notes.md, six src/ modules, two test files and two
scripts. The ones left are sound, and the test was applied rather than assumed:
the second colon is inside parentheses, inside a code span, or in a different
sentence.

Two of those nineteen were more than punctuation and are called out here because
they read as claims. src/messages/orders.ts had "incorporated into that same
request: OBR -> ServiceRequest", where the colon read as a definition of the
preceding phrase rather than a consequence of it; it now reads "so". And
src/messages/service-request.ts's priority bullet had a colon in its own label
AND in the sentence introducing the map.

TWO EXEMPTIONS, EACH WITH A WRITTEN REASON, AND NOTHING SKIPPED SILENTLY

50 occurrences remain. 49 are in CHANGELOG.md below its "Released before this
file was generated" heading: a dated ship-log whose entries are byte identical to
the tarballs they shipped in, measured by test/scripts/changelog-generation.test.ts.
The gate scans the generated half above the heading, because a changeset summary
becomes the published release body and a line in the tarball's changelog, and it
FAILS CLOSED: if the heading goes, the whole file is in scope. That archive is
also the gate's on-disk canary.

The last one is inside vendor/cosyte-hl7-0.0.0.tgz, a third-party pnpm pack
tarball whose DEFLATE stream holds the bytes by coincidence. It is declared
binary in a new .gitattributes, alongside the fhir tarball. Both blobs are
byte identical to base. That file cannot become a silencer: the gate REFUSES any
binary declaration outside vendor/, and it scans tracked FILENAMES whatever the
declaration says.

Everything else was swept, including CLAUDE.md and documentation/agent-notes.md.
The banner on the notes protects its claims from being softened, not its bytes
from being repunctuated, and an exemption there would grow because that file is
appended to. The one passage reproduced verbatim was swept too, and the sentence
introducing it now says so: the wording, every claim and every qualifier are
untouched and only the dash punctuation moved.

THE GATE

scripts/check-no-emdash.mjs shells out for nothing. It reads bytes with node:fs;
the only child processes are git ls-files and git check-attr, and both have their
exit status checked. It excludes nothing by path, because every banned spelling is
assembled at runtime from the codepoint, so the script holds itself to its own
rule rather than needing the self-exclusion that let an em dash scan green in a
sibling. Six probe checks refuse rather than report a clean tree it cannot prove
it read, and the enumeration floor is 80 paths, deliberately below the 103 tracked
today so an ordinary deletion is not mistaken for a filtered scan.

.github/workflows/no-emdash.yml runs two jobs, and the split is the design.
no-emdash scans tracked files and filenames and is safe to require, once it has
run on main, folded into ruleset 19914044 like every other context here.
no-emdash-messages scans the PR title, body and commit range and MUST NEVER be
required: Dependabot pastes the dependency's upstream release notes into a PR
body, and requiring it would block a dependency bump on prose nobody here wrote.
An actor if: on a required context is not the fix either; that leaves the check
pending, which is worse than red.

41 tests pin all of it: every banned spelling reds, every near miss stays green,
the gate holds its own source, the CHANGELOG boundary is proved on both sides and
with the heading removed, and every refusal is driven through a real scan of a
synthetic repository.

Three of those 41 exist because a refuter refuted this commit twice, and the
third is the one that closes the class.
The probe checked only that scanned + binary == tracked, and that arithmetic
balances for a path exclusion which ACCOUNTS for what it skips: push the skipped
paths onto the declared-binary list and the sum still adds up, while the
outside-vendor refusal reads .gitattributes rather than the skip list and never
sees them. Adding `|| rel.startsWith("docs-content/")` to the skip condition, 34
characters, hid seven pages that publish to the documentation site, printed a
clean banner and left all 38 tests green. probe() check 5b now reconciles the two
sets path for path, the real-tree case pins the banner at "2 declared binary",
and a mutation case reproduces that exclusion against a copy of the gate in a
throwaway repository and requires the refusal, with the unmutated fixture beside
it as the near miss. Proven red-before, green-after: the pre-fix gate exits 0 on
that fixture.

Its second pass showed that fix closed one bucket of two. Every partition check
counts FILES, so an exclusion that pushes what it skips onto the SCANNED list
rather than the declared-binary one leaves all of them byte identical to an
honest run: the path really is classified and really is counted, it is just never
opened. With a live em dash planted on a page that publishes to the documentation
site it printed the same banner, exited 0, and left all 40 tests green. The one
quantity such a mutation cannot fake is bytesRead, which the gate already prints,
so the suite now recomputes the expected total INDEPENDENTLY, from git ls-files
plus git check-attr plus statSync, and asserts the banner against it. That
assertion has to live in the test rather than in probe(), because an in-script
invariant can always be satisfied by the same edit that breaks the property. Both
surviving mutations red on it and on nothing else, measured.

DOCS

CLAUDE.md gains standing discipline 5 and stays inside its byte budget, which
took relocating narrative that documentation/agent-notes.md already carries: the
Dependabot limits, the older publish-state wording, and the provenance of the
third PHI-scanner residual. No trap was deleted to make room. The long form of
the sweep, both counts, both exemptions and every trap above is a new
"No em dash, anywhere" section in documentation/agent-notes.md.

Verified: typecheck, lint, format:check, phi-scan, check-agent-notes,
check-no-internal-refs, check-no-emdash, 388 tests, coverage, build, attw.
Refuter verdict on the fixed commit: pending re-check of the one finding above.
@NSchatz
NSchatz force-pushed the emdash-conformance-sweep-and-gate branch from ff5df61 to b8c2a50 Compare August 7, 2026 06:02
@NSchatz
NSchatz merged commit 9fdde50 into main Aug 7, 2026
9 checks passed
@NSchatz
NSchatz deleted the emdash-conformance-sweep-and-gate branch August 7, 2026 06:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant