Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/docs-hygiene/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
28 changes: 28 additions & 0 deletions plugins/docs-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# 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/<old>\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 `/<old>: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/<old>\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 `<old>` span inside a match; once the declaration alternatives accepted a trailing
comment, a comment that mentions the thing it documents (`name: <old> # <old> 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: <old> # package name` and
`name = "<old>" # 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: <old>#x` is a single scalar; TOML allows optional
whitespace, its value being quoted. JSON is excluded, having no comment syntax.

## [0.9.0]

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion plugins/docs-hygiene/skills/rename-references/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<old>\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/<old>([^\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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<old>\b` — `confirm` does NOT match in `confirmation`. Slash-token Form 1 uses `\B/<old>\b` — `/confirm` matches but not `path/confirm` (slash is path separator, not skill prefix).
Bare-token Form 2 uses `\b<old>\b` — `confirm` does NOT match in `confirmation`. Slash-token Form 1 uses `\B/<old>([^\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/<old>\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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Files containing BOTH (incomplete rename state):

**Algorithm:**

1. Sweep for `<old>` references using Form 1 (slash-token `\B/<old>\b`) and Form 3 (path `context/<old>.md`, `skills/<old>/`, `plugins/<old>`) from [patterns.md](patterns.md)
1. Sweep for `<old>` references using Form 1 (slash-token `\B/<old>([^\w-]|$)`) and Form 3 (path `context/<old>.md`, `skills/<old>/`, `plugins/<old>`) 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:** `/<old>` matched but no skill/command named `<old>` exists any more (skill renamed/removed)
Expand Down
28 changes: 22 additions & 6 deletions plugins/docs-hygiene/skills/rename-references/context/audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,32 @@ matching.** Some forms match a span far wider than the token: Form 7's
captured group to just ONE occurrence. On `description: "first <old> and then <old>"` 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 `<old>` 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 `<old>` 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: <old> # <old> 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 `<old>` 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":"<old>","id":"<old>"}` a global
Expand Down
44 changes: 39 additions & 5 deletions plugins/docs-hygiene/skills/rename-references/context/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,25 @@ Substitute `<old>` with the actual old token. Anchor patterns with word boundari
## Form 1: Slash-prefixed token (skill name)

```regex
\B/<old>\b
\B/<old>([^\w-]|$)
Comment thread
kyle-sexton marked this conversation as resolved.
```

- **Triage default:** Certain
- **Catches:** `/confirm`, `/test live`, `/<skill-name>` 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 `/<old>` 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

Expand Down Expand Up @@ -284,9 +295,9 @@ existing "Frozen historical records" rule already excludes. See `triage.md`
```regex
^#{1,6}\s+`?<old>`?\s*(#+\s*)?$
^`?<old>`?\s*$\n^(=+|-+)\s*$
^\s*(name|title|id):\s*("<old>"|'<old>'|<old>)\s*$
^\s*(name|title|id):\s*("<old>"|'<old>'|<old>)(\s+#.*)?\s*$
Comment thread
kyle-sexton marked this conversation as resolved.
(^|[{,])\s*"(name|title|id)"\s*:\s*"<old>"\s*(,|}|$)
^\s*"?(name|title|id)"?\s*=\s*("<old>"|'<old>')\s*$
^\s*"?(name|title|id)"?\s*=\s*("<old>"|'<old>')\s*(#.*)?$
(^|[{,])\s*("<old>"|<old>)\s*:\s*[{\[]
```

Expand Down Expand Up @@ -322,6 +333,29 @@ existing "Frozen historical records" rule already excludes. See `triage.md`
`{"description":"<old>"}` and a prose line quoting `"name": "<old>"` 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: <old> # package name`,
`name = "<old>" # 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: <old>#x` is the single scalar `<old>#x`, NOT `<old>` 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: <old> # package name`, `name: "<old>" # c`,
an indented `id: <old>` with a spaced trailing comment, `name = "<old>" # package name` and
`name = "<old>"#c` all match;
`name: <old>#x` and `name: <old>-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: <old> # <old> before publishing`. The survey enumerates
every `<old>` 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Match patterns where the rename intent is unambiguous regardless of surrounding

**Bucket criteria:**

- Form 1: slash-prefixed token (`\B/<old>\b`) — slash-tokens are skill names by convention; token in `<old>` position is virtually never an English word with a leading slash
- Form 1: slash-prefixed token (`\B/<old>([^\w-]|$)`) — slash-tokens are skill names by convention; token in `<old>` 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/<old>.md`, `skills/<old>/`, and a container-root segment
ending in the token, `plugins/<old>`) — paths are inherently specific
- Form 8: frontmatter glob set (`{a,b,<old>,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")
Expand Down
Loading
Loading