From a3707dcd3c5b623def4775d1c72e60ddb5f0bd5e Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:52:22 -0400 Subject: [PATCH 1/2] fix(docs-hygiene): hyphen-exclude Form 1; allow inline comments on declarations Two P1 findings raised on #1386 after it was already merged. Same review round, so they never reached main. Form 1 rewrote unrelated slash commands. `\b` treats a hyphen as a word boundary, so `\B/\b` prevented `/confirm` matching in `/confirmation` but NOT in `/confirm-changes` -- renaming `context` matched the unrelated `/context-guard`. Form 1 is Certain and sits on container mode's Certain allowlist, so that went through the default auto-apply path and renamed another command. Slash-command and container names are kebab-case, so this fires constantly rather than rarely. Now uses the consumed `([^\w-]|$)` terminator, the shape Forms 13 and 15 already use for the same reason. Verified against this tree that namespaced invocations are unaffected -- a colon is a valid terminator, so the nine `/docs-hygiene:` references still match -- while `/context-guard`, `/contextual` and `path/context` do not. Form 1 joins the other consume-the-delimiter forms in the survey's rescan-cursor rule, and its consumed character is not part of the reference. Manifest declarations could not carry an inline comment. `name: # package name` and `name = "" # package name` are ordinary self-documenting manifests, and the end-anchored alternatives rejected the whole line -- while filesystem evidence still selected container mode, so the registration went unmatched and was suppressed as residue while apply mode reported completion. The whitespace rule differs between the two on purpose. YAML starts a comment only when `#` follows whitespace, so `name: #x` is the single scalar `#x` and must NOT match; the YAML alternative therefore requires `(\s+#.*)?`. TOML's value is quoted, so the closing quote already ends the string unambiguously and `\s*(#.*)?` is safe. JSON is excluded entirely, having no comment syntax. All six positive shapes and both negatives verified. Phase 6 reconciliation grep on Form 1's boundary found it restated in four more places -- triage.md's Certain criteria, audit-modes.md's Orphans sweep, apply.md's word-boundary-trap gotcha, and audit.md's consume-the-delimiter form list -- all corrected here. Gates: validate-plugin-contracts (43 setup skills, 2122 files), claude plugin validate, markdownlint-cli2 0 errors over 36 files, changelog parity --check-bump, evals parse at 46 cases with no duplicate ids. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ATCcexm8GPTaNntu2yrGMk --- .../docs-hygiene/.claude-plugin/plugin.json | 2 +- plugins/docs-hygiene/CHANGELOG.md | 18 ++++++++++ .../skills/rename-references/context/apply.md | 4 ++- .../rename-references/context/audit-modes.md | 2 +- .../skills/rename-references/context/audit.md | 2 +- .../rename-references/context/patterns.md | 35 ++++++++++++++++--- .../rename-references/context/triage.md | 2 +- .../skills/rename-references/evals/evals.json | 26 ++++++++++++++ 8 files changed, 81 insertions(+), 10 deletions(-) diff --git a/plugins/docs-hygiene/.claude-plugin/plugin.json b/plugins/docs-hygiene/.claude-plugin/plugin.json index 400c23adc..5ab832cfb 100644 --- a/plugins/docs-hygiene/.claude-plugin/plugin.json +++ b/plugins/docs-hygiene/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "docs-hygiene", - "version": "0.9.0", + "version": "0.9.1", "description": "Documentation-hygiene toolkit of six skills: compress (flavor-trim markdown with a semantic-diff safety net), audit-noise (classify markdown noise), extract-ssot (deduplicate repeated content into a single source of truth), audit-encapsulation (detect citations into skill-private surfaces), rename-references (sweep stale references after renames), and audit-derivability (classify whether a whole document earns its existence — could a fresh agent re-derive it from the code?).", "author": { "name": "Melodic Software", diff --git a/plugins/docs-hygiene/CHANGELOG.md b/plugins/docs-hygiene/CHANGELOG.md index fe76d8388..75bb2ab06 100644 --- a/plugins/docs-hygiene/CHANGELOG.md +++ b/plugins/docs-hygiene/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog — docs-hygiene plugin +## [0.9.1] + +### Fixed + +- **Form 1's trailing boundary excludes a hyphen.** `\b` treats a hyphen as a word boundary, so + `\B/\b` matched `/context-guard` when renaming `context` — and Form 1 is Certain and sits + on container mode's Certain allowlist, so an unrelated command went through the default + auto-apply path and was rewritten. Slash-command and container names are kebab-case, so this + fires constantly rather than rarely. Now uses the same consumed `([^\w-]|$)` terminator as + Forms 13 and 15; a namespaced `/:sub` still matches, a colon being a valid terminator. +- **Manifest declarations may carry an inline comment.** `name: # package name` and + `name = "" # package name` are ordinary self-documenting manifests, and the end-anchored + declaration alternatives rejected the whole line — while filesystem evidence still selected + container mode, so the registration went unmatched and was suppressed as residue while apply + mode reported completion. The YAML form requires whitespace before `#`, since YAML starts a + comment only after whitespace and `name: #x` is a single scalar; TOML allows optional + whitespace, its value being quoted. JSON is excluded, having no comment syntax. + ## [0.9.0] ### Fixed diff --git a/plugins/docs-hygiene/skills/rename-references/context/apply.md b/plugins/docs-hygiene/skills/rename-references/context/apply.md index 951dd07ca..7c683350b 100644 --- a/plugins/docs-hygiene/skills/rename-references/context/apply.md +++ b/plugins/docs-hygiene/skills/rename-references/context/apply.md @@ -206,7 +206,9 @@ Edit tool's read-before-write guard catches files modified by another session. I ### Word-boundary trap -Bare-token Form 2 uses `\b\b` — `confirm` does NOT match in `confirmation`. Slash-token Form 1 uses `\B/\b` — `/confirm` matches but not `path/confirm` (slash is path separator, not skill prefix). +Bare-token Form 2 uses `\b\b` — `confirm` does NOT match in `confirmation`. Slash-token Form 1 uses `\B/([^\w-]|$)` — `/confirm` matches but not `path/confirm` (slash is path separator, not skill prefix). + +**A word boundary is NOT enough on the trailing side.** `\b` treats a hyphen as a boundary, so `\B/\b` matched `/confirm-changes` as well as `/confirm` — and Form 1 auto-applies. Slash-command and container names are kebab-case, so this fires constantly in practice; the consumed `([^\w-]|$)` terminator is what rules it out. Forms 3, 13, 14 and 15 exclude an adjacent hyphen for exactly the same reason. ### Frontmatter multi-line diff --git a/plugins/docs-hygiene/skills/rename-references/context/audit-modes.md b/plugins/docs-hygiene/skills/rename-references/context/audit-modes.md index d92e9f9c5..a676b4258 100644 --- a/plugins/docs-hygiene/skills/rename-references/context/audit-modes.md +++ b/plugins/docs-hygiene/skills/rename-references/context/audit-modes.md @@ -106,7 +106,7 @@ Files containing BOTH (incomplete rename state): **Algorithm:** -1. Sweep for `` references using Form 1 (slash-token `\B/\b`) and Form 3 (path `context/.md`, `skills//`, `plugins/`) from [patterns.md](patterns.md) +1. Sweep for `` references using Form 1 (slash-token `\B/([^\w-]|$)`) and Form 3 (path `context/.md`, `skills//`, `plugins/`) from [patterns.md](patterns.md) 2. For each match, classify: - **Orphan (broken):** path-form match where path does not exist on disk after rename. Verify via Glob/Read. E.g. `[text](context/old.md)` matched but `context/old.md` was renamed to `context/new.md` — link now broken - **Slash-token orphan:** `/` matched but no skill/command named `` exists any more (skill renamed/removed) diff --git a/plugins/docs-hygiene/skills/rename-references/context/audit.md b/plugins/docs-hygiene/skills/rename-references/context/audit.md index 7733920ff..f8f3d7cd4 100644 --- a/plugins/docs-hygiene/skills/rename-references/context/audit.md +++ b/plugins/docs-hygiene/skills/rename-references/context/audit.md @@ -72,7 +72,7 @@ falsely complete. **Then advance the rescan cursor to the end of the LAST enumerated `` span, not the end of the match.** Several forms deliberately CONSUME a trailing delimiter instead of using a lookahead, because -ripgrep's default engine rejects look-around — Forms 3, 13, 15 and both delimiter-anchored Form 14 +ripgrep's default engine rejects look-around — Forms 1, 3, 13, 15 and both delimiter-anchored Form 14 alternatives all do. That consumed delimiter is frequently the LEADING delimiter the next occurrence needs, so a rescan that resumes after the whole match eats the boundary and emits only the first of two adjacent references. Verified: on `{"name":"","id":""}` a global diff --git a/plugins/docs-hygiene/skills/rename-references/context/patterns.md b/plugins/docs-hygiene/skills/rename-references/context/patterns.md index 683c400f2..3a64d0f71 100644 --- a/plugins/docs-hygiene/skills/rename-references/context/patterns.md +++ b/plugins/docs-hygiene/skills/rename-references/context/patterns.md @@ -17,14 +17,25 @@ Substitute `` with the actual old token. Anchor patterns with word boundari ## Form 1: Slash-prefixed token (skill name) ```regex -\B/\b +\B/([^\w-]|$) ``` - **Triage default:** Certain - **Catches:** `/confirm`, `/test live`, `/` references in prose, tables, and frontmatter - **Why `\B/`:** word-boundary after slash would match `path/confirm` where slash is a path separator; non-word-boundary before slash means "the slash is not preceded by a word char," which excludes path contexts -- **Why `\b` after:** prevents `/confirm` matching in `/confirmation` -- **False-positives:** none typical — slash + identifier + word-boundary is high-precision +- **Trailing boundary excludes a hyphen, and that is load-bearing on a Certain form.** A bare `\b` + prevents `/confirm` matching in `/confirmation` but NOT in `/confirm-changes`, because `\b` + treats a hyphen as a word boundary. Slash-command and container names are kebab-case, so + renaming `context` matched the unrelated `/context-guard` and — Form 1 being on container + mode's Certain allowlist — auto-applied, rewriting another command's name. The consumed + `([^\w-]|$)` terminator fixes both cases at once, the same shape Forms 13 and 15 use for the + same reason. Verified: `/context`, `/context:sub` (a namespaced invocation, `:` is a valid + terminator) and `see /context.` match; `/context-guard`, `/contextual` and `path/context` do + not. +- **The consumed terminator is not part of the reference** — replace only the `/` span and + leave it in place, and note that this form now participates in the survey's cursor rule + (`audit.md` Phase 2) like the other consume-the-delimiter forms. +- **False-positives:** none typical — slash + identifier + a non-hyphen boundary is high-precision ## Form 2: Bare token with word boundary @@ -284,9 +295,9 @@ existing "Frozen historical records" rule already excludes. See `triage.md` ```regex ^#{1,6}\s+`?`?\s*(#+\s*)?$ ^`?`?\s*$\n^(=+|-+)\s*$ -^\s*(name|title|id):\s*(""|''|)\s*$ +^\s*(name|title|id):\s*(""|''|)(\s+#.*)?\s*$ (^|[{,])\s*"(name|title|id)"\s*:\s*""\s*(,|}|$) -^\s*"?(name|title|id)"?\s*=\s*(""|'')\s*$ +^\s*"?(name|title|id)"?\s*=\s*(""|'')\s*(#.*)?$ (^|[{,])\s*(""|)\s*:\s*[{\[] ``` @@ -322,6 +333,20 @@ existing "Frozen historical records" rule already excludes. See `triage.md` `{"description":""}` and a prose line quoting `"name": ""` mid-sentence do not. Precision on this repository is unchanged by the widening: still exactly `.claude-plugin/marketplace.json:192` and `plugins/docs-hygiene/.claude-plugin/plugin.json:3`. +- **A declaration may carry an INLINE COMMENT, and the end anchor must let it through.** Both + YAML and TOML let a manifest document its own fields — `name: # package name`, + `name = "" # package name` — and an end-anchored alternative rejected the whole line. + Filesystem evidence still selects container mode for such a manifest, so the registration went + unmatched and was suppressed as residue while apply mode reported completion. + **The whitespace before `#` is not decoration in the YAML case.** YAML starts a comment only + when `#` follows whitespace; `name: #x` is the single scalar `#x`, NOT `` plus a + comment. The YAML alternative therefore requires `(\s+#.*)?` — at least one space — while the + TOML one accepts `\s*(#.*)?` because its value is quoted, so the closing quote already ends the + string unambiguously. Verified: `name: # package name`, `name: "" # c`, + an indented `id: ` with a spaced trailing comment, `name = "" # package name` and + `name = ""#c` all match; + `name: #x` and `name: -extra # c` do not. JSON is excluded from this because JSON has + no comment syntax. - **The YAML and TOML alternatives keep their `$` anchor deliberately.** Both grammars are line-oriented for the shapes manifests actually use — block mappings and top-level key/value pairs — so the end-of-line anchor is a real discriminator there rather than an accident of diff --git a/plugins/docs-hygiene/skills/rename-references/context/triage.md b/plugins/docs-hygiene/skills/rename-references/context/triage.md index e554d79e5..fe029214e 100644 --- a/plugins/docs-hygiene/skills/rename-references/context/triage.md +++ b/plugins/docs-hygiene/skills/rename-references/context/triage.md @@ -8,7 +8,7 @@ Match patterns where the rename intent is unambiguous regardless of surrounding **Bucket criteria:** -- Form 1: slash-prefixed token (`\B/\b`) — slash-tokens are skill names by convention; token in `` position is virtually never an English word with a leading slash +- Form 1: slash-prefixed token (`\B/([^\w-]|$)`) — slash-tokens are skill names by convention; token in `` position is virtually never an English word with a leading slash. The trailing class excludes a hyphen, so `/context` does not match the unrelated `/context-guard`; a bare `\b` would, and this bucket auto-applies - Form 3: path references (`context/.md`, `skills//`, and a container-root segment ending in the token, `plugins/`) — paths are inherently specific - Form 8: frontmatter glob set (`{a,b,,c}`) — brace enumeration is a glob construct, not English prose. **Identifier mode only:** under container-rename mode Form 8 falls outside the Certain-eligibility allowlist and demotes to Ambiguous, as does Form 12 — a glob set and a dotted key both prove the token is an IDENTIFIER, which is not what a container rename is asking (`patterns.md` "Phase 0b") diff --git a/plugins/docs-hygiene/skills/rename-references/evals/evals.json b/plugins/docs-hygiene/skills/rename-references/evals/evals.json index b23d3b96a..2445c4e37 100644 --- a/plugins/docs-hygiene/skills/rename-references/evals/evals.json +++ b/plugins/docs-hygiene/skills/rename-references/evals/evals.json @@ -545,6 +545,32 @@ "Skips and container-mode residue are reported on separate lines", "The zero-stragglers line is explained as true but insufficient on its own" ] + }, + { + "id": 45, + "name": "slash-token-does-not-match-hyphenated-command", + "prompt": "Rename the `context` plugin to `context-tools`. The repo documents an unrelated `/context-guard` command. Is it safe to auto-apply the Certain bucket?", + "expected_output": "Yes, now that Form 1's trailing boundary excludes a hyphen. A bare `\\b` treats a hyphen as a word boundary, so `\\B/\\b` matched `/context-guard` as a `context` reference -- and Form 1 is on container mode's Certain allowlist, so the default auto-apply path would have rewritten another command's name to `/context-tools-guard`. The consumed `([^\\w-]|$)` terminator rules it out, the same shape Forms 13 and 15 use for the same reason. Discrimination is otherwise unchanged: `/context`, the namespaced `/context:sub` (a colon is a valid terminator) and `see /context.` still match, while `/context-guard`, `/contextual` and `path/context` do not. The consumed character is not part of the reference and is left in place, and Form 1 now participates in the survey's rescan-cursor rule like the other consume-the-delimiter forms.", + "files": [], + "expectations": [ + "`/context-guard` is not matched as a `context` reference", + "A namespaced `/context:sub` invocation still matches", + "The danger is tied to Form 1 being auto-applied rather than merely reported", + "The consumed terminator is not rewritten as part of the reference" + ] + }, + { + "id": 46, + "name": "manifest-declaration-with-inline-comment-is-matched", + "prompt": "Rename `docs-hygiene` to `doc-care`. The manifest documents its own field: name: docs-hygiene # package name. And the TOML one reads name = \"docs-hygiene\" # package name.", + "expected_output": "Both match. The declaration alternatives accept a trailing comment, because an end anchor otherwise rejected the whole line while filesystem evidence still selected container mode for the manifest -- leaving the registration unmatched and suppressed as residue while apply mode reported completion. The whitespace before the YAML `#` is required rather than decorative: YAML starts a comment only when `#` follows whitespace, so `name: docs-hygiene#x` is the single scalar `docs-hygiene#x` and must NOT match. The TOML alternative allows optional whitespace instead, because its value is quoted and the closing quote already ends the string unambiguously. JSON is excluded from this entirely, having no comment syntax.", + "files": [], + "expectations": [ + "A YAML declaration with a trailing comment is matched", + "A TOML declaration with a trailing comment is matched", + "`name: #x` is NOT matched, because YAML requires whitespace before a comment", + "JSON is excluded, with the absence of comment syntax given as the reason" + ] } ] } From 6741244178691ba886c710a9f0e8ad54c172da53 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:02:49 -0400 Subject: [PATCH 2/2] fix(docs-hygiene): align SKILL.md's slash-token gotcha; bound enumeration by region Two findings on this PR, both consequences of the two fixes it carries. SKILL.md's slash-token gotcha still prescribed `\B/\b` -- the exact expression Form 1 was corrected away from. SKILL.md is ALWAYS loaded, so a stale executable directive there outranks the corrected pattern in practice: an agent following the gotcha reintroduces the hyphenated-sibling defect while patterns.md claims it fixed. Now states the consumed terminator, why a trailing `\b` is insufficient, and that the consumed character is not part of the reference. The reconciliation grep should have caught it last round. It did run over Form 1's boundary, but its output exceeded the display budget and was truncated to a file, and the narrower follow-up greps used a hand-escaped `\B` that matched nothing. This round's grep matched the literal regex text with no escaping guess and found every site. Accepting inline comments widened the match to span text that is documentation rather than declaration -- and a comment routinely mentions the thing it documents. `name: # before publishing` is one declaration and one piece of prose, but the survey enumerates every `` span inside a match, so it emitted two records, both attributed to Form 14. That form is Certain and exempt from the common-word demotion inside a manifest, so apply mode rewrote the comment text. Enumeration is now bounded by a per-form REFERENCE REGION rather than the whole match, with the regions tabulated in audit.md: the declaration alternatives enumerate within the VALUE only; Form 7 keeps the whole quoted field, because its multiple occurrences are all genuine references; the delimiter-anchored forms enumerate the token span they anchor, excluding the consumed delimiter. Narrowing blanket-wide would have reintroduced the wide- match bug those regions were split to fix. Gates: validate-plugin-contracts (43 setup skills, 2122 files), claude plugin validate, markdownlint-cli2 0 errors over 36 files, changelog parity --check-bump, evals parse at 48 cases with no duplicate ids. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ATCcexm8GPTaNntu2yrGMk --- plugins/docs-hygiene/CHANGELOG.md | 10 +++++++ .../skills/rename-references/SKILL.md | 2 +- .../skills/rename-references/context/audit.md | 26 ++++++++++++++---- .../rename-references/context/patterns.md | 9 +++++++ .../skills/rename-references/evals/evals.json | 27 ++++++++++++++++++- 5 files changed, 67 insertions(+), 7 deletions(-) diff --git a/plugins/docs-hygiene/CHANGELOG.md b/plugins/docs-hygiene/CHANGELOG.md index 75bb2ab06..b74b60d49 100644 --- a/plugins/docs-hygiene/CHANGELOG.md +++ b/plugins/docs-hygiene/CHANGELOG.md @@ -10,6 +10,16 @@ auto-apply path and was rewritten. Slash-command and container names are kebab-case, so this fires constantly rather than rarely. Now uses the same consumed `([^\w-]|$)` terminator as Forms 13 and 15; a namespaced `/:sub` still matches, a colon being a valid terminator. +- **`SKILL.md`'s slash-token gotcha states Form 1's corrected expression.** It still prescribed + `\B/\b` — the exact defect above — and `SKILL.md` is always loaded, so an agent following + the gotcha would reintroduce it while `patterns.md` claimed it fixed. +- **Occurrence enumeration is bounded by a per-form REFERENCE REGION.** The survey emits a record + for every `` span inside a match; once the declaration alternatives accepted a trailing + comment, a comment that mentions the thing it documents (`name: # before + publishing`) produced a second record attributed to Form 14 — Certain, and exempt from the + common-word demotion inside a manifest — so apply mode rewrote the prose. The region for those + alternatives is the declaration VALUE; Form 7's stays the whole quoted field, whose occurrences + are all genuine references. - **Manifest declarations may carry an inline comment.** `name: # package name` and `name = "" # package name` are ordinary self-documenting manifests, and the end-anchored declaration alternatives rejected the whole line — while filesystem evidence still selected diff --git a/plugins/docs-hygiene/skills/rename-references/SKILL.md b/plugins/docs-hygiene/skills/rename-references/SKILL.md index 1d4788f55..e18e43ddd 100644 --- a/plugins/docs-hygiene/skills/rename-references/SKILL.md +++ b/plugins/docs-hygiene/skills/rename-references/SKILL.md @@ -131,7 +131,7 @@ Paths skipped from sweeps automatically: - **Self-reference in plan docs** — the active plan/work-notes document mentions the rename pair as part of *documenting* the migration. Auto-excluded; never modify. - **Concurrent sessions** — Edit tool's read-before-write guard catches racing edits. If guard fails, report to user and abort. - **Word-boundary trap** — `confirm` inside `confirmation` MUST NOT match. All bare-token patterns use `\b`. -- **Slash-token specificity** — `/confirm` should match but not `/confirmation` or `path/confirm`. Use `\B/\b` (non-word-boundary before slash, word-boundary after). +- **Slash-token specificity** — `/confirm` should match but not `/confirmation`, `/confirm-changes`, or `path/confirm`. Use `\B/([^\w-]|$)` (non-word-boundary before the slash; a CONSUMED terminator after that excludes a hyphen). A trailing `\b` is not enough: it treats a hyphen as a boundary, so it matches inside kebab-case sibling commands — and Form 1 is Certain, so that auto-applies. The consumed character is not part of the reference. - **Frontmatter trailing newline** — YAML frontmatter description strings can span lines. Patterns must handle multi-line. Use `multiline: true` on the Grep tool. - **Renames with overlap** — `test` → `test e2e` is a substring expansion. Apply most-specific match first, mark already-edited regions to prevent double-edit. - **Pattern false negative** — if Phase 6 re-sweep reveals a NEW syntactic form, that's a feature gap. Add the pattern to `context/patterns.md`, re-iterate. Do NOT silently apply. diff --git a/plugins/docs-hygiene/skills/rename-references/context/audit.md b/plugins/docs-hygiene/skills/rename-references/context/audit.md index f8f3d7cd4..756a06eeb 100644 --- a/plugins/docs-hygiene/skills/rename-references/context/audit.md +++ b/plugins/docs-hygiene/skills/rename-references/context/audit.md @@ -63,11 +63,27 @@ matching.** Some forms match a span far wider than the token: Form 7's captured group to just ONE occurrence. On `description: "first and then "` the pattern yields a single match for two references, and no amount of cursor advancing recovers the other — re-matching from inside the field cannot reproduce the `description:` prefix the pattern requires. -So for each match, scan its text for every occurrence of `` and emit one record per -occurrence, all attributed to the matching form. The whole-pattern match establishes THAT the form -applies and to what extent; the token spans inside it are the references. Under container mode a -lost occurrence becomes suppressed Form 2 residue, so this drops silently and the rename can -falsely complete. +So for each match, scan its REFERENCE REGION for every occurrence of `` and emit one record +per occurrence, all attributed to the matching form. The whole-pattern match establishes THAT the +form applies and to what extent; the token spans inside its reference region are the references. +Under container mode a lost occurrence becomes suppressed Form 2 residue, so this drops silently +and the rename can falsely complete. + +**The reference region is the whole match ONLY when the whole match is reference-bearing.** +Enumerating blindly is as wrong as enumerating too little, and in the more dangerous direction — +these forms are Certain, so a spurious record auto-applies. Each form's region: + +| Form | Reference region | Excluded from enumeration | +|---|---|---| +| 7 (frontmatter chain) | the whole quoted field | — the field's occurrences are all real references | +| 14 declaration alternatives | the declaration VALUE only | the trailing inline comment | +| 1, 3, 13, 15, Form 14 key-position | the token span the alternative anchors | the consumed delimiter and surrounding syntax | + +The comment case is the live hazard: `name: # before publishing` is one declaration +and one piece of ordinary prose. Enumerating the whole match emits two records, both attributed to +Form 14 — which is Certain and exempt from the common-word demotion in a manifest — so apply mode +rewrites the comment text too. The TOML alternative has the identical shape and the identical +region. A comment is documentation ABOUT the declaration, never a second declaration. **Then advance the rescan cursor to the end of the LAST enumerated `` span, not the end of the match.** diff --git a/plugins/docs-hygiene/skills/rename-references/context/patterns.md b/plugins/docs-hygiene/skills/rename-references/context/patterns.md index 3a64d0f71..91e302dd3 100644 --- a/plugins/docs-hygiene/skills/rename-references/context/patterns.md +++ b/plugins/docs-hygiene/skills/rename-references/context/patterns.md @@ -347,6 +347,15 @@ existing "Frozen historical records" rule already excludes. See `triage.md` `name = ""#c` all match; `name: #x` and `name: -extra # c` do not. JSON is excluded from this because JSON has no comment syntax. +- **The comment is INSIDE the match but OUTSIDE the reference region.** Widening the anchor means + the match now spans text that is documentation rather than declaration, and a comment routinely + mentions the thing it documents: `name: # before publishing`. The survey enumerates + every `` span inside a match (`audit.md` Phase 2), and these alternatives are Certain and + exempt from the common-word demotion inside a manifest — so blind enumeration would emit a + second record for the prose and apply mode would rewrite it. **Enumerate only within the + declaration VALUE for these two alternatives**; the comment is documentation ABOUT the + declaration, never a second declaration. `audit.md` carries the per-form reference-region table + this belongs to. - **The YAML and TOML alternatives keep their `$` anchor deliberately.** Both grammars are line-oriented for the shapes manifests actually use — block mappings and top-level key/value pairs — so the end-of-line anchor is a real discriminator there rather than an accident of diff --git a/plugins/docs-hygiene/skills/rename-references/evals/evals.json b/plugins/docs-hygiene/skills/rename-references/evals/evals.json index 2445c4e37..21ed3d9a1 100644 --- a/plugins/docs-hygiene/skills/rename-references/evals/evals.json +++ b/plugins/docs-hygiene/skills/rename-references/evals/evals.json @@ -513,7 +513,7 @@ "id": 42, "name": "wide-match-enumerates-every-token-span", "prompt": "Rename `docs-hygiene` to `doc-care`. A SKILL.md frontmatter field reads: description: \"first docs-hygiene and then docs-hygiene\". How many occurrence records does the survey emit?", - "expected_output": "Two. Form 7's pattern swallows the whole field and its greedy prefix binds the captured group to a single occurrence, so the whole-pattern match is ONE for two references -- and no cursor advance recovers the other, because re-matching from inside the field cannot reproduce the `description:` prefix the pattern requires. The survey therefore scans each match's text for every occurrence of the token and emits one record per occurrence, all attributed to the matching form: the whole-pattern match establishes THAT the form applies and over what extent, the token spans inside it are the references. Relying on repeated whole-pattern matching loses the earlier occurrence, and under container mode it becomes suppressed Form-2 residue, so the drop is silent and the rename can falsely complete.", + "expected_output": "Two. Form 7's pattern swallows the whole field and its greedy prefix binds the captured group to a single occurrence, so the whole-pattern match is ONE for two references -- and no cursor advance recovers the other, because re-matching from inside the field cannot reproduce the `description:` prefix the pattern requires. The survey therefore scans each match's REFERENCE REGION for every occurrence of the token and emits one record per occurrence, all attributed to the matching form: the whole-pattern match establishes THAT the form applies and over what extent, the token spans inside its reference region are the references. For Form 7 that region is the whole quoted field, since its occurrences are all real references; other forms narrow it, so enumeration never reaches text the match spans but does not refer through. Relying on repeated whole-pattern matching loses the earlier occurrence, and under container mode it becomes suppressed Form-2 residue, so the drop is silent and the rename can falsely complete.", "files": [], "expectations": [ "Both occurrences inside the single wide match produce records", @@ -571,6 +571,31 @@ "`name: #x` is NOT matched, because YAML requires whitespace before a comment", "JSON is excluded, with the absence of comment syntax given as the reason" ] + }, + { + "id": 47, + "name": "inline-comment-text-is-not-rewritten", + "prompt": "Rename the `test` plugin to `test-suite`. A manifest line reads: name: test # test before publishing. What gets rewritten?", + "expected_output": "Only the declaration value. The widened alternative makes the match span the comment as well, and the survey enumerates every token span inside a match -- so blind enumeration would emit TWO records, both attributed to Form 14, which is Certain and exempt from the common-word demotion inside a manifest, and apply mode would rewrite the ordinary prose to `test-suite before publishing`. Enumeration is therefore bounded to each form's REFERENCE REGION, and for the YAML and TOML declaration alternatives that region is the declaration VALUE, not the trailing comment: a comment is documentation ABOUT the declaration, never a second declaration. Form 7's region is by contrast the whole quoted field, because its multiple occurrences are all genuine references -- the region is per form, not a blanket narrowing.", + "files": [], + "expectations": [ + "The comment text is not rewritten", + "The declaration value is still rewritten", + "Enumeration is bounded by a per-form reference region rather than the whole match", + "The TOML alternative is given the same region as the YAML one" + ] + }, + { + "id": 48, + "name": "skill-md-slash-token-directive-matches-form-1", + "prompt": "SKILL.md's Gotchas tell me to use a particular regex for slash-token specificity. Does it still match what patterns.md defines for Form 1?", + "expected_output": "Yes -- both state `\\B/([^\\w-]|$)`. The gotcha previously said `\\B/\\b`, which is the bug Form 1 was fixed for: a trailing word boundary treats a hyphen as a boundary, so it matches inside kebab-case sibling commands like `/confirm-changes`, and Form 1 is Certain so that auto-applies. SKILL.md is always loaded, so a stale executable directive there outranks the corrected pattern in practice -- an agent following the gotcha would reintroduce the defect while patterns.md claimed it was fixed. The gotcha now also records that the terminator is CONSUMED and is not part of the reference.", + "files": [], + "expectations": [ + "The SKILL.md gotcha and the patterns.md Form 1 definition state the same expression", + "`/confirm-changes` is named as what a trailing `\\b` wrongly matches", + "The always-loaded surface is identified as the reason a stale directive there is worse than elsewhere" + ] } ] }