Sweep the repo-wide em dash and gate it in the test suite - #525
Conversation
CLAUDE.md bans U+2014 anywhere: code, comments, JSDoc, strings, or docs. The tree carried 2438 offending lines across 196 files, so the rule was documentation rather than a property of the repo. The gate comes first. `test/core/house-style-em-dash.test.js` scans every tracked, non-binary file for the character and fails with the offending `file:line` list. It failed on the pre-sweep tree with those 2438 lines and passes now, which is what makes this sweep the last one. The replacement is not mechanical. Prose takes the punctuation the sentence wants: a colon after a label or before the clause that explains, a comma before a connector or around an aside that used a pair of dashes as brackets, a semicolon where a comma would join an aside to the list a parenthesis already holds. Runtime strings, style declarations, JSON descriptions and placeholder table cells take the plain `-` CLAUDE.md asks for. No rationale is reworded and no text is reflowed: every file keeps its line count. Nineteen LLP titles and five `## Part N` headings were repunctuated by hand where the generated mark read badly, and the 90 heading anchors that changed slug as a result are repointed at all 34 of their call sites, so every `@ref` and in-repo anchor link still resolves. The `@ref` gloss gate is widened at the same time. Its em-dash check only ever read the line the `@ref` token sat on, so a gloss that wrapped could carry the character on its second line unseen; it now reads the whole gloss, with a fixture test for where a gloss starts and stops. `usage-policy-classification.test.js` asserts the consent copy carries no em dash. That assertion spells the character as a `\u2014` escape now, so the gate that backs it cannot sweep the assertion itself into a hyphen. Fixes #472 Co-Authored-By: Claude <noreply@anthropic.com>
🤖 neutral: merge-ordering note - this PR overlaps 10 of the 11 other open branchesIndependently verified before writing this: 198 files on The sweep is sound. The risk is ordering, not correctness.
Two branches (#505, #506) have most of their files rewritten by this PR. Why it matters beyond ordinary conflicts: the sweep repunctuates headings, which changes their slugs - 90 anchors moved and 34 Recommendation: merge this one last, after the eight in-flight fix PRs settle. It is pure hygiene with a regression gate attached, so it loses nothing by waiting, whereas every branch it lands ahead of pays a rebase plus an If you would rather land it first, say so on this thread and neutral will run the merge-base rung across the affected branches and re-audit their Two notes on scope, both from the worker's own report and both worth your eye:
|
The sweep's rule is a colon after a definition-list label, and a comma only when the clause already carries a colon. Two members of the client-action state enums took the comma without that reason, leaving a three-colons-and-one-comma list in each. Co-Authored-By: Claude <noreply@anthropic.com>
Review: repo-wide em dash sweep (201 files, 2448 substituted lines)Verdict: approve. One nit found and fixed ( I did not re-verify the mechanical claims the orchestrator already checked Test baseline
1. Meaning changes: none foundI reconstructed every substitution by aligning The strongest single result: across all 2448 substituted lines, zero Shape distribution: Targeted probes for relationship reversal:
Runtime strings behave: 2. Anchor and link integrity: verified independently, no new breakageI wrote a standalone GitHub-slug checker (own slugger, does not reuse repo
3. The gate is not vacuum-vulnerable
The vacuum guard ( Two limits worth knowing, neither a defect: it scans 4. The widened gloss check genuinely pins the boundaries
5.
|
The sweep's own rule offers parentheses alongside the comma, and it already
uses them. Twenty spans that an em dash pair had set off, and whose inner
text already carried its own comma, took a comma at each end instead. That
flattens a nested aside into a flat list, and in a few places into a comma
splice:
"contradicts an Active LLP, a Spec's must, a Decision's chosen option,
a Principle's always/never, call it out"
"deferred, consent, conflict, opt-out, and is the sibling of"
"LLP documents are living, you edit, supersede, and delete freely, so"
Parentheses restore the boundary the pair carried. The transform is
character-neutral (two ", " become " (" and ") "), so nothing rewraps, and
it still changes no word: the diff against master remains 2506 aligned
pairs with zero letter changes.
One paired span keeps its commas because it is already correct as a quoted
appositive ("The product intent, \"log in, logs flow\", is served by
neither"), and one sits inside parentheses already, so it takes a colon.
Co-Authored-By: Claude <noreply@anthropic.com>
Review round 2: repo-wide em dash sweepVerdict: approve. One finding, fixed and pushed ( Round 1 ( Round 1's fix landed and is inert
Master moved: no new em dashes, no conflict
The merge is clean (auto-merged 9 files, no conflicts), re-verified after my Gate and hygiene on the current head
Spot-checks (weighted to
|
| file:line | before the fix |
|---|---|
.claude/skills/llp-grill/SKILL.md:35 |
contradicts an Active LLP, a Spec's "must", a Decision's chosen option, a Principle's "always/never", call it out |
llp/0044-client-attach-on-join.decision.md:24 |
deferred, consent, conflict, opt-out, and is the sibling of |
llp/0025-remote-config-join-flow.spec.md:18 |
MDM deploys a **seed**, server URL + policy token, nothing else, and the gateway joins the fleet |
.claude/skills/llp-grill/SKILL.md:57 |
LLP documents are *living*, you edit, supersede, and delete freely, so (comma splice) |
llp/0041-central-config-client-actions.design.md:8 |
... can re-fire when its input drifts, attach re-attaches on a gateway rebind (comma splice) |
hypaware-core/plugins-workspace/{claude,codex}/src/config.js:6 |
the \backfill` sub-object that drives backfill-on-join, `{ on_join, window_days }`, and the optional `attach` sub-object` (reads as a third thing backfill drives) |
Full set also: DECISION-DISPOSITION.md:40, llp/0003:60, llp/0042:47,
llp/0049:38, llp/0054:19, llp/0063:274, llp/0063:477, llp/0071:117,
llp/0073:259, {claude,codex}/skills/hypaware-ai-usage-report/SKILL.md:273,
{claude,codex}/skills/hypaware-report-to-html/SKILL.md:42/:38.
This is not a new rule, it is the PR's own rule finished. CLAUDE.md offers
"a comma, colon, parentheses, or a sentence split", and the sweep already
reaches for parentheses in 12 places. Parentheses restore exactly what the pair
carried.
The fix is character-neutral (two , become ( and ) ), so nothing
rewraps: 23 insertions, 23 deletions, and the letters-unchanged proof still
returns zero for the sweep. Two of the 20 spanned a line break, and
parenthesising them restored the previous line to master's byte-identical text,
which is why the aligned-pair count drops from 2506 to 2504.
Two paired spans deliberately keep their commas:
llp/0063-login-auto-provision-forward-sink.decision.md:86-
The product intent, "log in, logs flow", is served by neither. A quoted
appositive with commas is already correct.llp/0041:8sits inside parentheses already, so nesting is not available;
the comma splice there takes a colon instead.
I did not touch single em dashes that introduced an elaboration and became a
comma or colon. Those are one-sided, nothing needs bracketing, and they read
correctly.
Non-blocking observations
.github/workflows/llp-check.ymlsays "Generated byneutral init". If that
generator's template still holds em dashes, re-running it reintroduces them,
but the gate this PR adds catches that on the nextnpm test, so it is
self-correcting rather than a leak.- The gate scans
git ls-files, so an untracked file is invisible, and
CLAUDE.mdis a symlink toAGENTS.mdso that content is scanned twice.
Both were noted in round 1; neither is a defect.
After the fix
npm test: 3081 pass, the same 8 known leave failures, no new ones. Gate 2/2,
hygiene 12/12, merge with origin/master still clean.
neutral: TRIAGE rung (LLP 0017) — PR #525Two review rounds are exhausted (round 1 Verdict: every residual is non-blocking. This PR touches no runtime code
Filed as a tracked follow-up rather than dropped: #532. Merge-ordering note, restated (not re-derived — the orchestrator already |
# Conflicts: # hypaware-core/plugins-workspace/openclaw/src/settings.js # llp/0148-shadow-wire-parity-in-plugin.decision.md # src/core/commands/central.js # src/core/config/client_detach_disk.js # src/core/daemon/status.js
neutral: TRIAGE rung (LLP 0017) - re-triage at head
|
What
The repo-wide em dash sweep the issue asks for, plus the test that stops it
coming back, plus the
@refgloss gate widening folded in per the maintainercomment.
Violation count: 2438 lines across 196 files before, 0 after.
The gate is the point
test/core/house-style-em-dash.test.jsis new. It scans every tracked,non-binary file for U+2014 and fails with the offending
file:linelist. Runboth ways on this branch:
The second test is the guard against a scan that silently matches nothing: it
asserts the tree was actually walked and that the file stating the rule is in
scope. The scan reads bytes and skips anything containing a NUL rather than
filtering by extension, so a new file type is covered the day it lands.
Scope, and the one thing deliberately excluded
In scope, per CLAUDE.md and the maintainer note on the issue: code, comments,
JSDoc, strings, docs, in-repo skills, and the whole LLP corpus including
llp/tombstones/.Excluded:
notes-archive/(2 files, 38 lines). It holds dated, signedreview transcripts of LLP drafts. They are records of what a reviewer wrote on
a given day, not project prose under maintenance, so re-punctuating them would
falsify the record rather than fix a style defect. The exemption is a commented
prefix list in the test, so adding another is a visible diff with an argument
attached.
How the replacement was chosen
Not one mark for all 2560 occurrences. Per site:
**Term** \u2014 gloss,## Heading \u2014 gloss,- [LLP 0025](...) \u2014 join flow) and before a clause that explains the onebefore it;
\u2014 so,\u2014 but,\u2014 not), and commas on bothsides of an aside that used a pair of dashes as brackets (never parentheses,
so nothing can end up unbalanced);
parenthesis already holds;
-in runtime strings, CSS declarations, JSONdescriptionfields,shell/HTML samples, and table cells where a lone dash was a "none"
placeholder;
ends up with two colons doing different jobs.
Nineteen LLP titles and the five
## Part Nheadings in LLP 0045 were thenrepunctuated by hand where the generated mark read badly
(
LLP 0040: Incremental sink reads (design),## Part 3. Reverse runs from disk: ...), and twenty bold labels were normalized to the colon their siblingsuse.
90 heading anchors changed slug as a result. All 34 of their call sites are
repointed,
@refannotations and in-repo anchor links alike. The three brokenanchor links that exist on
masterare the same three after this change: nonew ones.
No rationale is reworded and no text is reflowed. Every file keeps its exact
line count, verified:
git diff --numstatshows added == deleted for all 198swept files.
Gate widening (folded in from the issue's last section)
test/core/llp-ref-hygiene.test.jsonly ever read the line the@reftokensat on, so an em dash in a continuation line of a multi-line gloss was
invisible to it. The check now reads the whole gloss: the
@refline plus thewrapped lines under it, stopping at a blank line, a new
@tag, a secondannotation, the end of the comment, or (in Markdown) the next list item or
heading. A new fixture test pins where a gloss starts and stops, including the
exact regression: the character present in
.glossand absent from.text.The three known continuation-line instances (
format-iceberg/src/table-format.js,llp/0067-session-opt-out.design.md,src/core/runtime/activation.js) arefixed by the sweep and now covered by the gate.
One assertion needed care:
usage-policy-classification.test.jsasserts theconsent copy contains no em dash. Swept naively it would have asserted the copy
contains no hyphen and failed. It spells the character as a
\u2014escapenow, which the repo-wide gate does not match, so the assertion survives the gate
that backs it.
Verification
npm test: 3081 pass, 8 fail. The 8 areleave-command.test.js, failingidentically on
origin/masterwith these changes stashed. No regression.npm run typecheck: clean.npm run smoke -- core_boot_noop: ok.Fixes #472