diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ecbce0337..e0d728f7d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -114,6 +114,12 @@ "source": "./plugins/boris", "category": "learning", "tags": ["knowledge", "skill", "claude-code", "workflow", "tips", "best-practices"] + }, + { + "name": "docs-hygiene", + "source": "./plugins/docs-hygiene", + "category": "documentation", + "tags": ["maintenance", "skill", "markdown", "hygiene", "deduplication", "compress", "audit"] } ] } diff --git a/README.md b/README.md index 1c2fc41ad..0f68de90c 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Browse and manage with `/plugin`. To refresh after updates: `/plugin marketplace | [`context7`](plugins/context7) | Skill | Looks up current library documentation, API references, and code examples via Context7 — a two-step resolve-then-query workflow over the `ctx7` CLI or the consumer's Context7 MCP server, plus an upstream drift-check `update` action. | | [`thariq-skills`](plugins/thariq-skills) | Skill | Ships Anthropic's internal skill-authoring playbook as an on-demand knowledge skill — 9 skill categories, 9 authoring tips (gotchas sections, progressive disclosure, description-as-trigger), and distribution guidance, with a vendored upstream baseline and drift-check update script. | | [`boris`](plugins/boris) | Skill | Ships Boris Cherny's Claude Code workflow tips (howborisusesclaudecode.com) as an on-demand knowledge skill — 107 tips across 95 sections routed through topic reference files, with a vendored upstream baseline and drift-check update script. | +| [`docs-hygiene`](plugins/docs-hygiene) | Skills | Documentation-hygiene toolkit of five skills: `/docs-hygiene:compress` (flavor-trim markdown behind a semantic-diff safety net), `/docs-hygiene:declutter` (classify markdown noise, read-only), `/docs-hygiene:extract-ssot` (deduplicate repeated content into a single source of truth), `/docs-hygiene:encapsulation-audit` (detect citations into skill-private surfaces), and `/docs-hygiene:rename-references` (sweep stale references after renames). | Install one: `/plugin install @melodic-software`. diff --git a/plugins/docs-hygiene/.claude-plugin/plugin.json b/plugins/docs-hygiene/.claude-plugin/plugin.json new file mode 100644 index 000000000..48d7b666d --- /dev/null +++ b/plugins/docs-hygiene/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", + "name": "docs-hygiene", + "version": "0.1.0", + "description": "Documentation-hygiene toolkit of five skills: compress (flavor-trim markdown with a semantic-diff safety net), declutter (classify markdown noise), extract-ssot (deduplicate repeated content into a single source of truth), encapsulation-audit (detect citations into skill-private surfaces), and rename-references (sweep stale references after renames).", + "author": { + "name": "Melodic Software", + "email": "info@melodicsoftware.com" + }, + "license": "MIT", + "keywords": ["documentation", "markdown", "maintenance", "hygiene", "deduplication", "refactoring"] +} diff --git a/plugins/docs-hygiene/README.md b/plugins/docs-hygiene/README.md new file mode 100644 index 000000000..8d8b2cde1 --- /dev/null +++ b/plugins/docs-hygiene/README.md @@ -0,0 +1,43 @@ +# docs-hygiene + +A Claude Code plugin bundling five documentation-hygiene skills — one cohesive +capability: keeping a repository's tracked markdown lean, deduplicated, and +free of decayed references. Each skill is invocable on its own; together they +cover the flavor, noise, duplication, boundary, and rename axes of doc upkeep. + +## The five skills + +| Skill | What it does | +|---|---| +| `/docs-hygiene:compress` | Tightens markdown by dropping flavor (filler, hedging, articles) while preserving all content, behind a mandatory fresh-context semantic-diff audit that reverts any semantic loss. Supports an optional `caveman` plugin backend (`/caveman:compress`) with a built-in in-session fallback. | +| `/docs-hygiene:declutter` | Read-only classifier for five markdown noise shapes (historical citations, ghost refs to ephemeral working directories, "why this file exists" preambles, hard-coupled consumer lists, scope/loading meta-commentary) with tiered findings and per-shape treatment guidance. | +| `/docs-hygiene:extract-ssot` | Deduplicates content repeated across 3+ files into a single named source of truth and migrates call sites to cite it by heading — with refuse-fast verification gates (Rule of Three, Tier-0 evidence) so weak clusters are rejected instead of extracted. | +| `/docs-hygiene:encapsulation-audit` | Detects external citations reaching into skill-private surfaces inside `.claude/skills//` (private subdirectories, heading anchors, schema files) and routes each violation to a remediation path. Ships its own public-surface contract reference. | +| `/docs-hygiene:rename-references` | Sweeps stale references after renames — the forms plain token grep misses: slash-command tokens, relative paths from moved files, frontmatter chains and globs — via a 12-form pattern library with audit, half-rename detection, and apply modes. | + +## Install + +```shell +/plugin marketplace add melodic-software/claude-code-plugins +/plugin install docs-hygiene@melodic-software +``` + +## How the skills adapt to your repo + +The bundled defaults are repo-agnostic: detectors run against the repository +they are invoked in, output destinations default to conventional locations +(e.g. `.claude/rules/.md` for an extracted rule), and ephemeral-path +conventions default to a `.work//` example. Refine any of these through +your own repository's `CLAUDE.md` / `.claude/rules` — the skills read the +consuming project's context; nothing requires editing the plugin. + +## Configuration + +This plugin has no `userConfig`. The bundled scripts are read-only detectors +and fact emitters with no network access; `compress` persists optional +snapshots under the plugin's own data directory. + +## License + +MIT (SPDX-License-Identifier: MIT). See the LICENSE file at the root of the +melodic-software/claude-code-plugins repository. diff --git a/plugins/docs-hygiene/skills/compress/SKILL.md b/plugins/docs-hygiene/skills/compress/SKILL.md new file mode 100644 index 000000000..5d1123033 --- /dev/null +++ b/plugins/docs-hygiene/skills/compress/SKILL.md @@ -0,0 +1,123 @@ +--- +name: compress +description: "Compress (tighten, shorten, trim) markdown files by dropping flavor — filler, hedging, articles — while preserving all content (directives, qualifiers, thresholds, examples), with a mandatory semantic-diff subagent that reverts any SEMANTIC LOSS or AMBIGUITY. Use when: \"compress this doc\", \"tighten markdown\", \"cut prose\", \"shorten without losing meaning\", \"trim onboarding doc\", or verbose prose in docs/, READMEs, rule bodies, skill bodies, or third-party pasted text — actions: default (snapshot → backend → semantic-diff subagent → revert-pass → markdownlint) and audit (read-only dry-run classifying SKIP/COMPRESS/UNCERTAIN per file); flags: --force (bypass <3% revert rule), --keep-snapshot; not for: session compaction (/compact), markdown noise removal (/declutter), code-comment trimming, or content relocation/SSOT consolidation (/extract-ssot)." +argument-hint: "[audit] [target] [--force] [--keep-snapshot]" +user-invocable: true +disable-model-invocation: false +--- + +## Pre-computed context + +Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"` +Uncommitted .md files: !`git status --porcelain 2>/dev/null | grep '\.md$' | head -10 || echo "none"` + +## Purpose + +Markdown in `docs/`, README files, onboarding docs, third-party pasted prose, and drifted skill bodies accumulates FLAVOR — filler ("just", "really", "basically"), hedging ("perhaps", "might"), articles, pleasantries, redundant restatement. `context/flavor-vs-content-matrix.md` defines FLAVOR (safe to cut) vs CONTENT (never cut); this skill applies that taxonomy AT EDIT TIME to content where author-time discipline does NOT apply. + +Always-loaded instruction files (`.claude/rules/**`, `AGENTS.md`, `CLAUDE.md`, `**/SKILL.md`) bound empirically at 2-3% yield (baseline from the authoring repo: 3/3 attempts reverted, all flavor-only, 0 semantic loss). Likely 5-15% yield on author-time-undisciplined content. + +Methodology: snapshot original → backend mechanical compression (the `caveman` plugin via `/caveman:compress`, OR in-session Edit fallback) → spawn semantic-diff subagent comparing original vs condensed (output: SEMANTIC LOSS / AMBIGUITY / FALSE POSITIVE per finding with verbatim citations) → revert every SEMANTIC LOSS + AMBIGUITY → run `markdownlint-cli2` → ship or revert. + +## Backend selection + +Default-action Step B picks the mechanical-compression backend: the `caveman` plugin (marketplace `caveman`, invoked as `/caveman:compress`) when present, otherwise the in-session Edit-based fallback. Caveman performs the mechanical flavor cuts (articles, fillers, hedging, verbose-verb collapses) as the compression backend — it is NOT the verification gate. Fallback policy is graceful: the in-session Edit-based path substitutes whenever caveman is absent or unwanted. Subsequent steps (semantic-diff dispatch, revert pass, markdownlint) wrap the output regardless of backend choice. + +Note the distinction inside that plugin: `/caveman:compress` is a function-call skill (this skill's backend); `/caveman:caveman` is a session-wide response formatter — unrelated to this skill. + +**Step A — detect caveman plugin:** `bash "${CLAUDE_SKILL_DIR}/scripts/detect-caveman.sh"` + +**Step B — caveman backend (preferred):** + +```bash +tempdir=$(mktemp -d) +trap 'rm -rf "$tempdir"' EXIT +cp "$target" "$tempdir/$(basename "$target")" +# Invoke caveman via Skill tool on tempdir copy: +# Skill(caveman:compress, args="$tempdir/$(basename "$target")") +# Caveman writes compressed output to tempdir/basename and backup to tempdir/.original.md. +# Both stay inside tempdir; trap cleans on EXIT. +cp "$tempdir/$(basename "$target")" "$target" # only on caveman success +``` + +Tempdir wrapper contains caveman's hardcoded `.original.md` backup write. Real-path file replaced atomically on success. Consumers may add a defensive `**/*.original.md` entry to their `.gitignore` as belt-and-suspenders against tempdir cleanup races or future caveman backup-path-convention changes. + +**Step B fallback — in-session Edit (caveman absent or disabled):** + +Agent applies Edit ops directly on `$target` per the `context/flavor-vs-content-matrix.md` taxonomy. Same flavor-vs-content rules; no backend indirection. + +**Step C+ unchanged:** semantic-diff dispatch (mandatory hard rule), revert pass for SEMANTIC LOSS / AMBIGUITY / UNCERTAIN findings, markdownlint-cli2, summary. + +## Action router + +| Action | Args | Behavior | +|---|---|---| +| `` (default, no action keyword) | empty → uncommitted `.md` from `git diff`; file path → single-file; dir path → batch | snapshot → backend → dispatch → revert-pass → markdownlint verify → summary | +| `audit [target]` | same target rules | read-only dry-run; compute expected-yield heuristic per `context/target-types.md`; classify SKIP/COMPRESS/UNCERTAIN | + +Flags (apply to both actions): + +- `--force` — proceed even when the default `<3% AND 0 semantic-loss → REVERT` rule would trip. User owns the sub-3% diff +- `--keep-snapshot` — persist the original to `${CLAUDE_PLUGIN_DATA}/snapshots/Z-.orig.md` (the plugin data directory survives plugin updates) + +## Auto-detect default + +1. Empty arg AND clean tree → friendly no-op exit 0 ("No uncommitted .md files. Pass file/dir target.") +2. Empty arg AND uncommitted `.md` files → batch default action over those files +3. Single file path → single-file default action +4. Directory path → batch default action (filenames sorted lexically for deterministic output) +5. First positional == `audit` → audit action on rest + +## Hard rules + +- **Semantic-diff dispatch is mandatory for default action.** Audit is read-only — no dispatch. +- **Post-edit `markdownlint-cli2` MUST pass** (using the consuming repository's markdownlint config when present). Non-zero exit blocks ship; revert and surface failures. +- **Default `<3% AND 0 semantic-loss → REVERT`.** Proven safe in the authoring repo's empirical baseline (always-loaded instruction files: 3/3 attempts reverted). `--force` bypasses. +- **Summary output deterministic.** No timestamps; filenames sort lexically. +- **Snapshot default = ephemeral** (`mktemp -d`, deleted post-dispatch). `--keep-snapshot` persists to `${CLAUDE_PLUGIN_DATA}/snapshots/` instead. +- **Always-loaded instruction-file policy: SOFT-BLOCK.** Default reverts <3%/0SL on ANY file including `.claude/rules/**` / `AGENTS.md` / `CLAUDE.md` / `**/SKILL.md`. `--force` bypasses on ANY file — user owns the result. `audit` heuristic emits informational SKIP recommendation on always-loaded paths citing the 2-3% empirical baseline; not a structural gate. +- **Subagent dispatch follows `context/semantic-diff-prompt.md` template.** Findings must carry verifiable citations; training-recall citation tokens are forbidden — `[known]` / `[from memory]` / `[context]` / `[obvious]` / `[standard]` / `[usual]`. +- **Backend choice does NOT bypass semantic-diff dispatch.** When the caveman backend is absent or unwanted, `/compress` falls back to in-session Edit-based compression. Backend selection determines only the mechanical-compression path; semantic-diff + revert pass + markdownlint hard rules apply regardless. LLM-compression fabrication risk (caveman backend OR in-session Edit) caught structurally by the revert pass. + +## Output schema (default action, per target) + +```text +: (compression_pct=N.N%, semantic_loss=K, ambiguity=M, false_positive=P, markdownlint=PASS|FAIL) +``` + +`action_taken` ∈ {`compressed`, `reverted`, `skipped`}. Aggregate at end of batch. + +Audit action output: table with `target`, `expected_yield_pct`, `classify` (SKIP/COMPRESS/UNCERTAIN), `reason`. + +## Gotchas + +Observed failure points — each traces to a real incident; grown iteratively. + +- **Self-audit drifts toward EXPANSION.** The semantic-diff dispatch must run as a SEPARATE fresh-context audit, never a self-audit by the model that produced the edits — self-audit re-adds words just removed ("preserve clarity"). Empirically observed in the authoring repo: 4/4 reverse-direction edits in one batch compression wave. Subagents cannot reliably spawn the verifier themselves (nested subagent support is version-dependent, and a fresh-context verifier beats self-critique regardless), so for batch fan-out follow `context/fan-out-orchestration.md`: the main session dispatches separate compress + audit subagents, reconciling per finding. +- **Sub-3% diffs auto-revert unless `--force`.** Default `<3% AND 0 semantic-loss → REVERT`; always-loaded instruction files bound at 2-3% yield (empirical baseline: 3/3 reverted, all flavor-only). Pass `--force` only when a targeted sub-3% diff is intentional — the user owns the result. +- **Caveman writes a `.original.md` backup beside the target.** The caveman backend hardcodes this backup path; running it against the real file litters the repo. Backend Step B wraps caveman in a `mktemp -d` tempdir so the backup lands there and the `trap` cleans it; a gitignore entry for `**/*.original.md` in the consuming repo is optional belt-and-suspenders. + +## When NOT to use + +- Code files (`.cs`, `.py`, `.ts`, `.sh`, etc.) — methodology is markdown-specific. Code-comment compression is out of scope +- Binary files +- Author-time-disciplined instruction files (`.claude/rules/**`, `AGENTS.md`, `CLAUDE.md`, `**/SKILL.md`) — `audit` will SKIP-recommend; sub-3% revert default applies (Gotchas "Sub-3% diffs auto-revert unless `--force`") +- Conversation summarization or session compaction — that's the built-in `/compact`, different semantic +- **Subagent context invoking `/compress` for batch fan-out** — see Gotchas "Self-audit drifts toward EXPANSION"; follow `context/fan-out-orchestration.md` + +## What this skill is NOT + +- **Not an orchestrator surface.** `/compress` prints a human-readable summary; no structured/`--json` output +- **Not a lint front-end.** `markdownlint-cli2` is the post-edit verifier, not the primary purpose +- **Not a code-comment compressor.** Out of scope +- **Not a `/code-review` / `/simplify` shadow.** The built-in `/code-review` and `/simplify` review code changes; `/compress` rewrites markdown prose. Different concerns +- **Not `/declutter`.** `/compress` owns FLAVOR (filler, hedging, articles, redundant restatement). `/declutter` owns NOISE classification (historical citations, ghost refs, "Why this file exists" preambles, hard-coupled enumerated consumer lists) per its own taxonomy. Different concerns; both may apply to the same target iteratively +- **Not a content-relocation / cite-don't-recap tool.** When an inline passage recaps detail that already lives in a cited single source of truth (another doc or rule), condensing it is content RELOCATION, not flavor removal — the mandatory semantic-diff net sees the words gone from THIS file and reverts them as SEMANTIC LOSS, blind to the SSOT. Apply "reference, don't duplicate" as a MANUAL editorial pass (verify the cited SSOT actually holds the detail first — an unread pointer is an unverified claim); use `/extract-ssot` when the duplicated cluster spans 3+ files + +## Cross-references + +- `context/semantic-diff-prompt.md` — subagent dispatch template (Agent tool prompt + return-format contract) +- `context/flavor-vs-content-matrix.md` — canonical FLAVOR / CONTENT taxonomy + per-content-type variants +- `context/target-types.md` — per-action argument shapes + author-time-signal heuristic +- `context/fan-out-orchestration.md` — multi-phase batch fan-out recipe; read when compressing N files via parallel subagents (keeps the semantic-diff in a separate fresh-context auditor) +- `context/integration.md` — composition contract with sibling skills and consumer workflows diff --git a/plugins/docs-hygiene/skills/compress/context/fan-out-orchestration.md b/plugins/docs-hygiene/skills/compress/context/fan-out-orchestration.md new file mode 100644 index 000000000..c7cda8d64 --- /dev/null +++ b/plugins/docs-hygiene/skills/compress/context/fan-out-orchestration.md @@ -0,0 +1,74 @@ +# Compress fan-out orchestration + +Read this when batch-compressing N markdown files via parallel subagents. Codifies the multi-phase split that keeps the mandatory semantic-diff in a SEPARATE fresh-context auditor. Before Claude Code v2.1.172 a subagent could not spawn the verifier at all (no nested Agent tool); as of v2.1.172 a foreground subagent can, but nested spawning is version-dependent and a fresh-context verifier beats self-critique regardless — so the auditor phase stays a main-session dispatch. + +**Why this exists:** `/compress` "Hard rules" mandate semantic-diff dispatch. A subagent that invokes `/compress` must NOT run that dispatch as a self-audit in its own context — self-audit by the same model that produced the edits drifts toward EXPANSION ("preserve clarity" re-adds words just removed). Empirically observed: 4/4 reverse-direction edits in a compression wave (see ## History). Fix: move the semantic-diff into a separate fresh-context subagent dispatched by the main session. + +## Architecture (three phases per wave) + +### Phase A — compressor subagents (parallel, 3-5 per wave) + +Each subagent compresses exactly ONE file via the Edit tool. **NO `/compress` slash invocation, NO self-audit, NO re-review.** Returns a diff stat. Latitude follows this skill's flavor-vs-content taxonomy (`context/flavor-vs-content-matrix.md`) — full mechanical drops plus prose-quality moves (passive → active, nominalization collapse). + +Canonical Phase A prompt template (compose verbatim, substitute ``): + +```text +Compress exactly ONE file: + +LATITUDE: +- Mechanical drops: articles (the/a/an) before clear nouns, filler (just/really/basically/actually/simply), hedging (perhaps/somewhat/might in factually-direct statements), pleasantries, verbose verb phrases (in order to → to, due to the fact that → because, make use of → use) +- Prose playbook: passive → active voice, nominalization collapse ("performs analysis of" → "analyzes", "is responsible for" → "owns") + +HARD RULES: +- NEVER add words. EVER. +- NEVER swap word X for synonym X' unless X' is strictly shorter AND same meaning +- NEVER touch code blocks (fenced ``` or inline `...`), URLs, file paths, identifiers, env vars, slash commands, hook names +- NEVER touch directive force ("must" vs "should" vs "may") +- NEVER touch qualifiers narrowing scope (ONLY, NEVER, every, all, only, exact) +- NEVER touch thresholds, version pins, SHAs (3+, 5+, ≥30s, 0.11.0) +- NEVER touch examples, counter-examples, "X not Y" pairs +- NEVER touch error messages, quoted text, citations +- NEVER self-audit. NEVER re-read your own edits. NEVER "preserve clarity". +- Touch ONLY . FORBIDDEN: any other file, any git operation, any other repo path. + +DELIVERABLE: apply Edit ops; return exactly one line: +: edited (changes=N, bytes_saved=B) + +If nothing safely droppable after one read-through: : no-op (reason) +``` + +### Phase B — auditor subagents (parallel, one per Phase-A modified file) + +Main session dispatches via the Agent tool. Each subagent applies `/compress`'s semantic-diff prompt template against ONE file's diff. The main session has the Agent tool; this dispatch succeeds. + +Main session preparation per subagent: + +- ORIGINAL content via `git show HEAD:` (free, no disk overhead; requires the user pre-staged the baseline) +- CONDENSED content via the current file body +- Prompt body: the semantic-diff prompt owned by this skill (`context/semantic-diff-prompt.md` — substitutes `{ORIG}` / `{COND}` placeholders into a FINDING-block + TOTAL-summary contract) + +Subagent returns FINDING blocks + a TOTAL summary line per the semantic-diff-prompt contract. Forbidden citation tokens (training-recall markers) invalidate the dispatch — revert that file's candidate. + +### Phase C — main-session reconciliation + +Per FINDING block returned in Phase B: + +- `SEMANTIC LOSS` / `AMBIGUITY` / `UNCERTAIN` → main session reverts that specific CONDENSED line back to ORIGINAL (per-finding revert via the Edit tool, not whole-file revert) +- `FALSE POSITIVE` → keep the compression +- Markdownlint each file post-reconcile; non-zero exit → whole-file revert (per-finding revert produced malformed state) +- Update any batch-tracking artifact your workflow maintains — main session only; subagents never write shared state + +## Request budget + +8 requests per wave (4 compress + 4 audit). For 71 files at 4-per-wave: 18 waves × 8 = 144 requests total. Comparable to the in-skill dispatch path; quality is dramatically better because the auditor cannot self-modify the file. + +## Orchestration rules + +- **Phase A scope fence** — each compressor subagent's prompt names exactly ONE allowed file; any other file, git operation, or path is forbidden (the template above encodes this) +- **Phase A does NOT invoke `/compress`** as a slash command from subagents — self-audit in the compressor context caused reverse-direction edits (see ## History) +- **Refuse-fast threshold** — 5 consecutive Phase A or Phase B ERROR returns aborts the batch +- **Phase B returns are unverified synthesis** — the main session reverts per finding rather than verifying each by hand; a forbidden citation token invalidates the whole dispatch + +## History + +- 2026-05-23 — authoring-repo batch compression wave, empirical: 4/4 reverse-direction edits from self-audit (led to this architecture) diff --git a/plugins/docs-hygiene/skills/compress/context/flavor-vs-content-matrix.md b/plugins/docs-hygiene/skills/compress/context/flavor-vs-content-matrix.md new file mode 100644 index 000000000..78fbd5d75 --- /dev/null +++ b/plugins/docs-hygiene/skills/compress/context/flavor-vs-content-matrix.md @@ -0,0 +1,61 @@ +# Flavor vs content matrix + +Canonical FLAVOR / CONTENT taxonomy for the `/compress` semantic-diff pass, plus per-content-type variants tuning expected yield and revert-pass focus. + +## Canonical taxonomy + +### Flavor (safe to cut) + +- Articles (a/an/the) +- Filler (just/really/basically/actually/simply) +- Hedging (perhaps/somewhat/might) +- Pleasantries +- Redundant restatement of bold rule names +- "in order to" / "due to the fact that" verbose forms +- Conversational connectives ("that said", "in other words") +- Verbose verb phrases ("make use of" → "use") + +### Content (NEVER cut) + +- (a) every directive, including imperative force — "must have" ≠ "has" +- (b) every concrete prohibited-pattern example with a literal token +- (c) every counter-example / anti-example — both halves of "X not Y" pairs +- (d) every qualifier narrowing scope (ONLY, repeatedly, instantly recognized, that appear in) +- (e) every rule-unique "why" rationale — if removing it lets two readers infer different applicability, keep it +- (f) every cross-reference, file path, env var, SHA, version pin, identifier, slash command, hook name, agent name +- (g) every exception clause + example +- (h) every threshold (3+, 5+, ≥30s, <2min) +- (i) every enumeration item +- (j) every inline-code token + +## Per-content-type variants + +The taxonomy is invariant across content types. What varies is the EXPECTED YIELD and the RISK PROFILE for which (a)–(j) Content items are most often at stake. Variants below tune the revert-pass strictness and the audit-action expected-yield estimate per `context/target-types.md`. + +| Content type | Expected yield | At-risk content items (per (a)–(j) above) | Variant guidance | +|---|---|---|---| +| **Always-loaded instruction file** (`.claude/rules/**`, `AGENTS.md`, `CLAUDE.md`, `**/SKILL.md`) | 2-3% | (a) directives, (d) scope qualifiers, (e) rule-unique rationale, (f) cross-references | Author-time-disciplined. Default action will revert per SKILL.md "Hard rules" (<3% AND 0SL → REVERT). `--force` only when a targeted sub-3% diff is intentional. Empirical baseline: 3/3 attempts reverted | +| **Onboarding doc** (README onboarding, `docs/onboarding-*.md`, contributor guides) | 8-15% | (b) prohibited-pattern tokens, (c) counter-examples, (h) thresholds | Verbose-prose baseline. Hedging + pleasantries dense; restatement of policy across sections common. Revert-pass strictness: keep every "X not Y" pair intact (counter-example loss = ambiguity in onboarding) | +| **README** (`README.md`, `*/README.md` at app/lib/service roots) | 5-12% | (f) cross-references, (g) exception clauses, (j) inline-code tokens | Project-front-door surface. Inline-code density usually high (commands, paths); revert any (j) drop. Cross-references load-bearing for navigation | +| **Drifted skill body** (`**/SKILL.md` past ~250 lines AND not author-time-disciplined) | 4-7% | (a) directives, (e) rule-unique rationale, (i) enumeration items | Skill bodies tend to accumulate procedural prose during evolution. Revert any directive softening ("must" → "should"); revert any enumeration-item drop. Often a single revert-pass produces a final ship | +| **Third-party pasted prose** (vendor docs, external policy text, copied research notes) | 10-20% | (b) prohibited-pattern tokens, (h) thresholds, (j) inline-code tokens | Highest yield + highest risk. Pasted prose carries verbose flavor authors did not edit. Inline-code tokens (CLI flags, schema field names) MUST survive verbatim; treat any (j) loss as SEMANTIC LOSS not AMBIGUITY | + +## Variants never relax the preservation contract + +The (a)–(j) Content list defines the universal preservation contract. Per-content-type framing only changes which list items fire most often and at what yield — it never implies per-type CONTENT relaxation, which is forbidden: every (a)–(j) item is preserved on every content type. The variant table above tunes EXPECTED YIELD + revert-pass FOCUS, never the preservation contract. + +## Audit-action expected-yield mapping + +`/compress audit ` classifies SKIP / COMPRESS / UNCERTAIN per `context/target-types.md` "Author-time-signal heuristic". The "Expected yield" column above feeds that heuristic's output: + +- Expected yield < 3% (always-loaded instruction files) → audit emits **SKIP** with empirical-baseline citation +- Expected yield 3-7% (drifted skill bodies) → audit emits **UNCERTAIN**; user gates via `--force` or skip +- Expected yield ≥ 8% (onboarding / README / third-party) → audit emits **COMPRESS** + +Numeric ranges drift; revisit the variant table as empirical evidence accumulates. + +## Cross-references + +- `../SKILL.md` "Auto-detect default" + "Hard rules" — default action revert rules consuming this variant table +- `context/target-types.md` — heuristic that feeds the audit-action SKIP/COMPRESS/UNCERTAIN classification +- `context/semantic-diff-prompt.md` — dispatch template that operationalizes the preservation contract diff --git a/plugins/docs-hygiene/skills/compress/context/integration.md b/plugins/docs-hygiene/skills/compress/context/integration.md new file mode 100644 index 000000000..dec2cfd37 --- /dev/null +++ b/plugins/docs-hygiene/skills/compress/context/integration.md @@ -0,0 +1,50 @@ +# Integration — composition contract + +How `/compress` composes with sibling skills in this plugin and with a consuming repository's own workflows. Every citation below uses the `/skill-name ` public-surface form; nothing reaches into another skill's internals (paths, schemas, scripts, heading anchors). + +## Composition table + +| Surface | Direction | Contract | +|---|---|---| +| The consuming repo's markdown lint | `/compress` runs it | Post-edit verification. SKILL.md "Hard rules" requires `markdownlint-cli2` PASS on every ship, using the consuming repository's markdownlint config when present. If the consumer has a broader lint workflow, it may run after a `/compress` batch to surface the full report. Failure blocks ship per the `/compress` revert rule | +| A planning workflow (if the consumer has one) | calls `/compress` | Plan authoring. When a planning artifact grows ≥ 2000 words (or an exploration/research artifact beyond 1500 words), the author may invoke `/compress ` on the artifact before handing it off. Composition is plan-level; the consumer's workflow decides when | +| An instruction-audit workflow (if the consumer has one) | calls `/compress` | Always-loaded surface audit. When such an audit flags `CLAUDE.md` or rule-file size bloat AND empirical yield > 3%, the user may invoke `/compress --force ` to take the targeted sub-3% diff. `--force` is mandatory on always-loaded instruction paths — audit will SKIP-recommend without it | +| `/encapsulation-audit` | parallel concern | No invocation either direction. `/encapsulation-audit` detects external citations into skill-private surfaces; `/compress` edits the markdown targets it is given. The two skills do not interact at runtime | +| A pre-PR quality gate (if the consumer has one) | calls `/compress` | When a pre-PR check surfaces uncommitted `.md` files in the working tree, the user may invoke `/compress` (empty arg auto-detects) before PR prep. `/compress` does not auto-trigger from any gate; user-gated | + +Boundaries with the other bundled siblings — `/declutter` (noise classification, not flavor) and `/extract-ssot` (content relocation across 3+ files, not flavor) — are defined in `../SKILL.md` "What this skill is NOT". + +## Public-surface invocation forms + +Citations from sibling skills or consumer workflows MUST use one of: + +```text +/compress # empty-arg auto-detect over uncommitted .md +/compress # single-file default action +/compress # batch default action +/compress audit # empty-arg audit (read-only dry-run) +/compress audit # audit single file or dir +/compress --force # bypass <3%/0SL revert rule (user owns sub-3% diff) +/compress --keep-snapshot # persist .orig.md to the plugin data directory +``` + +NEVER cite this skill's `context/` files, its scripts, or any heading anchor inside its `SKILL.md` from outside the skill. The `context/*.md` files are private implementation surface; `/compress`'s public contract is the action + arg + flag set above. + +## Anti-patterns + +| Don't | Do | +|---|---| +| `/compress` to compress code files (`.cs`, `.py`, `.sh`) | Out of scope per SKILL.md "When NOT to use"; code-comment compression is out of scope | +| Chain `/compress` + a separate markdown lint pass assuming `/compress` skipped lint | `/compress` runs `markdownlint-cli2` internally per Hard rules; a redundant lint invocation costs an extra pass with no signal | +| Pipe `/compress` output into an orchestrator expecting structured data | `/compress` summary is human-readable, not structured. No `--json` | +| `/compress` to summarize a conversation | That's the built-in `/compact`, different semantic (conversation summarization, not markdown content) | +| Bypass the semantic-diff dispatch with a `--no-verify`-style flag | No such flag exists. SKILL.md "Hard rules" makes dispatch mandatory for default action; `audit` is the read-only escape if dispatch cost is the concern | + +## Composition with build/test front-ends + +`/compress` does NOT compose with build or test workflows — those are code-correctness surfaces. Markdown content has no build or test gate beyond markdownlint, which `/compress` invokes directly. Within whatever pre-PR sequence the consuming repository runs, `/compress` is opportunistic before staging when uncommitted markdown exists, NOT a mandatory step. + +## Cross-references + +- `../SKILL.md` "Action router" — public-surface action set the composition table cites +- `../SKILL.md` "What this skill is NOT" — boundaries against lint front-ends, code review, `/declutter`, `/extract-ssot` diff --git a/plugins/docs-hygiene/skills/compress/context/semantic-diff-prompt.md b/plugins/docs-hygiene/skills/compress/context/semantic-diff-prompt.md new file mode 100644 index 000000000..808f2e903 --- /dev/null +++ b/plugins/docs-hygiene/skills/compress/context/semantic-diff-prompt.md @@ -0,0 +1,78 @@ +# Semantic-diff subagent dispatch template + +Agent tool prompt body + return-format contract for the default action. Loaded by `/compress` when dispatching the comparison pass; private implementation surface — do NOT cite this file from outside the skill (external consumers invoke `/compress`). + +## Dispatch shape + +Spawn one subagent per target file via `Agent` tool. Subagent type: `general-purpose`. Pass two file paths (original snapshot + condensed candidate) plus the prompt body below verbatim. Subagent returns categorized findings; main session decides revert vs ship. + +## Prompt body (paste into Agent tool `prompt` field verbatim, with `{ORIG}` / `{COND}` substituted) + +```text +Compare two markdown files and classify every textual difference. ORIGINAL is the snapshot; CONDENSED is the candidate produced by a compression pass that intends to drop FLAVOR (filler, hedging, articles, pleasantries, redundant restatement) while preserving CONTENT (directives, qualifiers, examples, thresholds, exceptions, cross-references, identifiers). + +ORIGINAL: {ORIG} +CONDENSED: {COND} + +For every difference, classify as exactly one of: + + SEMANTIC LOSS — content removed/altered that changes what a reader must do, infer, or rely on. Includes: dropped directive ("must" → silence), narrowed qualifier ("ONLY X" → "X"), removed anti-example, removed threshold, removed exception clause, removed identifier, removed cross-reference, removed inline-code token. + + AMBIGUITY — content removed/altered such that two readers could now infer different applicability. Includes: collapsed rule-unique rationale, dropped "why" that constrained scope, removed enumeration item where order mattered, merged distinct clauses that had different scopes. + + FALSE POSITIVE — pure flavor cut, no content delta. Includes: article drop ("the X" → "X"), filler drop ("just", "really", "basically"), hedging drop ("perhaps", "might"), pleasantry drop, verbose-verb collapse ("make use of" → "use"), restatement removed. + +Output schema (one block per finding, in CONDENSED line order): + + FINDING N: + ORIGINAL: "" + CONDENSED: "" + RATIONALE: + CITATION: + +Allowed CITATION tokens (verify primary source THIS turn before quoting): + + [:] file read this turn (e.g. [README.md:142]) + [ --help:] CLI help output captured this turn + [] doc URL fetched this turn (paste full URL) + [TBD — ] cannot verify primary source; flag the gap + +Forbidden CITATION tokens (training-recall markers, treat as unverified): + + [known], [from memory], [context], [obvious], [standard], [usual] + +If a finding cannot be classified into exactly one of the three categories, emit "FINDING N: UNCERTAIN" with rationale; main session treats UNCERTAIN as AMBIGUITY for revert purposes. + +End with a summary line: + TOTAL: ; SEMANTIC LOSS: ; AMBIGUITY: ; FALSE POSITIVE:

; UNCERTAIN: + +Do NOT propose revisions. Do NOT score quality. Classify only. +``` + +## Required-pattern checklist (sanity-check grep targets) + +The prompt body above contains all four allowed citation token patterns and all six forbidden tokens. When editing this file, drift-detect from the skill root via: + +```bash +grep -F '[:]' context/semantic-diff-prompt.md +grep -F '[ --help:]' context/semantic-diff-prompt.md +grep -F '[]' context/semantic-diff-prompt.md +grep -F '[TBD — ]' context/semantic-diff-prompt.md +``` + +All four MUST match. + +## Return-format contract + +Main session parses the subagent return for the `TOTAL:` summary line; counts feed the output schema (`compression_pct`, `semantic_loss`, `ambiguity`, `false_positive`). Per-finding blocks drive the revert pass — every SEMANTIC LOSS + AMBIGUITY (+ UNCERTAIN) finding's CONDENSED quote reverts to its ORIGINAL form. + +## Failure modes + +- **Subagent returns prose without the FINDING N: blocks** — treat as dispatch failure; surface error + revert entire candidate. Do NOT ship a partially-classified diff. +- **Subagent uses forbidden citation token** — treat ALL findings from that dispatch as unverified training recall; revert the entire candidate. +- **Subagent returns 0 findings** — verify with `diff -u {ORIG} {COND}` that files actually differ; 0 findings on a non-zero diff = dispatch failure (revert). + +## Cross-references + +- `../SKILL.md` "Hard rules" — semantic-diff dispatch mandatory for default action; forbidden-token list restated there as a hard rule +- `context/flavor-vs-content-matrix.md` — the FLAVOR / CONTENT taxonomy this template operationalizes diff --git a/plugins/docs-hygiene/skills/compress/context/target-types.md b/plugins/docs-hygiene/skills/compress/context/target-types.md new file mode 100644 index 000000000..065dee923 --- /dev/null +++ b/plugins/docs-hygiene/skills/compress/context/target-types.md @@ -0,0 +1,85 @@ +# Target types + author-time-signal heuristic + +Argument-shape resolution for default + audit actions, plus the mechanical-scan heuristic that drives the audit action's SKIP / COMPRESS / UNCERTAIN classification. + +## Argument shapes + +Per `../SKILL.md` "Auto-detect default", argument resolution at invocation: + +| Invocation | Target set | Action | +|---|---|---| +| `/compress` (empty arg) AND uncommitted `.md` exist | files from `git status --porcelain` matching `*.md` | default action over each, batch | +| `/compress` (empty arg) AND clean tree | (none) | friendly no-op exit 0 ("No uncommitted .md files. Pass file/dir target.") | +| `/compress ` | single file | default action, single-file | +| `/compress

` | every `.md` under `` (recursive); filenames sorted lexically for determinism | default action, batch | +| `/compress audit` (empty rest) AND uncommitted `.md` exist | files from `git status --porcelain` matching `*.md` | audit action over each | +| `/compress audit` (empty rest) AND clean tree | (none) | friendly no-op exit 0 | +| `/compress audit ` | single file | audit action | +| `/compress audit ` | every `.md` under ``; lexical sort | audit action, batch | + +Flags `--force` and `--keep-snapshot` apply per `../SKILL.md` "Action router". Position-independent within the arg list. + +### Target validation + +Per-target gates before any dispatch: + +1. Path exists and is readable → otherwise skip target with `reason=missing` +2. Path ends in `.md` (case-insensitive) → otherwise skip with `reason=non-markdown` +3. Path NOT a symlink escaping repo root → otherwise skip with `reason=symlink-escape` +4. Path NOT inside `.git/` → otherwise skip with `reason=git-internal` + +Binary files and non-markdown files are out of scope per `../SKILL.md` "When NOT to use". + +## Author-time-signal heuristic (audit action only) + +Audit is a pure mechanical scan — no subagent dispatch, no edits. Per target, compute an expected-yield estimate from five signals; emit SKIP / COMPRESS / UNCERTAIN per the classification table below. + +### Five signals + +| # | Signal | Method | Effect on expected-yield | +|---|---|---|---| +| 1 | Author-time-disciplined path (instruction-file glob) | path matches `.claude/rules/**` OR `AGENTS.md` OR `CLAUDE.md` OR `**/SKILL.md` (any depth) | force expected ≤ 3%; emit empirical-baseline citation (3/3 attempts reverted) | +| 2 | Inline-code-token density | `awk` count of backtick pairs (`` ` ``) per kilo-word (1000 words = 1 unit); density > 10 = high | high density → narrower compressible flavor → lower expected yield | +| 3 | Cross-reference density | regex count per kilo-word of `@`-paths, `.md` cites, file-system path tokens (`[a-z][a-z0-9._/-]+\.(md\|cs\|sh\|json\|yaml)`); density > 8 = high | high density → load-bearing references → lower expected yield | +| 4 | Explicit compression-discipline cite | `grep -F` for the fixed string `Prose compression discipline` — a file citing the consuming repo's author-time compression-discipline convention marks itself as already disciplined | match → author-time-disciplined → expected ≤ 3% | +| 5 | Default fallback (no other signal fires) | none of 1-4 match | verbose-prose baseline → expected 5-15% | + +### Classification table + +| Expected yield | classify | reason text | +|---|---|---| +| ≤ 3% (signals 1 OR 4 fire) | `SKIP` | "author-time-disciplined; empirical baseline 3/3 reverted; use `--force` only for targeted sub-3% diff" | +| 3-7% (signals 2 OR 3 fire, no signal 1/4) | `UNCERTAIN` | "inline-code density H AND/OR cross-ref density H; flavor band narrow" — H values inlined | +| ≥ 8% (signal 5 fallback) | `COMPRESS` | "verbose-prose baseline; expected flavor cuts on filler/hedging/articles" | + +### Output table (audit action) + +Per target, one row: + +| `target` | `expected_yield_pct` | `classify` | `reason` | +|---|---|---|---| +| `` | `N-M%` (range) | `SKIP\|COMPRESS\|UNCERTAIN` | `` | + +Aggregate at end: `Total: K skips, M compress-recommended, P uncertain`. + +## Why mechanical not subagent + +`audit` is a READ-ONLY pre-flight check. Dispatching a subagent per target would burn request budget against the default rule (`<3% AND 0SL → REVERT`) which audits predict cheaply. Mechanical scan ~50ms per file; subagent dispatch ~5-15s + request cost. + +Empirically (authoring-repo baseline): instruction-file paths produce <3% yield 3/3 attempts. Heuristic encodes that signal as a path glob — no subagent needed to predict the same verdict. + +## Recheck triggers + +| Condition | Action | +|---|---| +| Audit classifies an instruction-file path COMPRESS (signal 1 misfires) | Tighten path glob OR add a signal 1 exception; note the exception in this file | +| Default-fallback files (signal 5) consistently yield <5% | Bump COMPRESS threshold OR add a 6th signal capturing the new author-time-disciplined surface | +| Empirical yield baseline shifts beyond 3% | Update SKIP reason text + bump signal 1 expected band; rev the variant table in `context/flavor-vs-content-matrix.md` | +| New always-loaded instruction path lands outside `.claude/rules/**` | Extend signal 1 glob; verify SKIP fires on the new path | + +## Cross-references + +- `../SKILL.md` "Auto-detect default" + "Action router" — consumes argument-shape table +- `../SKILL.md` "Hard rules" — revert + soft-block enforcement that audit predicts +- `context/flavor-vs-content-matrix.md` — per-content-type expected-yield bands feeding the classification table +- `context/semantic-diff-prompt.md` — dispatch template (audit does NOT dispatch; default action does) diff --git a/plugins/docs-hygiene/skills/compress/scripts/detect-caveman.sh b/plugins/docs-hygiene/skills/compress/scripts/detect-caveman.sh new file mode 100755 index 000000000..dd4e8014c --- /dev/null +++ b/plugins/docs-hygiene/skills/compress/scripts/detect-caveman.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Detect caveman plugin availability for /compress backend selection. +# +# Output: +# Caveman backend: available|absent|unknown +# Caveman plugin id: +# +# Exit: always 0. +# set -e omitted — script must never abort; always exits 0 per contract. +set -uo pipefail + +usage() { + cat <<'EOF' +detect-caveman.sh — caveman plugin availability facts for /compress. + +Usage: + detect-caveman.sh [--help] + +Exit: always 0. +EOF +} + +case "${1:-}" in +-h | --help) + usage + exit 0 + ;; +*) ;; +esac + +if ! command -v claude >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + printf 'Caveman backend: unknown\n' + printf 'Caveman plugin id: none\n' + exit 0 +fi + +plugin_id="$(claude plugin list --json 2>/dev/null | jq -r '.[] | select(.id | startswith("caveman@")) | .id' 2>/dev/null | head -1 | tr -d '\r')" + +if [[ -n "$plugin_id" ]]; then + printf 'Caveman backend: available\n' + printf 'Caveman plugin id: %s\n' "$plugin_id" +else + printf 'Caveman backend: absent\n' + printf 'Caveman plugin id: none\n' +fi diff --git a/plugins/docs-hygiene/skills/compress/scripts/detect-caveman.test.sh b/plugins/docs-hygiene/skills/compress/scripts/detect-caveman.test.sh new file mode 100755 index 000000000..897f358eb --- /dev/null +++ b/plugins/docs-hygiene/skills/compress/scripts/detect-caveman.test.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Contract test for detect-caveman.sh (the /compress backend detector). +# +# Self-contained: no shared assertion lib; resolves the script under test +# relative to this file. Asserts the always-exit-0 contract, both output +# labels, and graceful degradation to "unknown" when claude/jq are absent. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DETECT="$SCRIPT_DIR/detect-caveman.sh" + +PASS=0 +FAIL=0 +ok() { + echo "ok: $*" + PASS=$((PASS + 1)) +} +fail() { + echo "FAIL: $*" >&2 + FAIL=$((FAIL + 1)) +} + +assert_exit0() { + local label="$1" + shift + if "$@" >/dev/null 2>&1; then + ok "$label" + else + fail "$label (non-zero exit)" + fi +} + +assert_contains() { + local label="$1" haystack="$2" needle="$3" + if [[ "$haystack" == *"$needle"* ]]; then + ok "$label" + else + fail "$label (missing: $needle)" + fi +} + +assert_exit0 "--help exits 0" bash "$DETECT" --help +assert_exit0 "-h exits 0" bash "$DETECT" -h +assert_exit0 "bare invocation exits 0" bash "$DETECT" + +help_out="$(bash "$DETECT" --help 2>/dev/null)" +assert_contains "--help prints usage" "$help_out" "Usage:" + +out="$(bash "$DETECT" 2>/dev/null)" +assert_contains "backend label present" "$out" "Caveman backend:" +assert_contains "plugin id label present" "$out" "Caveman plugin id:" + +# Degradation contract: without claude/jq on PATH the script must report +# "unknown" (and still exit 0) rather than fail. Resolve bash first so the +# empty PATH only affects lookups inside the script under test. +BASH_BIN="$(command -v bash)" +if out_nopath="$(PATH="" "$BASH_BIN" "$DETECT" 2>/dev/null)"; then + ok "empty-PATH invocation exits 0" +else + fail "empty-PATH invocation exits 0 (non-zero exit)" +fi +assert_contains "degrades to unknown without claude/jq" "$out_nopath" "Caveman backend: unknown" +assert_contains "plugin id none without claude/jq" "$out_nopath" "Caveman plugin id: none" + +echo +if [[ $FAIL -ne 0 ]]; then + echo "$FAIL check(s) failed." >&2 + exit 1 +fi +echo "OK: detect-caveman.sh tests passed ($PASS checks)" diff --git a/plugins/docs-hygiene/skills/declutter/SKILL.md b/plugins/docs-hygiene/skills/declutter/SKILL.md new file mode 100644 index 000000000..6d800f993 --- /dev/null +++ b/plugins/docs-hygiene/skills/declutter/SKILL.md @@ -0,0 +1,92 @@ +--- +name: declutter +description: "Classify tracked markdown for five noise shapes — historical citations, ghost refs to ephemeral working-directory paths, \"Why this file exists\" preambles, hard-coupled enumerated consumer lists, and scope/loading meta-commentary — emitting Tier 1 (remove/relocate), Tier 2 (review needed), and Tier 3 (likely legitimate) findings with per-shape treatment guidance; read-only, no edits applied. Use when: 'declutter', 'audit markdown noise', 'check for stale citations', 'find ghost refs', 'classify preamble', 'sweep a rule/skill/convention doc for noise', or before editing any tracked .md — not for prose flavor/compression (use /compress) or structural markdown lint (your repo's markdown linter)." +argument-hint: "[audit] [target]" +user-invocable: true +disable-model-invocation: false +allowed-tools: Bash(bash *declutter/scripts/detect.sh*) +--- + +## Pre-computed context + +Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"` +Uncommitted .md files: !`git status --porcelain 2>/dev/null | grep '\.md$' | head -10 || echo "none"` +Noise findings (sample): !`bash "${CLAUDE_SKILL_DIR}/scripts/detect.sh" 2>/dev/null | grep -E '^(Summary total:|Finding shape:)' | head -20 || echo "none"` + +## Purpose + +Tracked markdown — rules, skill bodies, instruction files (`CLAUDE.md`, `AGENTS.md`), `docs/`, READMEs — accumulates five NOISE shapes distinct from FLAVOR (owned by the sibling `/compress`). Each shape carries a maintenance tax plus a reader-facing tax that compounds across the corpus. This skill is a read-only classifier: it surfaces candidates with treatment guidance; the author hand-applies every edit. + +## Noise shapes and treatments + +| Shape | What it looks like | Default tier | Treatment | +|---|---|---|---| +| `citation` — historical citations | Dated incident citations, inline provenance attribution, migration/rename narration ("Empirically observed 2026-…", "was renamed to", "we pivoted from") when the current form suffices | 1 | Relocate to a per-file `## Sources` / `## History` footer; strip when non-load-bearing (version control preserves history). Keep inline only when the date is load-bearing (methodology or freshness stamp) | +| `ghost-ref` — ephemeral working-directory refs | Concrete paths into ephemeral working directories (for example a `.work//` slice convention) cited from durable surfaces — the ref breaks when the working directory is retired | 2 | 3-way classify: promote the content to a durable home, replace with a commit-SHA permalink, or strip. Slot-variable forms (`` as a schema placeholder, not a literal name) are NOT ghost refs | +| `preamble` — "Why this file exists" openers | Opening section explaining motivation/history/rationale | 2 | Diataxis classify: KEEP on Explanation-quadrant files (rule bodies, ADRs, convention rationale); STRIP on Reference-quadrant files (data tables, registries, cheat-sheets), replacing with a 1-sentence orientation | +| `enum-list` — hard-coupled consumer lists | Tables/lists hardcoding N specific consumers that drift on every add/remove ("the following five skills…", bulleted `/skill — role` rosters) | 1 | Replace with a runtime derivation (a grep/list command cited inline) or a category citation; hardcode only when both fail | +| `scope-meta` — scope/loading meta-commentary | Body prose restating loading mechanics that config/frontmatter already owns ("Path-scoped to X", "Loads on Read of Y", "Auto-loads when…") | 1 | Strip the clause — the frontmatter/config is the single source of truth; keep a genuine cross-ref riding the same sentence. Files with no scoping frontmatter MAY state scope in one sentence | + +Consumers with their own ephemeral-path or noise conventions can refine these defaults in their repo's `CLAUDE.md` / rules; the classifier's shapes and tiers above are the skill's built-in baseline. + +## Action router + +| Action | Args | Behavior | +|---|---|---| +| `` (default, no action keyword) | empty → uncommitted `.md` files from git; file path → single-file; dir path → batch | run `bash "${CLAUDE_SKILL_DIR}/scripts/detect.sh"` on targets; map the emitted facts to the per-file tier table using the treatments above | +| `audit [target]` | same target rules | explicit form of the default; same behavior | + +Single action v1; `relocate` and `generalize` actions are deferred until real demand surfaces — author hand-edits driven by audit output cover the sweep workflow. + +## Auto-detect default + +1. Empty arg AND clean tree → friendly no-op exit 0 ("No uncommitted .md files. Pass file/dir target.") +2. Empty arg AND uncommitted `.md` files → batch audit over those files +3. Single file path → single-file audit +4. Directory path → batch audit (filenames sorted lexically for deterministic output) +5. First positional == `audit` → audit on rest (explicit form) + +## Hard rules + +- **Read-only.** No `Edit`, no `Write`, no mutating `Bash` ops. The author owns every treatment edit. +- **Tier semantics.** Tier 1 = definite noise; Tier 2 = review needed; Tier 3 = likely legitimate (surfaced for awareness). +- **Section EXEMPTIONS never flagged:** `## Recheck triggers`, `## Cross-references`, `## Sources` / `## History` / `## External authority` footers, ADR amendment blocks, `CHANGELOG.md` entries and release notes, frontmatter. +- **Opt-out markers respected.** `` (covers the next paragraph) and `` (next line) skip the wrapped content. +- **Slot-variable refs NOT flagged as ghost refs.** A `` / `` / `` token is a schema placeholder, not a literal path. +- **Output deterministic.** Filenames sort lexically; per-file tier rows sort by line number; no timestamps in output. +- **Default action is the audit action** — `/declutter ` is identical to `/declutter audit `. + +## Output schema + +Per target file: + +```text +: N finding(s) — T1=, T2=, T3= + +| Tier | Shape | Line | Excerpt | Treatment | +|------|-------|------|---------|-----------| +| 1 | citation | 42 | "Empirically observed 2026-..." | Relocate to a ## Sources / ## History footer | +| 2 | ghost-ref | 87 | ".work/foo-slice/PLAN.md cites..." | 3-way classify (promote / SHA-permalink / strip) | +| 2 | preamble | 7 | "## Why this file exists" | Diataxis classify (KEEP if Explanation; STRIP if Reference) | +| 3 | preamble | 1 | (top-of-file orientation paragraph) | Likely legitimate; surfaced for awareness | +``` + +Batch aggregate at end: + +```text +Total: file(s) audited, Tier 1, Tier 2, Tier 3 findings. +``` + +`shape` values: `citation`, `ghost-ref`, `preamble`, `enum-list`, `scope-meta`. + +## What this skill is NOT + +- **Not `/compress`.** The sibling `/compress` owns FLAVOR (filler, hedging, articles, redundant restatement); `/declutter` owns NOISE (the five shapes above). Different concerns; both may apply to the same target iteratively. +- **Not a markdown linter.** Structural GFM conventions belong to the repo's markdown linter (e.g. markdownlint-cli2); `/declutter` is semantic noise classification. +- **Not an Edit operation.** Read-only: it surfaces findings; the author applies treatments. +- **Not a content deduplicator.** When the noise is the same concept repeated across 3+ files, that is the sibling `/extract-ssot`'s territory. + +## Sources + +- [Diataxis Explanation](https://diataxis.fr/explanation/) — the Diataxis classifier behind the preamble treatment +- [markdownlint configuration](https://github.com/DavidAnson/markdownlint?tab=readme-ov-file#configuration) — opt-out marker HTML-comment form precedent diff --git a/plugins/docs-hygiene/skills/declutter/scripts/detect.sh b/plugins/docs-hygiene/skills/declutter/scripts/detect.sh new file mode 100755 index 000000000..828db1551 --- /dev/null +++ b/plugins/docs-hygiene/skills/declutter/scripts/detect.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# Noise findings for /declutter. Read-only. +# +# Output: File, Finding tier/shape/line/excerpt; Summary lines. +# Exit: always 0 on audit paths — a read-only audit must never fail the caller, +# so -e is omitted; 2 on unknown arguments. +set -u + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/noise-shapes.sh +source "$SCRIPT_DIR/lib/noise-shapes.sh" + +PATHS_FILE="" +TARGETS=() + +usage() { + cat <<'EOF' +detect.sh — emit markdown noise findings for /declutter. + +Usage: + detect.sh ... + detect.sh --paths-file + detect.sh --help + +When no paths are given, audits the uncommitted .md files of the repository +it runs in (from git status). Exit: 0 on audit, 2 on unknown arguments. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --paths-file) + PATHS_FILE="${2:-}" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + --) + shift + while [[ $# -gt 0 ]]; do + TARGETS+=("$1") + shift + done + ;; + -*) + echo "detect.sh: unknown arg '$1'" >&2 + exit 2 + ;; + *) + TARGETS+=("$1") + shift + ;; + esac +done + +repo_root="$(git rev-parse --show-toplevel 2>/dev/null | tr -d '\r')" +if [[ -n "$repo_root" ]]; then + cd "$repo_root" 2>/dev/null || true +fi + +if [[ ${#TARGETS[@]} -eq 0 ]]; then + if [[ -n "$PATHS_FILE" ]]; then + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line//$'\r'/}" + [[ -z "$line" ]] && continue + TARGETS+=("$line") + done <"$PATHS_FILE" + elif [[ -n "$repo_root" ]]; then + # Uncommitted .md files: modified/added/renamed/untracked, per git status. + while IFS= read -r line; do + line="${line//$'\r'/}" + [[ -z "$line" ]] && continue + TARGETS+=("$line") + done < <(git status --porcelain 2>/dev/null | awk '/\.md$/ {print $NF}') + fi +fi + +# Expand directory targets to the .md files inside them (recursive), so +# `detect.sh ` batch-audits instead of silently skipping non-files. +EXPANDED=() +for target in ${TARGETS[@]+"${TARGETS[@]}"}; do + if [[ -d "$target" ]]; then + while IFS= read -r md; do + EXPANDED+=("$md") + done < <(find "$target" -type f -name '*.md' 2>/dev/null) + else + EXPANDED+=("$target") + fi +done +TARGETS=(${EXPANDED[@]+"${EXPANDED[@]}"}) + +if [[ ${#TARGETS[@]} -eq 0 ]]; then + echo "Summary total: files=0 T1=0 T2=0 T3=0" + echo "Note: no markdown targets — pass file paths or edit some .md files" + exit 0 +fi + +mapfile -t SORTED < <(printf '%s\n' "${TARGETS[@]}" | LC_ALL=C sort -u) + +total_t1=0 total_t2=0 total_t3=0 files_audited=0 + +audit_file() { + local file="$1" + [[ -f "$file" ]] || return 0 + files_audited=$((files_audited + 1)) + + local t1=0 t2=0 t3=0 + local in_exempt=0 current_section="" prev_line="" line_num=0 shapes shape tier excerpt + + while IFS= read -r line || [[ -n "$line" ]]; do + line_num=$((line_num + 1)) + if [[ "$line" =~ ^##[[:space:]]+ ]]; then + current_section="${line#'## '}" + current_section="${current_section%%$'\r'*}" + if declutter_section_exempt "$current_section"; then + in_exempt=1 + else + in_exempt=0 + fi + fi + if [[ $in_exempt -eq 1 ]] || declutter_line_skipped "$prev_line" "$line"; then + prev_line="$line" + continue + fi + shapes="$(declutter_detect_shapes "$line" || true)" + if [[ -n "$shapes" ]]; then + excerpt="$(declutter_trim_excerpt "$line")" + while IFS= read -r shape; do + [[ -z "$shape" ]] && continue + tier="$(declutter_shape_tier "$shape")" + printf 'File: %s\n' "$file" + printf 'Finding tier: %s\n' "$tier" + printf 'Finding shape: %s\n' "$shape" + printf 'Finding line: %s\n' "$line_num" + printf 'Finding excerpt: %s\n' "$excerpt" + printf '%s\n' '---' + case "$tier" in + 1) t1=$((t1 + 1)) total_t1=$((total_t1 + 1)) ;; + 2) t2=$((t2 + 1)) total_t2=$((total_t2 + 1)) ;; + *) t3=$((t3 + 1)) total_t3=$((total_t3 + 1)) ;; + esac + done <<<"$shapes" + fi + prev_line="$line" + done <"$file" + + printf 'Summary file: %s | T1=%s T2=%s T3=%s\n' "$file" "$t1" "$t2" "$t3" +} + +for file in "${SORTED[@]}"; do + audit_file "$file" +done + +printf 'Summary total: files=%s T1=%s T2=%s T3=%s\n' "$files_audited" "$total_t1" "$total_t2" "$total_t3" +exit 0 diff --git a/plugins/docs-hygiene/skills/declutter/scripts/detect.test.sh b/plugins/docs-hygiene/skills/declutter/scripts/detect.test.sh new file mode 100755 index 000000000..d97b4a6b3 --- /dev/null +++ b/plugins/docs-hygiene/skills/declutter/scripts/detect.test.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# Self-contained tests for detect.sh (no external test lib — ships with the +# plugin; fixtures are built inline in a tmpdir). +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DETECT="$SCRIPT_DIR/detect.sh" +TEST_TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TEST_TMPDIR"' EXIT + +FAILED=0 +CASE_NUM=0 + +pass() { + CASE_NUM=$((CASE_NUM + 1)) + printf 'PASS: %s\n' "$1" +} +fail() { + CASE_NUM=$((CASE_NUM + 1)) + FAILED=$((FAILED + 1)) + printf 'FAIL: %s\n expected: %s\n actual: %s\n' "$1" "$2" "$3" >&2 +} +assert_exit() { + if [[ "$2" == "$3" ]]; then pass "$1"; else fail "$1" "exit $2" "exit $3"; fi +} +assert_contains() { + case "$2" in + *"$3"*) pass "$1" ;; + *) fail "$1" "contains: $3" "$2" ;; + esac +} +assert_not_contains() { + case "$2" in + *"$3"*) fail "$1" "absent: $3" "present" ;; + *) pass "$1" ;; + esac +} + +# --- Fixtures (built inline; no shipped fixture files) --------------------------- + +ALL_SHAPES="$TEST_TMPDIR/all-shapes.md" +cat >"$ALL_SHAPES" <<'EOF' +# Fixture: every shape + +The plan lives at .work/foo-slice/PLAN.md for this effort. + +## Why this file exists + +Empirically observed 2026-01-01 during the rollout. + +The following five skills consume this rule. + +- `/skill-a` — does one thing +- `/skill-b` — does another + +Path-scoped to `src/**` so it loads there. +EOF + +CLEAN="$TEST_TMPDIR/clean.md" +cat >"$CLEAN" <<'EOF' +# Clean fixture + +Plain prose with no noise shapes. The schema uses .work//PLAN.md as a +slot-variable example, which is not a ghost ref. + +## Cross-references + +Was renamed to something — exempt section, never flagged. +EOF + +OPTOUT="$TEST_TMPDIR/legit-optouts.md" +cat >"$OPTOUT" <<'EOF' +# Opt-out fixture + + +Empirically observed during the bar-rollout window. + + +We pivoted from the 2026-05-01 incident layout. + +Path-scoped to `docs/**` per the loader. +EOF + +# --- 1. --help contract ----------------------------------------------------------- + +help_exit=0 +bash "$DETECT" --help >/dev/null 2>&1 || help_exit=$? +assert_exit "--help exits 0" 0 "$help_exit" + +unknown_exit=0 +bash "$DETECT" --bogus >/dev/null 2>&1 || unknown_exit=$? +assert_exit "unknown flag exits 2" 2 "$unknown_exit" + +# --- 2. All shapes detected -------------------------------------------------------- + +out="$(bash "$DETECT" "$ALL_SHAPES")" +assert_contains "ghost-ref finding" "$out" "Finding shape: ghost-ref" +assert_contains "citation finding" "$out" "Finding shape: citation" +assert_contains "enum-list finding" "$out" "Finding shape: enum-list" +assert_contains "scope-meta finding" "$out" "Finding shape: scope-meta" +assert_contains "preamble finding" "$out" "Finding shape: preamble" +assert_contains "preamble tier 2" "$out" "Finding tier: 2" +assert_contains "file summary" "$out" "Summary file: $ALL_SHAPES" + +# --- 3. Clean fixture: slot-variable not flagged; exempt section skipped ----------- + +clean_out="$(bash "$DETECT" "$CLEAN")" +assert_contains "clean summary present" "$clean_out" "Summary total:" +assert_not_contains "slot-variable not a ghost ref" "$clean_out" "Finding shape: ghost-ref" +assert_not_contains "exempt section suppressed" "$clean_out" "Finding shape: citation" + +# --- 4. Opt-out markers suppress wrapped content ------------------------------------ + +opt_out="$(bash "$DETECT" "$OPTOUT")" +assert_not_contains "opt-out citation suppressed" "$opt_out" "bar-rollout" +assert_not_contains "opt-out line suppressed" "$opt_out" "2026-05-01 incident" +assert_contains "scope-meta still detected" "$opt_out" "Finding shape: scope-meta" + +# --- 5. Backtick-wrapped slash-command roster detected ------------------------------ + +BT_FIXTURE="$TEST_TMPDIR/bt.md" +cat >"$BT_FIXTURE" <<'EOF' +- `/skill-z` — backtick-wrapped slash command +EOF +bt_out="$(bash "$DETECT" "$BT_FIXTURE")" +assert_contains "backtick enum-list detected" "$bt_out" "Finding shape: enum-list" + +# --- 6. Directory target expands to its .md files ------------------------------------ + +DIR_FIXTURE="$TEST_TMPDIR/dir-target/nested" +mkdir -p "$DIR_FIXTURE" +cp "$ALL_SHAPES" "$DIR_FIXTURE/inner.md" +dir_out="$(bash "$DETECT" "$TEST_TMPDIR/dir-target")" +assert_contains "directory target audits nested .md" "$dir_out" "Summary file: $DIR_FIXTURE/inner.md" +assert_contains "directory target finds shapes" "$dir_out" "Finding shape: ghost-ref" + +# --- 7. --paths-file input ---------------------------------------------------------- + +PATHS="$TEST_TMPDIR/paths.txt" +printf '%s\n' "$CLEAN" >"$PATHS" +pf_out="$(bash "$DETECT" --paths-file "$PATHS")" +assert_contains "paths-file target audited" "$pf_out" "Summary file: $CLEAN" + +# --- Final report -------------------------------------------------------------------- + +if [[ "$FAILED" -eq 0 ]]; then + printf '\nAll %d checks passed.\n' "$CASE_NUM" + exit 0 +fi +printf '\n%d/%d checks failed.\n' "$FAILED" "$CASE_NUM" >&2 +exit 1 diff --git a/plugins/docs-hygiene/skills/declutter/scripts/lib/noise-shapes.sh b/plugins/docs-hygiene/skills/declutter/scripts/lib/noise-shapes.sh new file mode 100644 index 000000000..98f93bdf9 --- /dev/null +++ b/plugins/docs-hygiene/skills/declutter/scripts/lib/noise-shapes.sh @@ -0,0 +1,83 @@ +# shellcheck shell=bash +# Shared noise-shape detectors for /declutter (sourceable; not invoked directly). +# Shape definitions and treatments: the skill's SKILL.md "Noise shapes and treatments". + +declutter_trim_excerpt() { + local line="$1" + line="${line//$'\r'/}" + line="${line#"${line%%[![:space:]]*}"}" + if ((${#line} > 120)); then + line="${line:0:117}..." + fi + printf '%s' "$line" +} + +declutter_is_slot_work_ref() { + local line="$1" + [[ "$line" == *'.work/'* || "$line" == *'.work/'* || "$line" == *'.work/'* ]] +} + +declutter_line_skipped() { + local prev="$1" line="$2" + [[ "$prev" == *'markdown-discipline-ignore'* ]] && return 0 + [[ "$line" == *'markdown-discipline-ignore'* ]] && return 0 + return 1 +} + +# Emit zero or more shape names (one per line on stdout). +declutter_detect_shapes() { + local line="$1" + local found=0 + if ! declutter_is_slot_work_ref "$line"; then + if [[ "$line" =~ \.work/[a-z][a-z0-9_-]*/ ]]; then + printf '%s\n' 'ghost-ref' + found=1 + fi + fi + if [[ "$line" =~ ^##[[:space:]]+Why[[:space:]]+this[[:space:]]+file[[:space:]]+exists ]]; then + printf '%s\n' 'preamble' + found=1 + fi + if [[ "$line" =~ [Ee]mpirically[[:space:]]+observed ]] || + [[ "$line" =~ [Ww]e[[:space:]]+pivoted[[:space:]]+from ]] || + [[ "$line" =~ [Ww]as[[:space:]]+renamed[[:space:]]+to ]] || + [[ "$line" =~ [Pp]re-convention ]] || + [[ "$line" =~ [Ll]egacy[[:space:]]+layout ]]; then + printf '%s\n' 'citation' + found=1 + fi + if [[ "$line" =~ [Ff]ollowing[[:space:]]+(five|four|three|six|seven|eight|nine|ten|[0-9]+)[[:space:]]+(skills|consumers|agents|modules) ]]; then + printf '%s\n' 'enum-list' + found=1 + fi + if [[ "$line" =~ ^[[:space:]]*-[[:space:]]+\`?/[a-z][a-z0-9_-]*\`?[[:space:]]— ]]; then + printf '%s\n' 'enum-list' + found=1 + fi + if [[ "$line" =~ [Pp]ath-scoped[[:space:]]+to ]] || + [[ "$line" =~ [Ll]oads[[:space:]]+on[[:space:]]+[Rr]ead[[:space:]]+of ]] || + [[ "$line" =~ [Aa]uto-loads[[:space:]]+when ]]; then + printf '%s\n' 'scope-meta' + found=1 + fi + return "$found" +} + +declutter_shape_tier() { + local shape="$1" + case "$shape" in + ghost-ref | preamble) printf '2' ;; + citation | enum-list | scope-meta) printf '1' ;; + *) printf '3' ;; + esac +} + +declutter_section_exempt() { + local heading="$1" + case "$heading" in + "Recheck triggers" | "Cross-references" | "Sources" | "History" | "External authority") return 0 ;; + *) ;; + esac + [[ "$heading" == *"amendment"* ]] && return 0 + return 1 +} diff --git a/plugins/docs-hygiene/skills/encapsulation-audit/SKILL.md b/plugins/docs-hygiene/skills/encapsulation-audit/SKILL.md new file mode 100644 index 000000000..22fa68366 --- /dev/null +++ b/plugins/docs-hygiene/skills/encapsulation-audit/SKILL.md @@ -0,0 +1,184 @@ +--- +name: encapsulation-audit +description: "Audit and remediate skill-encapsulation violations — external citations reaching into private surfaces inside `.claude/skills//` beyond the slash invocation. Use when: 'audit encapsulation', 'find skill leaks', 'skill boundary violation', 'who is reaching into ', 'check skill boundaries', 'public API drift', or before refactoring a skill." +argument-hint: "[detect|fix|file-issues]" +user-invocable: true +disable-model-invocation: false +--- + +## Why this skill exists + +Skills have a public API and a private body — the contract is `context/public-surface-contract.md` (bundled with this skill). External consumers — rules, agents, other skills, prose docs — must cite the public surface only. Editing the private body must not break external consumers because no external consumer is allowed to depend on it. + +Without enforcement, private bodies leak. A rule file cites a path inside `//` because the content is convenient. The skill author refactors that subdir and the citation breaks silently — nothing fails the build, nothing greps red. The skill cannot evolve without coordinating with every leaker. + +This skill provides the detection + classification + remediation discipline that closes that gap. Distinct concern from `/extract-ssot` (which detects content duplication via Rule of Three). Encapsulation violations are **single-violation matters** — Rule of Three does not gate them. + +## Public surface matrix + +The **Skill** surface — public = frontmatter + documented actions + args/flags + `/skill-name` slash invocation + the `scripts/` entry surface; private = everything else inside `.claude/skills//` (any OTHER subdirectory, all `*.schema.json` at any depth, all heading anchors) — is defined by `context/public-surface-contract.md`, including the data-file and scripts/ entry-surface carve-outs and the rip-and-paste-portability rationale. This skill audits cites against two more authoring surfaces the contract file does not enumerate: + +| Surface | Public (cite externally) | Private | +|---------|--------------------------|---------| +| **Rule file** (`.claude/rules/*.md`) | All H2/H3 headings (entire file is shared vocabulary) | n/a | +| **Scheduled-automation prompt** (e.g. `.claude/routines/*.md`, if the consumer repo keeps them) | Top-of-file orchestration prompt | Internal exclusion lists, escape conditions | + +### CI / git-hook sharing — pick a technique, not an exception + +Workflows and git hooks needing logic that ALSO lives in a skill must not reach into skill internals. Pick a sharing technique by logic size — scripts/ facade (the skill exposes a public `scripts/.sh` entry delegating to a private backend; hooks/CI invoke it), intentional duplication, plugin packaging, or headless `claude -p '/skill '` invocation — per `context/public-surface-contract.md` "CI / git-hook consumption — entry surface, not internals". + +The choice is per-cite. The bundled detect script is this plugin's detector; any hard gate (pre-commit hook, CI job, drift comparison against a vendored copy) is whatever the consuming repo wires around it. + +## Action router + +| Argument | Action | Purpose | +|----------|--------|---------| +| *(empty)* / `detect` | Default | Run detection grep, classify legal vs illegal, output violation table | +| `fix :` | Targeted remediation | Interactive Path A (promote-out) vs Path B (route-via-`/`) for one hit | +| `file-issues` | Batch | File one tracking work item per illegal hit in the consumer's tracker (e.g. `gh issue create`); emit a checklist if no tracker is available | + +One action per response. + +## Detection + +`bash "${CLAUDE_SKILL_DIR}/scripts/detect.sh"` (`--apply-filters` optional) + +- Any path into a subdirectory under a skill (`//...` — regardless of subdir name) **except `scripts/`** (entry-surface carve-out, see below) +- Heading-anchor cites into `SKILL.md` body structure (`/SKILL.md#`) +- Any `*.schema.json` file at any depth (`/.schema.json`) + +Legal external cites: bare `/SKILL.md` path (discouraged but legal — natural-language + slash invocation is canonical), `/.json` data files at skill root (data-file carve-out), and `/scripts/` entry scripts (entry-surface carve-out below). + +**scripts/ entry-surface carve-out:** a skill's `scripts/` is its declared ENTRY surface per `context/public-surface-contract.md`. Harness / CI / hooks / workflow registries MAY path-cite a skill's entry scripts directly, so this inbound audit treats `/scripts/...` cites as legal (like data files and bare `SKILL.md`) and never flags them. The skill-to-skill half of the asymmetry — a sibling SKILL.md citing another skill's `scripts/` stays slash-only — is out of this inbound audit's scope; a consuming repo that wants it enforced wires its own outbound gate. + +The script scans the consumer repo it runs in: every `.claude/` child directory except `skills/` (self-citation domain; opt in via `--include-skills` for skill-maintenance review) and `worktrees/`, plus `.github/`, `docs/`, `.lefthook/`, root instruction/config files (`AGENTS.md`, `CLAUDE.md`, `README.md`, `CONTRIBUTING.md`, `lefthook.yml`), and `.claude/` top-level files — each filtered to what exists. + +Or invoke the skill (the agent applies the filter taxonomy below): + +```text +/encapsulation-audit detect +``` + +## Filter taxonomy — legal hits + +`detect.sh` output is broad without `--apply-filters`. Use the filter taxonomy below for KIND-1/2/3; `--apply-filters` drops self-citation, plugin cache, and worktree paths. + +| Filter | Hit shape | Why legal | +|--------|-----------|-----------| +| **Self-citation** | `.claude/skills//SKILL.md` cites `.claude/skills//` | Intra-skill progressive disclosure (per `context/public-surface-contract.md`) | +| **KIND-1 meta-prose** | A rule or doc describing the encapsulation contract itself, OR documenting skill internals as a worked example / historical narrative / empirically-verified quirk | Self-referential explanatory prose, not citation | +| **KIND-2 forced-cite** | Another tool's semantics structurally require a verbatim path (a path-scoped rule trigger, a watch-glob, a drift-gate comparing against a vendored copy) | Citation IS the structural contract | +| **KIND-3 self-test** | This skill's / a hook's own regression fixtures embed literal violation strings | Exercise the filter; not real citations | +| **Mirror-automation** | A scheduled-automation prompt (e.g. `.claude/routines/.md`) cites `.claude/skills//` when its basename matches the skill name | Such prompts are cron arms of slash skills; mirror cites are part of the skill contract per the Public surface matrix | +| **Glob-config** | Skill-internal path used as a glob/filter in an exclusion list, hooks allowlist, or `.gitignore` — not a content `Read` cite | Path is structural filter syntax, not progressive-disclosure content (overlaps KIND-2; named separately for grading) | +| **Plugin cache** | `~/.claude/plugins/cache//...` | Upstream territory; foreign contract | +| **Worktree path** | `//.claude/skills//...` (per the repo's worktree convention — e.g. `.worktrees/`, `.claude/worktrees/`, `.git/worktrees/`) | Worktrees share the tracked tree; same rules apply at the root path | + +Hits that survive ALL filters = illegal. Report. + +## Remediation paths + +### Path A — Promote content out (caller wants the data) + +Use when: the caller needs data the private file contains, and the data is genuinely shared vocabulary or constraint. + +1. Apply `/extract-ssot verify` (the 6-gate refuse-fast check) to the private file's content +2. If it passes: extract the content into a shared rule or convention doc outside the skill (e.g. `.claude/rules/.md`, or extend an existing one) +3. Migrate the violator AND the original skill body to cite the new doc by heading +4. Delete or shrink the private file +5. Run `/rename-references` to sweep all syntactic forms + +### Path B — Route via `/skill-name` invocation (caller wants the behavior) + +Use when: the caller wants the skill's behavior, not the data. A reference into the private body is a workaround because the skill has no public action covering the use case. + +1. Identify which skill action would deliver the desired behavior +2. If the action exists: rewrite the caller to invoke `/ ` +3. If the action does NOT exist: + - Surface the gap to the user as a side note + - File a tracking work item in the consumer's tracker for the missing public action + - Leave the violation in place with a `# TODO(encapsulation-audit): missing / ` marker + - Do NOT silently inline a workaround that makes the violation harder to find later + +## Refactor pass discipline + +When invoked from another skill's execute pass (e.g. `/extract-ssot execute`): + +| Step | Action | +|------|--------| +| 1 | Run the detection grep against the caller list (or repo-wide) | +| 2 | Classify each match via the filter taxonomy: legal hit vs violation | +| 3 | For each violation, choose Path A (promote) or Path B (route via `/name`) | +| 4 | DO NOT preserve a violation because "it works today" — broken-window pattern | +| 5 | If Path B is needed but the action is missing, file a tracking work item and STOP — don't ship a partial fix | +| 6 | Run a `/rename-references` sweep after edits | + +A surfaced violation is never silently ignored: fix it, capture it as a side note to the user, or file a tracking work item. + +## Output shape (detect mode) + +```markdown +# Encapsulation audit — N violations + +## Violations +| File | Line | Cite | Suggested path | +|------|------|------|----------------| +| ... | ... | `//` | A or B | + +## Filtered (legal — sample for verification) +| File | Line | Filter applied | +|------|------|----------------| + +## Summary +- Total raw hits: M +- Filtered legal: M-N +- Illegal violations: N +- Path A candidates: a +- Path B candidates: b +- Missing public action (Path B blocked): c + +## Recommended next step +`/encapsulation-audit fix :` for one-by-one OR `/encapsulation-audit file-issues` for batch work-item filing +``` + +## Anti-patterns guarded + +- **Single-violation tolerance** — "it's only one place, no big deal." Encapsulation rot accumulates one violation at a time. Each is a binary contract break, not a Rule of Three threshold. +- **Silent workaround** — the caller inlines the data instead of routing through the skill, hiding the violation from future audits. Mitigated by the `# TODO(encapsulation-audit)` marker requirement. +- **Filter laziness** — flagging every grep hit without applying the filter taxonomy. False positives erode trust in the audit; users stop running it. +- **Wrong-direction Path A** — promoting skill-internal content to a shared doc when the caller actually wanted skill behavior. Mitigated by the "data vs behavior" classification step in Path A vs Path B selection. + +## Sanity checks + +| When | Check | Evidence | +|------|-------|----------| +| Pre-detection | `git ls-files` enumerable in scope | Bash output | +| Post-detection | Each raw hit classified per filter taxonomy OR illegal | Diff against filter table | +| Pre-fix | Path A vs Path B chosen with explicit data-vs-behavior reasoning | Session notes or plan artifact | +| Post-fix | Detection grep re-run; violation count decreased | Bash output | +| Post-fix | `/rename-references` sweep ran | Tool output | + +## What this skill does NOT do + +- Detect content duplication / Rule of Three clusters → `/extract-ssot` +- Enforce code-side public/private (TypeScript `export`, .NET `internal`, Python `_prefix`) → language-level tooling (compiler, lint) +- Refactor skill bodies that ARE legitimately self-contained (no external violations) → general refactoring, out of scope +- File work items without classification — Path A vs Path B must be picked first +- Auto-fix without user review — each Path B route changes call-site invocation; the user inspects the diff + +## Cross-references + +- `context/public-surface-contract.md` — canonical definition of what's public vs private, both carve-outs, the violation-shape table, and the CI / git-hook consumption techniques +- `/extract-ssot verify` — 6-gate refuse-fast check for Path A "promote out" decisions +- `/extract-ssot execute` — writes the SSOT and sweeps citations after a Path A migration +- `/extract-ssot` — duplication-skill counterpart; an encapsulation violation is one of its failure modes (cross-cite) +- `/rename-references` — load-bearing multi-pattern sweep after any heading change + +## Recheck triggers + +| Condition | Action | +|-----------|--------| +| `context/public-surface-contract.md` changes | Re-sync the Public surface matrix here and the pattern comments in `scripts/detect.sh` | +| A new CI workflow or git hook needs skill logic | Pick a technique from `context/public-surface-contract.md` "CI / git-hook consumption — entry surface, not internals"; prefer a skill `scripts/` facade the hook invokes — no vendored copy | +| Violations recur across audits | Promote the `detect` action to a scheduled cadence in the consumer repo (e.g. `/loop` or `/schedule`) | +| Anthropic ships a native skill-boundary linter | Demote this skill to advisory or sunset it | diff --git a/plugins/docs-hygiene/skills/encapsulation-audit/context/public-surface-contract.md b/plugins/docs-hygiene/skills/encapsulation-audit/context/public-surface-contract.md new file mode 100644 index 000000000..b8628e3db --- /dev/null +++ b/plugins/docs-hygiene/skills/encapsulation-audit/context/public-surface-contract.md @@ -0,0 +1,70 @@ +# Skill public-surface contract + +The contract this skill audits against, applicable to any repo with `.claude/skills/`. The bundled `scripts/detect.sh` encodes it mechanically; this file is the reasoning source the agent applies when classifying and remediating hits. A consuming repo may layer its own conventions on top, but the surfaces and carve-outs below are what the detector implements. + +Upstream anchors (cited by URL, not recapped) — Anthropic publishes no formal public-surface contract for skills; this is a stricter discipline consistent with the documented progressive-disclosure model: + +- +- +- +- + +## Public surface + +The public surface of a skill is ONLY: + +1. YAML frontmatter (the documented fields) +2. Documented actions — named action + arg shape + output shape declared in the SKILL.md body +3. Arguments and flags +4. The `/skill-name` slash invocation (`/plugin:skill-name` for plugin-packaged skills) + +External consumers — rules, agents, other skills, prose docs, ADRs, READMEs, CI configs — describe WHAT they want done in natural language (`/skill-name `), and the skill body decides HOW: which scripts to call, which schemas to validate against, which reference files to load. + +## Private — everything else + +Anything inside `.claude/skills//` beyond the public surface is private: all files, all subdirectories regardless of name (`context/`, `reference/`, `actions/`, `evals/`, `templates/`, or any other author-chosen name), all `*.schema.json` files at any depth, and all heading anchors inside `SKILL.md` or its supporting files. Skill authors may rename, refactor, split, or merge any private surface without breaking external consumers because no external consumer may depend on it. + +This guarantees skills are rip-and-paste portable: moving `.claude/skills//` into another repo carries every implementation detail with it; nothing outside the skill depends on internal layout. Caveat: CI / hook / registry consumers of the entry surface (carve-out below) re-point on rip. + +## Carve-out — `scripts/` entry surface + +A skill's `scripts/` directory is its declared entry surface. Harness surfaces, CI workflows, git hooks, and automation registries MAY path-cite `scripts/` entry scripts directly. **Sibling skills may NOT** — skill-to-skill stays slash-only. That outbound half of the asymmetry is out of scope for this inbound audit; a consuming repo that wants it enforced wires its own outbound gate. + +A skill MAY expose a `scripts/.sh` entry as a declared public facade (delegating to a private backend directory) that hooks/CI invoke directly — the encapsulation-respecting alternative to vendoring a copy of the logic. A meta-tooling consumer that only READS a skill-internal path as data — a version-drift gate reading a pinned-version file, a path-scoped trigger naming the file it watches — cites that path under the KIND-2 forced-cite exemption (see the filter taxonomy in SKILL.md): it names a path structurally, it does not invoke skill logic. Logic invocation goes through the facade; data/path reference is KIND-2. + +## Carve-out — data files at skill root + +Plain data files at skill root (`/.json` or a `/.md` data table, NOT `*.schema.json`) are a documented exception to "everything inside is private": they are legal external cites. The data file is the canonical single source the skill reads at runtime, and a vendored copy would race the skill's writer. Schema files (`*.schema.json`) stay private — route via `/skill-name ` or vendor the schema to a shared tooling location the consumer repo owns. + +## Cite by slash invocation, never by path or heading anchor + +External citations into skill internals fragment the contract — when skill authors refactor, every external citation breaks silently because nothing enforces the link. + +| Violation shape | Fix | +|-----------------|-----| +| Cite to any path inside `.claude/skills//` from a rule / agent / doc / prose | Replace with `/skill-name ` natural-language invocation. The skill body chooses which internal file / script / schema to use. Add the action if missing | +| Cite to `.claude/skills//SKILL.md#some-heading` from outside | Heading anchors are body structure (private). Replace with `/skill-name ` invocation | +| Cite to `/.schema.json` from outside | Replace with `/skill-name ` (the action validates internally). Schema location is implementation detail | +| Cite to `//.md` where the content is genuinely cross-cutting shared vocabulary or constraint | **Path A — promote.** Move the content to a shared rule or convention doc outside the skill (e.g. `.claude/rules/.md`); consumers cite the new location | +| Cite to `//.md` where the content belongs to the skill | **Path B — route.** Replace the external reference with a `/skill-name` invocation, or `/skill-name ` if a matching action exists | + +When promoting (Path A), leave the original file in place if the skill still consumes it; the promoted doc becomes the single source of truth and the skill's internals reference it. Don't dual-maintain the same body in both locations. + +## CI / git-hook consumption — entry surface, not internals + +Workflows and git hooks needing logic that ALSO lives in a skill consume the skill's `scripts/` entry surface directly per the entry-surface carve-out. The registry (workflow YAML, hook config) holds a pointer to the entry script or a thin protocol adapter delegating to it — never a reach into `lib/` or any other private subdirectory. + +| Need | Technique | +|---|---| +| Skill logic from CI / hooks / automation registries | **Direct entry-surface consumption** — path-cite the skill's `scripts/` entry script, or a thin protocol adapter delegating to it | +| A few lines, not worth an entry script | **Intentional duplication** — the skill has its version; the hook has its own. Audit alignment via a test; a comment names the duplicate | +| Mature, repo-external reuse | **Plugin packaging** — graduate the skill to a plugin; the manifest declares interfaces | +| LLM-shaped CI work (not a mechanical gate) | **Headless invocation** — CI runs `claude -p '/skill '`. Reserve for non-mechanical work | + +The choice is per-cite. Enforcement split: the bundled `scripts/detect.sh` is the detector this plugin ships; any hard gate (pre-commit hook, CI job, drift comparison) is whatever the consuming repo wires around it. + +## What this contract does NOT cover + +- **Self-citation** inside a skill's own files (`.claude/skills//SKILL.md` citing `.claude/skills//context/.md`) is LEGAL and expected — progressive disclosure depends on it. +- **Plugin-cache citations** (`~/.claude/plugins/cache//...`) — plugin internals are upstream territory; treat by the upstream's contract, not this one. +- **Worktree citations** — worktrees share the same `.claude/skills/` tree as the main checkout; the same rules apply at the root path. diff --git a/plugins/docs-hygiene/skills/encapsulation-audit/scripts/detect.sh b/plugins/docs-hygiene/skills/encapsulation-audit/scripts/detect.sh new file mode 100755 index 000000000..3b4fe7c1a --- /dev/null +++ b/plugins/docs-hygiene/skills/encapsulation-audit/scripts/detect.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# Find external citations into private skill internals (.claude/skills//...). +# +# Contract this detector encodes: ../context/public-surface-contract.md +# (bundled with this skill). +# Scan root: the git repository the script runs in (the consumer repo), or the +# current directory when outside a git repo. +# Output TSV: file, line, match-text. Use --apply-filters to drop known legal hits. +# Exit: 0 clean, 1 violations, 2 environment error. +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null | tr -d '\r' || pwd)" +cd "$REPO_ROOT" + +INCLUDE_SKILLS=0 +APPLY_FILTERS=0 +for arg in "$@"; do + case "$arg" in + --include-skills) INCLUDE_SKILLS=1 ;; + --apply-filters) APPLY_FILTERS=1 ;; + --help | -h) + cat <<'USAGE' +Usage: encapsulation-audit detect [--include-skills] [--apply-filters] + +Find external citations into private skill internals. +Output TSV: file, line, match-text. +Exit: 0 clean, 1 violations, 2 environment error. + + --include-skills Include .claude/skills/ in scope (intra-skill self-citation review) + --apply-filters Drop self-citation, plugin cache, and worktree path hits +USAGE + exit 0 + ;; + *) + echo "encapsulation-audit/detect: unknown arg '$arg'" >&2 + exit 2 + ;; + esac +done + +# In-scope authoring surfaces, generalized for any consumer repo and filtered +# to what exists (absent surfaces are skipped silently): +# - every .claude/ child directory EXCEPT skills/ (intra-skill self-citation +# is legal; opt back in via --include-skills) and worktrees/ (worktrees +# share the tracked tree; the same rules apply at the root path) +# - .github/ (workflows), docs/, .lefthook/ (git-hook scripts) +# - root instruction/config files plus .claude/ top-level files +# CI/hook surfaces stay IN-SCOPE for hit detection; the scripts/ entry-surface +# carve-out and the filter taxonomy decide legality downstream, not exclusion. +SCOPE_DIRS=() +for d in .claude/*/; do + d="${d%/}" + [[ -d "$d" ]] || continue + if [[ "$d" == ".claude/skills" && "$INCLUDE_SKILLS" -eq 0 ]]; then + continue + fi + if [[ "$d" == ".claude/worktrees" ]]; then + continue + fi + SCOPE_DIRS+=("$d") +done +for d in .github docs .lefthook; do + [[ -d "$d" ]] && SCOPE_DIRS+=("$d") +done + +SCOPE_FILES=() +for f in AGENTS.md CLAUDE.md README.md CONTRIBUTING.md lefthook.yml \ + .claude/*.md .claude/*.json .claude/*.yml .claude/*.yaml; do + [[ -f "$f" ]] && SCOPE_FILES+=("$f") +done + +if [[ ${#SCOPE_DIRS[@]} -eq 0 && ${#SCOPE_FILES[@]} -eq 0 ]]; then + echo "encapsulation-audit: no in-scope dirs or files present" >&2 + exit 2 +fi + +# Private-surface pattern per ../context/public-surface-contract.md: any +# subdir under a skill root is private; *.schema.json at any depth is +# private; /SKILL.md# heading-anchor cites are private. +# Three alternations (BRE form, escaped): # spellchecker:disable-line +# 1. `//` — any kebab-style subdir name +# 2. `/SKILL.md#` — heading-anchor cites +# 3. `/.schema.json` — schema files at any depth +# Subdir char class `[a-z][a-z0-9_-]+` matches kebab-style names and +# excludes `SKILL.md` (uppercase) so bare `/SKILL.md` path cites +# pass (discouraged-but-legal). Does NOT match plain-JSON data files at +# skill root (`/catalog.json`) per the data-file carve-out. +PATTERN='\.claude/skills/[a-z][a-z0-9-]\+/[a-z][a-z0-9_-]\+/\|\.claude/skills/[a-z][a-z0-9-]\+/SKILL\.md#\|\.claude/skills/[a-z][a-z0-9-]\+/[^/]\+\.schema\.json' + +# scripts/ entry-surface carve-out: a skill's `scripts/` is its declared ENTRY +# surface — harness / CI / hooks / workflow registries MAY path-cite it. Like +# the data-file and bare-SKILL.md public surfaces, scripts/ cites are never +# private, so they are dropped here rather than emitted as raw hits. PATTERN +# still matches scripts/ (the subdir alternation; BRE has no lookahead to # spellchecker:disable-line +# exclude one name), so the carve-out is a post-grep exclusion. The scan grep +# below uses `-o` to emit ONE record per cite, so this `grep -vE` drops only +# the scripts/ cite — a line co-citing a scripts/ entry script AND another +# skill's private subdir keeps the genuine cite instead of dropping the whole +# line. The skill-to-skill half of the asymmetry (a sibling SKILL.md citing +# another skill's scripts/ stays slash-only) is out of this inbound audit's +# scope — see the contract file. +SCRIPTS_RE='\.claude/skills/[a-z][a-z0-9-]+/scripts/' + +HITS_FILE="$(mktemp)" +trap 'rm -f "$HITS_FILE"' EXIT + +# Aggregate hits, then drop scripts/ entry-surface cites (carve-out above). +{ + if [[ ${#SCOPE_DIRS[@]} -gt 0 ]]; then + grep -rno "$PATTERN" \ + --include='*.md' --include='*.sh' --include='*.json' \ + --include='*.yml' --include='*.yaml' \ + "${SCOPE_DIRS[@]}" 2>/dev/null || true + fi + if [[ ${#SCOPE_FILES[@]} -gt 0 ]]; then + # -H forces the file prefix even when only one file exists. + grep -Hno "$PATTERN" "${SCOPE_FILES[@]}" 2>/dev/null || true + fi +} | grep -vE "$SCRIPTS_RE" >"$HITS_FILE" || true + +if [[ ! -s "$HITS_FILE" ]]; then + if [[ "$APPLY_FILTERS" -eq 1 ]]; then + printf 'Summary: raw=0 legal=0 illegal=0\n' >&2 + fi + exit 0 +fi + +# Convert grep "file:line:match" → "filelinematch". The `-o` scan +# emits only the matched path text, which contains no colons, so colon +# splitting is unambiguous. Under --apply-filters, drop the known-legal hit +# shapes: self-citation (a skill citing its own internals), plugin cache +# (upstream territory), and worktree paths (shared tree; same rules apply at +# the root path). +raw=0 +illegal=0 +while IFS= read -r line; do + [[ -z "$line" ]] && continue + raw=$((raw + 1)) + file="${line%%:*}" + rest="${line#*:}" + line_no="${rest%%:*}" + text="${rest#*:}" + if [[ "$APPLY_FILTERS" -eq 1 ]]; then + legal=0 + if [[ "$file" =~ ^\.claude/skills/([^/]+)/ ]] && + [[ "$text" == *".claude/skills/${BASH_REMATCH[1]}/"* ]]; then + legal=1 + elif [[ "$text" == *"plugins/cache/"* ]]; then + legal=1 + elif [[ "$file" == *".worktrees/"* || "$text" == *".worktrees/"* ]]; then + legal=1 + elif [[ "$file" == *".claude/worktrees/"* || "$text" == *".claude/worktrees/"* ]]; then + legal=1 + elif [[ "$file" == *".git/worktrees/"* || "$text" == *".git/worktrees/"* ]]; then + legal=1 + fi + if [[ "$legal" -eq 1 ]]; then + continue + fi + fi + illegal=$((illegal + 1)) + printf '%s\t%s\t%s\n' "$file" "$line_no" "$text" +done <"$HITS_FILE" + +if [[ "$APPLY_FILTERS" -eq 1 ]]; then + legal=$((raw - illegal)) + printf 'Summary: raw=%s legal=%s illegal=%s\n' "$raw" "$legal" "$illegal" >&2 +fi + +if [[ "$illegal" -eq 0 ]]; then + exit 0 +fi +exit 1 diff --git a/plugins/docs-hygiene/skills/encapsulation-audit/scripts/detect.test.sh b/plugins/docs-hygiene/skills/encapsulation-audit/scripts/detect.test.sh new file mode 100755 index 000000000..a66637002 --- /dev/null +++ b/plugins/docs-hygiene/skills/encapsulation-audit/scripts/detect.test.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# Regression tests for detect.sh (self-contained — no external test lib). +# +# detect.sh resolves its scan root via `git rev-parse --show-toplevel`, so +# every scan case runs against a deterministic throwaway fixture repo. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT="$SCRIPT_DIR/detect.sh" + +FAILED=0 +CASE_NUM=0 + +pass() { + CASE_NUM=$((CASE_NUM + 1)) + printf 'PASS: %s\n' "$1" +} +fail() { + CASE_NUM=$((CASE_NUM + 1)) + FAILED=$((FAILED + 1)) + printf 'FAIL: %s\n expected: %s\n actual: %s\n' "$1" "$2" "$3" >&2 +} +assert_exit() { + if [[ "$2" == "$3" ]]; then + pass "$1" + else + fail "$1" "exit $2" "exit $3" + fi +} +assert_contains() { + case "$2" in + *"$3"*) pass "$1" ;; + *) fail "$1" "contains: $3" "$2" ;; + esac +} +assert_not_contains() { + case "$2" in + *"$3"*) fail "$1" "does not contain: $3" "$2" ;; + *) pass "$1" ;; + esac +} +assert_silent() { + if [[ -z "$2" ]]; then + pass "$1" + else + fail "$1" "empty output" "$2" + fi +} + +FIXTURE_REPOS=() +cleanup_fixtures() { + local r + for r in "${FIXTURE_REPOS[@]:-}"; do + [[ -n "$r" ]] && rm -rf "$r" + done +} +trap cleanup_fixtures EXIT + +# Build a throwaway git repo carrying one file with the given content, and +# echo its path. detect.sh scans the working tree of whatever repo it runs in. +fixture_repo() { + local file_relpath="$1" line_content="$2" + local repo + repo="$(mktemp -d)" + git init --quiet "$repo" + ( + cd "$repo" || exit 1 + mkdir -p "$(dirname "$file_relpath")" + printf '%s\n' "$line_content" >"$file_relpath" + ) + FIXTURE_REPOS+=("$repo") + printf '%s' "$repo" +} + +skill_root=".claude/skills" + +# --help exits 0 with non-empty usage +out="$(bash "$SCRIPT" --help 2>&1)" +code=$? +assert_exit "--help exits 0" 0 "$code" +assert_contains "--help prints usage" "$out" "encapsulation-audit detect" + +# Unknown arg → exit 2 with diagnostic +out="$(bash "$SCRIPT" --bogus-arg 2>&1)" +code=$? +assert_exit "unknown arg exits 2" 2 "$code" +assert_contains "unknown arg diagnostic" "$out" "unknown arg" + +# Empty repo (no in-scope surfaces at all) → environment error +empty_repo="$(mktemp -d)" +git init --quiet "$empty_repo" +FIXTURE_REPOS+=("$empty_repo") +out="$(cd "$empty_repo" && bash "$SCRIPT" 2>&1)" +code=$? +assert_exit "empty repo → exit 2 (no in-scope surfaces)" 2 "$code" +assert_contains "empty repo diagnostic" "$out" "no in-scope" + +# Clean in-scope file → exit 0, silent +clean_repo="$(fixture_repo ".claude/rules/clean.md" "nothing to see here")" +out="$(cd "$clean_repo" && bash "$SCRIPT" 2>/dev/null)" +code=$? +assert_exit "clean repo → exit 0" 0 "$code" +assert_silent "clean repo emits nothing" "$out" + +# Private-subdir cite from a rule file → violation, TSV row emitted +viol_repo="$(fixture_repo ".claude/rules/viol.md" \ + "see ${skill_root}/foo/context/notes.md for detail")" +out="$(cd "$viol_repo" && bash "$SCRIPT" 2>/dev/null)" +code=$? +assert_exit "private-subdir cite → exit 1" 1 "$code" +expected_row="$(printf '.claude/rules/viol.md\t1\t%s/foo/context/' "$skill_root")" +assert_contains "violation row is filelinematch" "$out" "$expected_row" + +# Heading-anchor cite → violation +anchor_repo="$(fixture_repo "docs/guide.md" \ + "see ${skill_root}/foo/SKILL.md#some-heading")" +out="$(cd "$anchor_repo" && bash "$SCRIPT" 2>/dev/null)" +assert_exit "SKILL.md#anchor cite → exit 1" 1 "$?" +assert_contains "anchor cite emitted" "$out" "${skill_root}/foo/SKILL.md#" + +# Bare SKILL.md path cite → legal (discouraged but public) +bare_repo="$(fixture_repo "docs/guide.md" \ + "see ${skill_root}/foo/SKILL.md")" +out="$(cd "$bare_repo" && bash "$SCRIPT" 2>/dev/null)" +assert_exit "bare SKILL.md cite → exit 0" 0 "$?" +assert_silent "bare SKILL.md cite emits nothing" "$out" + +# *.schema.json at any depth → violation +schema_repo="$(fixture_repo ".claude/rules/schema.md" \ + "validate against ${skill_root}/foo/output.schema.json")" +out="$(cd "$schema_repo" && bash "$SCRIPT" 2>/dev/null)" +assert_exit "schema.json cite → exit 1" 1 "$?" +assert_contains "schema.json cite emitted" "$out" "output.schema.json" + +# Plain data file at skill root → legal (data-file carve-out) +data_repo="$(fixture_repo ".claude/rules/data.md" \ + "reads ${skill_root}/foo/catalog.json at runtime")" +out="$(cd "$data_repo" && bash "$SCRIPT" 2>/dev/null)" +assert_exit "root data-file cite → exit 0" 0 "$?" +assert_silent "root data-file cite emits nothing" "$out" + +# Root instruction file is in scope +readme_repo="$(fixture_repo "README.md" \ + "internals at ${skill_root}/foo/reference/table.md")" +out="$(cd "$readme_repo" && bash "$SCRIPT" 2>/dev/null)" +assert_exit "README.md violation → exit 1" 1 "$?" +assert_contains "README.md hit emitted" "$out" "README.md" + +# .github/ workflows are in scope +gh_repo="$(fixture_repo ".github/workflows/ci.yml" \ + "run: cat ${skill_root}/foo/lib/helper.sh")" +out="$(cd "$gh_repo" && bash "$SCRIPT" 2>/dev/null)" +assert_exit ".github violation → exit 1" 1 "$?" +assert_contains ".github hit emitted" "$out" "${skill_root}/foo/lib/" + +# Mixed line: scripts/ entry cite + other-skill private cite. The genuine cite +# must survive (exit 1, emitted) while the scripts/ cite stays carved out — +# per-cite granularity, not whole-line masking. +mixed_repo="$(fixture_repo ".claude/rules/mixed.md" \ + "see ${skill_root}/alpha/scripts/foo.sh and ${skill_root}/beta/research/bar.md")" +out="$(cd "$mixed_repo" && bash "$SCRIPT" 2>/dev/null)" +assert_exit "mixed line: scripts/ + private → exit 1 (masking fix)" 1 "$?" +assert_contains "mixed line: emits the private cite" "$out" "${skill_root}/beta/research/" +assert_not_contains "mixed line: scripts/ cite stays carved out" "$out" "/scripts/" + +# Pure scripts/ line stays carved out (entry-surface carve-out) +pure_repo="$(fixture_repo ".claude/rules/pure-scripts.md" \ + "see ${skill_root}/alpha/scripts/foo.sh")" +out="$(cd "$pure_repo" && bash "$SCRIPT" 2>/dev/null)" +assert_exit "pure scripts/ line → exit 0 (carve-out)" 0 "$?" +assert_silent "pure scripts/ line emits nothing" "$out" + +# Self-citation: skills dir is out of scope by default... +self_repo="$(fixture_repo "${skill_root}/foo/SKILL.md" \ + "progressive disclosure: ${skill_root}/foo/context/detail.md")" +# ...a clean in-scope file keeps the default run off the exit-2 path. +mkdir -p "$self_repo/.claude/rules" +printf 'clean\n' >"$self_repo/.claude/rules/clean.md" +out="$(cd "$self_repo" && bash "$SCRIPT" 2>/dev/null)" +assert_exit "self-citation out of scope by default → exit 0" 0 "$?" + +# ...surfaces raw with --include-skills... +out="$(cd "$self_repo" && bash "$SCRIPT" --include-skills 2>/dev/null)" +assert_exit "self-citation raw with --include-skills → exit 1" 1 "$?" +assert_contains "self-citation raw hit emitted" "$out" "${skill_root}/foo/context/" + +# ...and is filtered legal with --include-skills --apply-filters +err="$(cd "$self_repo" && bash "$SCRIPT" --include-skills --apply-filters 2>&1 >/dev/null)" +code=$? +assert_exit "self-citation filtered legal → exit 0" 0 "$code" +assert_contains "summary counts self-citation as legal" "$err" "raw=1 legal=1 illegal=0" + +if [[ "$FAILED" -ne 0 ]]; then + exit 1 +fi +printf '\nAll %d checks passed.\n' "$CASE_NUM" diff --git a/plugins/docs-hygiene/skills/extract-ssot/SKILL.md b/plugins/docs-hygiene/skills/extract-ssot/SKILL.md new file mode 100644 index 000000000..e22840c0b --- /dev/null +++ b/plugins/docs-hygiene/skills/extract-ssot/SKILL.md @@ -0,0 +1,181 @@ +--- +name: extract-ssot +description: "Deduplicate repeated markdown content — rule files, skill bodies, ADRs, docs — into a single named source of truth and migrate every call site to cite it by exact heading. Use when the same prose, literal, or concept appears (or is reworded) across 3+ files: 'DRY this prose', 'extract a shared rule', 'single source of truth for X', a value-bump diff touching 3+ files — refuses extraction below the Rule of Three." +argument-hint: "[identify|verify|plan|execute|batch|unwind] []" +user-invocable: true +disable-model-invocation: false +--- + +# Extract SSOT + +## Why this skill exists + +Codifies the markdown-SSOT-extraction pattern as a repeatable workflow. Each invocation targets ONE cluster of repeated markdown content and resolves it to a single named source of truth — consolidating into an existing SSOT home when one already owns the concept, otherwise creating a new artifact (e.g. a rule file or a new skill) — plus migration of all call sites to cite by exact heading. + +The principle is the coding Rule of Three / DRY, applied to markdown text. If the same unit of prose appears 3+ times, changes together, and has an identity that can be named, collapse to one definition and reference by name. Below 3 instances, inline is the disciplined call — premature abstraction is the dominant failure mode. + +Typical markdown extraction shapes the workflow handles: + +- A vocabulary of CLI/API verbs that many prompts, skills, and rules invoke inline — collapsed into one rule file with stable headings the call sites cite +- A constraint or guardrail repeated across multiple skills (e.g. "always run X before Y") — promoted to a single always-loaded rule +- A workflow primitive appearing across several skills — extracted to a shared primitive doc that skills cite by name + +Extraction is dangerous: ~19% failure rate even on curated skills (SkillsBench, n=84 tasks), ~50% on practitioner-authored skills (40-skill failure analysis). This skill encodes the guardrails (Rule of Three, categorical-shape test, ≤500-line bound, one-level-deep mandate, Metz unwind procedure) so each invocation is reversible and reviewable. + +**Code / config escape-hatch.** Repeated string literals, magic constants, helper functions in source code, or repeated CI / settings / MCP stanzas in config files are also extractable in principle (Rule of Three applies). This skill flags such clusters during `identify` but does NOT ship a citation contract for them — the caller uses the language-idiomatic form (`import` / `using` / `source`, YAML anchor, JSON `$ref`, build-tool include) and language-aware refactoring tools (IDE rename, Roslyn / ts-morph). Markdown is the only file class with no language-level rename safety net — that is where the contract here adds value. + +## Evidence discipline + +Every extraction decision must be grounded in **direct evidence captured this session** — grep output or file reads you performed yourself. This skill calls such evidence **Tier 0**. Recall ("I remember seeing this repeated"), a subagent's survey summary, or any other synthesized claim is NOT Tier 0 — promote it via your own grep before it drives a plan or an edit. A subagent roster is a lead list, never proof. + +## Scope: markdown SSOT + +**In-scope:** repeated content across the consuming repository's tracked markdown — instruction files (`CLAUDE.md`, `AGENTS.md`, `README.md`), rule files (`.claude/rules/`), skill bodies (`.claude/skills/`), agent definitions (`.claude/agents/`), automation/routine prompts, ADRs, and docs. Markdown is the file class with no compiler or IDE refactor to catch a broken reference — the citation contract here is what closes that gap. + +| Citation target | Form | +|-----------------|------| +| Rule-file H3 heading | `` per `.md` "" `` | +| New skill | `/` invocation (skill internals NOT cited externally — see `/encapsulation-audit`) | + +**Out-of-scope, but flagged during `identify`:** + +- **Code clusters** (`.cs`, `.ts`, `.py`, `.sh`, `.ps1`, …) — repeated literal, magic constant, regex, helper function. Use language-idiomatic extraction (constants file, shared module, IDE rename refactor / Roslyn / ts-morph). Compiler + lint catch missed call sites — citation rot is structurally prevented. +- **Config clusters** (`.yml`, `.json`, `.toml`, `.editorconfig`) — repeated CI step, MCP entry, settings stanza. Use the tooling's native include / anchor / `$ref` mechanism. Schema validation catches mismatches. +- **Mixed clusters** spanning markdown + code + config — pick the canonical owner (usually code/schema where runtime authority lives), then each file class cites in its native form. This skill handles the markdown half; the code/config half follows that file class's own conventions. + +The 6-test extraction gate (Rule of Three, namable, stable, self-contained, bounded, one level deep) generalizes to all file classes, and `context/decision-framework.md` annotates each test with code/config equivalents. The HOW (citation contract, rename sweep, encapsulation rule) is markdown-specific. + +Boundary with single-file refactoring: a rename, inline, or extract confined to one file or one recent diff is ordinary editing, not this skill's job. `/extract-ssot` handles cross-file markdown deduplication where the repeated unit needs a stable name and cross-file citations. When work touches both, normalize the call sites first, then lift the named unit. + +## When to use vs not use + +**Use** when: 3+ instances of the same unit exist across files; the instances change together (correlated edits); the unit has a stable identity that can be named; the unit is self-contained (extracts cleanly without dragging unrelated context). **Normal-work entry point** — when a cleanup pass, audit, value-bump diff, or review surfaces ANY of the three duplication smells — (a) same literal repeated across 3+ tracked files, (b) value-bump diff touches 3+ unrelated files, (c) same concept reworded across 3+ files or contradicting nuance between files — route detection here. The skill accepts both literal and semantic clusters; the 6-test gate's "namable + categorical-shape + stable identity" tests admit semantic clusters provided the unit can be named and instances change together. + +**Classify each file's role before flagging it as a duplicate:** + +- **DESCRIBE** — the file IS the SSOT and owns the value or concept; keep the body. +- **USE** — the file consumes the concept as a load-bearing reference (rules, agent prompts, skill bodies invoking the rule); it MUST cite the SSOT by exact heading or path + key rather than restate body content. +- **EXPOSE** — the file surfaces the concept to humans for onboarding clarity (README install commands, error messages, public-facing docs); it MAY restate when onboarding clarity outweighs maintenance cost AND adjacent prose cross-references the SSOT. + +Contract identifiers the SSOT defines (tier names, label slugs, action verbs, command names) stay inline in consumers — naming them is not duplication; those tokens ARE the contract surface. Content (definitions, criteria, mapping tables, thresholds, exception clauses) must cite, never recap. + +**Don't use** for: single-file refactoring inside one diff; recent-diff simplification of a single skill or feature; filing a tracking issue (route to the repo's issue tracker); writing a new skill from scratch without an underlying repetition trigger (use a skill-authoring workflow such as the skill-creator plugin); cross-language type sharing where the answer is codegen, not text dedup. + +Full decision matrix: `context/decision-framework.md` (6+5 checklist with worked examples). + +## Action router + +| Argument | Action | Purpose | +|----------|--------|---------| +| *(empty)* | Smart default | Auto-detect: working notes from a prior run hold an active candidate roster → resume the current phase; otherwise → `identify` | +| `identify []` | Find candidates (default = exhaustive subagent survey) | Dispatches a read-only exploration subagent over 30+ duplication heuristics (full body in `actions/identify.md`); ranks by ROI; emits batch-sequencing matrix + recommended `/extract-ssot batch` invocation. Refuses premature (<3 instances). Single-cluster mode (`identify `) skips the subagent for a targeted Tier 0 grep | +| `verify ` | Refuse-fast pre-extraction gate | 6-gate cheap check (Tier 0 grep, citation state, primary-source URL gate, bifurcation check, off-by-one heuristic, LOW-ROI threshold). Output: `PROCEED \| REFUSE-{reason} \| WARN`. OPTIONAL — does not gate `plan`/`execute`. See `actions/verify.md` | +| `plan ` | Architect | Pre-step (Tier 0 grep): does an existing rule/doc already own the concept? If yes → consolidate-into-existing branch (extend the home + de-recap consumers, no new artifact). Else choose creation output type (rule vs skill); draft or extend SSOT body; sketch migration plan | +| `execute ` | Migrate | Write or extend the SSOT (skip writing when an existing home already documents the concept); rewrite call sites to cite + de-recap inline reproductions; sweep references via `/rename-references` if a heading/identifier changed; verify | +| `batch ` | Multi-candidate orchestration | Auto-`verify` filter, file-overlap matrix, sequential-by-default dispatch, lesson injection between subagents. See `actions/batch.md` | +| `unwind ` | Reverse | Re-introduce duplication per Sandi Metz wrong-abstraction recovery | + +One action per response; actions don't chain implicitly. + +## Decision framework + +Before recommending extraction, run the 6-test gate (all must pass) + 5-test inline gate (any one keeps inline). Full checklist with evidence and worked examples in `context/decision-framework.md`. + +Headline gate: **Rule of Three** (Don Roberts / Fowler) — refuse extraction at <3 instances. Premature abstraction is the dominant failure mode. + +## Output type + +Markdown branch (primary): + +| Shape | Target | Trigger | +|-------|--------|---------| +| Concept already has an SSOT home | Consolidate into the existing file (extend it only where a consumer carries nuance the home lacks) + de-recap the inline reproductions; create no new artifact | An existing rule/skill/doc already owns the concept and consumers recap it inline instead of citing it. Positive output-type form of `verify` Gate 2 + anti-pattern Shape C; `identify` flags it as `edit-existing-rule` / `trim-to-citation` | +| Vocabulary, IF-THEN rules, hard constraints, ≤500 lines | Rule file wherever the consuming repository's own conventions place shared rules — default `.claude/rules/.md` (always-loaded) or a path-scoped rule file | Categorical markdown content; consumers cite by H3 heading | +| Workflow, multi-action, has its own actions/anti-patterns | New skill at `.claude/skills//SKILL.md`, authored via the consumer's skill-authoring workflow (e.g. the skill-creator plugin) | Process content; consumers invoke `/` | +| New action on existing skill | Action row added to the skill's action router | The workflow maps cleanly onto an existing skill's concern — same domain, same triggers, same output surface — rather than warranting a new top-level skill | + +Skill-vs-rule heuristic: if the SSOT body is mostly nouns (named units the caller cites), it's a rule file. If the SSOT body is mostly verbs (steps the caller invokes), it's a skill. + +Non-markdown escape (out of scope for this skill's HOW; flag during `identify`, refer the caller to language-idiomatic tooling): code constants / shared modules / helper libraries; config includes / YAML anchors / JSON `$ref`. See `context/decision-framework.md` "Output type: rule file vs skill" for the fuller table including code/config rows and worked examples; this section is the canonical markdown summary. + +## Citation form + +For markdown call sites, cite by exact H3 heading text + 1-line inline summary. Template: + +```text + per `.md` "". + per `.md` "" — . +``` + +For code call sites, use the language's native import syntax. For config call sites, use the tooling's native include / anchor / `$ref` mechanism. + +One level deep — never chain `A.md` → `B.md` → `C.md`. A heading rename triggers a `/rename-references` sweep across all 10 syntactic forms. + +Full contract incl. line-wrap edge case: `context/citation-form.md`. + +## Encapsulation rule + +Encapsulation enforcement (detection grep, public/private surface matrix, remediation paths) lives in its own skill — `/encapsulation-audit`. Different concern from duplication: violations are single-instance matters (Rule of Three does not gate them). + +`/extract-ssot execute` invokes `/encapsulation-audit detect` during the refactor pass to catch any encapsulation violations introduced or exposed by the migration. See `/encapsulation-audit` for the public surface matrix, filter taxonomy, and remediation paths. + +## Anti-patterns guarded + +13-pattern taxonomy with mitigations: citation rot, over-indirection, leaky abstraction, loss of locality, reference resolution failure, wrong abstraction, premature extraction, self-generated SSOT, cache invalidation cascade, encapsulation violation, source-of-truth bifurcation, primary-source citation gate, Shape C dedup-by-deletion (positive). Each pattern + symptom + mitigation procedure: `context/anti-patterns.md`. + +Patterns #11/#12/#13 derive from the empirical lessons in `context/lessons.md` and are surfaced as REFUSE triggers in the `verify` action. + +The `unwind` action implements Metz's 3-step recovery for the wrong-abstraction case (re-introduce → keep used subset → delete unneeded → re-isolate). + +## Phases per invocation + +```text +identify-cluster → architect-plan → execute-migration → sweep-references → verify +``` + +For multi-session work, persist the candidate roster, plan, and per-phase status to working notes in the consuming repository (wherever its conventions put task notes) so a fresh session can resume from durable state instead of re-deriving it. End each phase with a short status entry: what's done, what's next. + +Per-phase checklist: `context/execution-checklist.md`. + +## Sanity checks + +| When | Check | Evidence | +|------|-------|----------| +| Pre-extraction | 3+ instances confirmed via grep (Tier 0) | Grep output captured in the plan/working notes | +| Pre-extraction | Cluster has stable identity that can be named; instances change together | Decision-framework checklist marked in the plan | +| Pre-extraction | File-class scope identified (markdown / code / config / mixed) | Listed in the plan; citation form chosen per class | +| Per-callsite | Citation/import in the form native to the call site's file class | Diff review | +| Post-extraction | All 10 `/rename-references` patterns swept (markdown call sites) | Skill output | +| Post-extraction | SSOT reads sensibly in isolation (leaky-abstraction self-test) | Manual read | +| Post-extraction | Lint clean across affected file classes; cross-references and imports resolve | Linter/build output | + +## What this skill does NOT do + +- Decide for the user whether to migrate a specific cluster (advisory; user picks) +- Modify files outside the repository it runs in +- Skip per-phase user diff review +- Single-file micro-refactoring (rename, inline, extract confined to one file) — ordinary editing +- Workitem filing — route to the repo's issue tracker +- Author a brand-new skill from scratch without a repetition trigger — use a skill-authoring workflow (e.g. the skill-creator plugin) +- Cross-language type sharing where the answer is codegen, not text dedup +- **Replace a workspace-wide verification pass** — the `verify` action here is a per-cluster refuse-fast pre-extraction gate, not a build+test+lint run; run the consuming repository's own verification after `execute` + +## Cross-references + +- `context/decision-framework.md` — 6+5 gate, Pre-extraction Tier 0 checklist, output-type table, worked examples +- `context/citation-form.md` — full citation contract for markdown call sites +- `context/anti-patterns.md` — 13-pattern taxonomy with mitigations +- `context/execution-checklist.md` — per-phase checks for the `execute` action +- `context/lessons.md` — append-only empirical lessons from batch executions; consumed by the `verify` action and `context/decision-framework.md` +- `actions/identify.md`, `actions/verify.md`, `actions/batch.md` — action bodies (private surface) +- `/rename-references` — load-bearing 10-pattern sweep after any heading change (owns the syntactic-form set) +- `/encapsulation-audit` — encapsulation detection + remediation (separate concern) + +## Recheck triggers + +| Condition | Action | +|-----------|--------| +| External tool/CLI/API documented inside an SSOT rule ships a major version bump | Re-verify the Tier 0 flag/verb set in the affected rule file; cited entries may have moved or renamed | +| `/rename-references` adds a new syntactic form to its 10-pattern sweep | Update the sweep step in `context/execution-checklist.md` | +| Anthropic ships a first-class native rule/skill linker (heading-rename auto-sweep) | Demote the `/rename-references` step to advisory; reduce sweep scope | +| Practitioner-authored skill failure rate drops below 20% (SkillsBench refresh) | Reduce gate strictness; consider relaxing Rule of Three to Two for low-risk vocabulary | diff --git a/plugins/docs-hygiene/skills/extract-ssot/actions/batch.md b/plugins/docs-hygiene/skills/extract-ssot/actions/batch.md new file mode 100644 index 000000000..82d12659e --- /dev/null +++ b/plugins/docs-hygiene/skills/extract-ssot/actions/batch.md @@ -0,0 +1,230 @@ +# `batch` action — multi-candidate orchestration + +Multi-candidate orchestration. Computes a file-overlap matrix across candidates, dispatches refuse-fast `verify` to filter, then runs `plan`/`execute` in non-overlapping parallel waves OR strict sequential order (concurrent-write risk → sequential by default). Accumulates lessons in `context/lessons.md` between subagent dispatches. + +Loaded by `/extract-ssot batch `. Private surface — invoke via `/extract-ssot batch`, never cite this file directly (contract: `/encapsulation-audit`). + +## When to invoke + +| Use case | Invoke | +|----------|--------| +| `/extract-ssot identify` produced 5+ candidates and you want efficient orchestration | YES | +| Manual list of candidates to migrate in one pass | YES | +| Single candidate | NO — use `/extract-ssot plan ` directly | +| < 3 candidates | NO — manual sequential dispatch is simpler | + +This is NOT the bundled Claude Code `/batch` skill. Bundled `/batch` is polyglot worktree-parallelized refactor per [code.claude.com/docs/en/commands](https://code.claude.com/docs/en/commands); this `batch` action is local SSOT-cluster orchestration. + +## Inputs + +```text +/extract-ssot batch [ ... ] +``` + +OR resume from working notes if a `batch` phase is mid-flight. + +Candidate names match `/extract-ssot identify` output's cluster names. + +## Steps + +```text +1. Pre-flight: read context/lessons.md (snapshot for this batch) +2. For each candidate: invoke `verify ` (Tier 0 grep, citation state, etc.) +3. Filter: drop REFUSE-* candidates; keep PROCEED + WARN +4. Compute file-overlap matrix across surviving candidates +5. Group into non-overlapping waves (graph coloring on overlap matrix) +6. Dispatch wave-by-wave; SEQUENTIAL within wave when shared files exist +7. Inject lesson snapshot into each subagent's prompt +8. Per dispatch: capture verdict + new lessons surfaced +9. Append new lessons to context/lessons.md (Lesson N+1 entries) +10. Write a batch audit log entry to the working notes +``` + +## Step 1 — Pre-flight + +Read `context/lessons.md` once at batch start. The snapshot is the lesson set injected into all subagent dispatches in this batch. Avoids race conditions where subagent A and B both append simultaneously. + +## Step 2 — Verify filter (HARD GATE for batches ≥5) + +For each candidate, invoke `verify` (private action — see `actions/verify.md`). Capture per-candidate output. + +**HARD GATE rule (per Lesson 10):** when `` size ≥ 5, `verify` is MANDATORY before any `plan`/`execute` dispatch — refuse-fast at this step rather than spawning subagents on false-positive candidates. Subagent identify passes routinely produce ~95% FP rates without per-cluster Tier 0 verification; gating here prevents wasted dispatches. Smaller batches (1-4 candidates) may skip `verify` per user discretion (the action is still OPTIONAL there). + +If the batch fails the verify-gate (≥80% candidates REFUSE), abort the batch and surface the diagnostic to the user — it likely signals the identify pass needs hardening per the Discrimination rules in `actions/identify.md`. Don't dispatch `plan`/`execute` on the surviving 20%; the user picks scope manually. + +```yaml +candidate: +verify-status: PROCEED | REFUSE-{reason} | WARN +verify-evidence: [...] +``` + +Output forms the batch summary's first column. + +## Step 3 — Filter + +Drop candidates with `REFUSE-*` status from the dispatch list. Keep `PROCEED` + `WARN`. Surface the dropped candidates with reasons in the batch audit log so the user sees the refuse-fast savings. + +## Step 4 — File-overlap matrix + +For each surviving candidate, identify the file set the candidate would touch: + +- ALLOWED list from the candidate spec (output target file + sweep call sites) +- FORBIDDEN list (other candidates' territory; previous-batch territory) + +Compute overlap: + +```text + | C1 | C2 | C3 | C4 | + C1 | - | ∅ | X | ∅ | + C2 | ∅ | - | ∅ | X | + C3 | X | ∅ | - | ∅ | + C4 | ∅ | X | ∅ | - | +``` + +`X` = at least one shared file (write conflict); `∅` = disjoint. + +Implementation: for each pair (Ci, Cj), grep both candidate specs for ALLOWED files, intersect sets. If the intersection is non-empty, mark `X`. Capture the full intersection list in the audit log. + +## Step 5 — Wave grouping (graph coloring) + +Build an undirected graph: nodes = candidates, edges = `X` overlaps. Color with greedy graph-coloring; nodes of the same color = one wave. + +Naive heuristic when N < 10: + +- Sort candidates by overlap-degree (highest first) +- Wave 1 = highest-degree candidate; add candidates with no edge to wave-1 members +- Wave 2 = next highest-degree candidate not yet assigned; same rule +- Repeat + +Output: wave-grouped candidate list: + +```yaml +waves: + - wave: 1 + candidates: [C1, C2] + - wave: 2 + candidates: [C3, C4] +``` + +## Step 6 — Dispatch policy + +**SEQUENTIAL within wave when ANY of:** + +- Wave has > 1 candidate AND any pair has shared files (collision risk — concurrent agents editing the same file silently overwrite each other; there is no file-level locking) +- Wave touches files that another wave already touched in this batch (chronological dependency) +- Candidate has `verify-status: WARN` (an extra adversarial-review step is warranted) + +**PARALLEL within wave allowed when ALL of:** + +- All candidates in the wave are pairwise disjoint per the overlap matrix +- No candidate touches files already edited in earlier waves +- All candidates have `verify-status: PROCEED` (HIGH confidence) + +Default: sequential. Parallel is opt-in via the batch-action argument `--parallel-waves`. Parallel collisions are a real bug class (no file locking); the default conservatism is intentional. + +## Step 7 — Lesson injection + +Each subagent dispatched in this batch receives the lesson snapshot from Step 1 in its prompt: + +```text +## Empirical lessons from prior batches + +(snapshot of context/lessons.md as of batch start) + +Lesson 1 — Discriminating-phrase grep beats keyword density +... (full lessons.md body) +``` + +The subagent treats lessons as advisory — applies them in its own decision-making but does NOT modify `lessons.md` directly. New lessons from THIS subagent's run are returned in the deliverable summary, not committed by the subagent. + +## Step 8 — Per-dispatch capture + +Each subagent return value contains: + +```yaml +candidate: +verdict: EXTRACTED | REFUSED-{reason} | DEFERRED +files-modified: [...] +new-lessons: [free-form patterns observed] +sanity-check-evidence: [...] +``` + +`new-lessons` is the field where empirical patterns surface for the orchestrator to codify. + +## Step 9 — Lesson append + +After all waves complete, the orchestrator (main session) reviews `new-lessons` from all dispatches: + +- Cross-check against existing lessons.md (avoid duplicate Lesson N entries) +- For genuinely novel patterns: append `## Lesson N+1: ` per `context/lessons.md` "Append guidance for future batches" +- The Source field references THIS batch's audit log +- The Encoded-in field documents which downstream artifacts (anti-patterns.md, verify gates) should consume the new lesson + +Subagent-reported lessons are synthesis until the orchestrator re-verifies them with its own grep — verify each novel-lesson claim before the lessons.md append. + +If no novel patterns surface, no append. Don't force. + +## Step 10 — Batch audit log + +Append to the working notes: + +```markdown +--- +type: batch +date: +batch-size: +--- + +## Batch summary + +| # | Candidate | Verify | Verdict | Wave | Files modified | +|---|-----------|--------|---------|------|----------------| +| 1 | C1 | PROCEED | EXTRACTED | 1 | path1, path2 | +| 2 | C2 | PROCEED | REFUSED-low-roi | 1 | (none) | +| 3 | C3 | REFUSE-already-cites-canonical | (skipped) | (n/a) | (none) | +| ... | | | | | | + +## File-overlap matrix + + +## Wave plan + + +## Dispatch policy +- Sequential / parallel + rationale + +## Lessons accumulated +- Lesson N+1: (if any) + +## Refuse-fast savings + candidates refused at verify; subagent dispatches saved +``` + +The log doubles as the batch retrospective and as source material for the PR description. + +## Side observations + +Hard limit ≤2 side notes per response: + +- If multiple candidates surface the SAME refuse-pattern (e.g. 3 candidates REFUSE-already-cites-canonical for the same canonical file), surface ONE side observation suggesting the canonical file document a stable heading for its audience; batch the rest into the log +- If all candidates in a wave PROCEED but the wave's parallel-vs-sequential choice was nontrivial, surface the rationale (so the user can adjust `--parallel-waves` next time) + +## Recheck triggers + +| Condition | Action | +|-----------|--------| +| Claude Code ships file-level locking for concurrent sessions | Re-evaluate parallel-by-default; relax the sequential-within-wave constraint | +| `lessons.md` exceeds 400 lines | Trigger archive-and-trim per `context/lessons.md` "Append guidance for future batches"; preserve the most-recent + greatest-impact lessons inline | +| The `verify` action ships a new gate (Gate 7+) | Update the Step 2 verify filter to capture new reason codes | +| Anthropic ships a canonical batch/multi-target action convention for skills | Re-align the Step 6 dispatch policy; expose `--parallel-waves` differently if upstream prescribes | +| `/extract-ssot batch` consistently produces > 50% refuse-fast filtering | Diagnostic signal that the `/extract-ssot identify` survey heuristic needs tuning; document the tuning in `lessons.md` | + +## Cross-references + +- `actions/verify.md` — Step 2 sub-routine; refuse-fast gate per candidate +- `context/lessons.md` — Step 1 snapshot source; Step 9 append destination +- `context/decision-framework.md` "Pre-extraction Tier 0 checklist" — the same gates `verify` runs, documented for human-readable batch review +- `context/anti-patterns.md` #11 / #12 / #13 — REFUSE patterns the verify filter encodes +- SKILL.md "Evidence discipline" — subagent return values are synthesis by default; the orchestrator MUST verify novel-lesson claims before the lessons.md append +- `/extract-ssot identify` — produces the ranked candidate list this batch action consumes +- Bundled Claude Code `/batch` skill — distinct concern (worktree-parallelized polyglot refactor); see SKILL.md "What this skill does NOT do" diff --git a/plugins/docs-hygiene/skills/extract-ssot/actions/identify.md b/plugins/docs-hygiene/skills/extract-ssot/actions/identify.md new file mode 100644 index 000000000..3fc3c18b5 --- /dev/null +++ b/plugins/docs-hygiene/skills/extract-ssot/actions/identify.md @@ -0,0 +1,308 @@ +# `identify` action — exhaustive duplication survey + +Default mode dispatches a read-only exploration subagent that runs 30+ duplication heuristics across all markdown surfaces, emits a ranked candidate roster, computes a file-overlap matrix, and returns a batch-sequencing recommendation ready to feed `/extract-ssot batch`. + +Private surface — external consumers invoke `/extract-ssot identify`, never cite this file directly (contract: `/encapsulation-audit`). + +## Two modes + +| Invocation | Mode | Behavior | +|------------|------|----------| +| `/extract-ssot identify` | Exhaustive (default) | Read-only subagent deep survey across instruction files, rules, skills, agents, ADRs, docs. Returns a ranked candidate roster + dependency chains + file-overlap matrix + batch wave plan | +| `/extract-ssot identify ` | Targeted | Tier 0 grep on a named cluster only. Returns instance count + Tier 0 evidence + suggested output type. No subagent dispatch | + +User signals like "find ANY and ALL", "deep dive", "exhaustive", "full list", or `/extract-ssot identify` with no args = default to exhaustive mode. + +## When to invoke + +| Use case | Mode | +|----------|------| +| User asks for full duplication audit | Exhaustive | +| Maintenance / quarterly sweep | Exhaustive | +| User has one cluster in mind already | Targeted | +| Resume from working notes holding a candidate roster | Skip identify; route to `verify` / `plan` / `execute` / `batch` | + +## Inputs + +```text +/extract-ssot identify # exhaustive default +/extract-ssot identify # targeted +``` + +## Exhaustive mode steps + +```text +1. Pre-flight: confirm no working notes with an active candidate roster (would imply resume, not new identify) +2. Dispatch a read-only exploration subagent with the survey prompt (template below) +3. Subagent searches markdown surfaces with 30+ heuristics (template lists them) +4. Subagent returns ranked candidate table + dependency chains + file-overlap matrix +5. Main session classifies output: deduplicate against context/lessons.md known-refused patterns +6. Main session emits batch-sequencing recommendation (waves, sequential vs parallel, hot files) +7. Main session offers user: dispatch /extract-ssot batch with top-N waves, or pick specific clusters +8. Persist the roster to working notes so the user can resume from durable state +``` + +## Subagent prompt template + +The subagent receives a self-contained prompt. Skeleton: + +```text +Goal: EXHAUSTIVE duplication survey for /extract-ssot. Find ANY and ALL duplication +candidates across markdown in this repo. Apply STRICT Tier 0 discipline — see +"Discrimination rules" below before adding any candidate to the roster. + +Repo: + +## Survey scope — git-tracked files only + +Use `git ls-files` to enumerate the survey universe. EXCLUDE: +- Gitignored files (anything `git check-ignore ` returns exit 0 for) +- Ephemeral task/working-notes directories +- Vendored/third-party verbatim content (upstream copies, NOT repo authoring) +- Distilled external teaching material (course notes, book digests — content, NOT repo convention) +- Test fixtures and eval data (test inputs, NOT call sites) +- Run logs and other generated output +- Single-use / archived prompts + +In-scope authoring surfaces (adapt to what this repo actually has): +- CLAUDE.md, AGENTS.md, README.md, and other root instruction files +- .claude/rules/**/*.md (incl. nested subdirectories) +- .claude/skills/**/*.md (SKILL.md, context/, reference/, actions/, templates/) +- .claude/agents/*.md +- Automation / routine / scheduled-agent prompts (NOT their run logs) +- ADRs and docs/**/*.md +- .github/**/*.md and per-tool markdown (contributor docs, tool READMEs) + +## Discrimination rules — CRITICAL + +Each candidate MUST be classified by repetition form. Forms (a), (e)+(framing-only), and +(i) count as extraction candidates. Form (c2) full-paragraph semantic reword also counts +when the stability+reader-burden test passes. + +| Form | Counts as duplication? | Example | +|------|------------------------|---------| +| (a) Verbatim block reproduction (≥15 words, copy-paste) | YES | The same dependency-direction rule text in 5 files | +| (b) Section-header presence (same `## X` heading, different body) | NO — convention/template | `## What this skill does NOT do` in 18 skills with unique non-goals each | +| (c1) 1-line teaching reference / single concept mention | NO | A verification tier mentioned once in a paragraph | +| (c2) Full-paragraph reword of same canonical truth (no verbatim ≥8 word phrase shared) | YES — semantic cluster; gate via stability+reader-burden test | 4 skills each restate the same session-hygiene rule in their own wording | +| (d) Correct citation to existing SSOT (`per X.md "Y"`) | NO — desired state | Citation IS the architecture | +| (e) Shared framing + per-instance unique data | BORDERLINE — extract framing IF stability+reader-burden test passes | 5 agents share an intro paragraph; only the examples differ | +| (f) Language-native dedup (bash `source`, Python `import`, MSBuild ``, JSON `$ref`) | NO — already extracted | 34 hooks `source hook-utils.sh` IS the dedup | +| (g) Per-instance unique scope-specific list (exclusion lists, allowed-file lists, etc.) | NO — content unique even when section-header shared | Per-prompt exclusion lists are scope-specific | +| (h) Domain-specific application of shared rule | NO — context-specific | Each skill applies a testing default in its own framing | +| (i) Semantic-paraphrase cluster — 3+ instances assert same canonical truth in different wording; no shared verbatim ≥8 word phrase but reader could not tell which is canonical | YES — extract iff stability OR reader-burden test passes | A commit-policy framing restated across the instruction file + 3 skills + 2 prompts in different words | + +**Stability + reader-burden combined test — applies to forms (c2), (e), (i).** Extract iff EITHER: +- Changing the canonical truth would force updates in 3+ places in lockstep (maintenance burden), OR +- Reader cannot tell which instance is canonical (ambiguity) + +If only ONE passes: borderline (mark WARN). If NEITHER: REFUSE-low-roi. + +**Two-pass survey required.** Run BOTH: +- **Pass A — literal:** verbatim discriminating-phrase grep. Catches (a). +- **Pass B — semantic:** for each known canonical SSOT (the repo's rule files and + always-loaded instruction files) AND for each topical concept the survey surfaces, + read consumer files looking for paragraphs that restate the rule in DIFFERENT WORDS. + Cluster by canonical-truth, not by shared phrase. Catches (c2), (e), (i). + +Pass A alone systematically misses (c2)/(i). + +## Per-candidate evidence requirement + +For EACH candidate, capture (NOT optional). Use ONE of two evidence shapes depending on form: + +**Literal shape (forms a, e):** + +1. **Discriminating phrase** (≥8 words, verbatim, unique enough for clean grep) +2. **Reproduction count** = distinct files containing the discriminating phrase in form (a) or (e). NOT keyword density. NOT section-header count. +3. **Body excerpt** (first 2 reproductions verbatim) for human review + +**Semantic shape (forms c2, i):** + +1. **Canonical-truth one-sentence statement** — the single rule/fact each reproduction asserts in its own words +2. **Reproduction count** = distinct files whose paragraph reproduces the canonical-truth in any phrasing. Reading-driven clustering, NOT phrase-grep counting. +3. **Body excerpt** (first 3 reproductions verbatim — even though wording differs, capture each instance's actual phrasing so the reviewer can verify the semantic match) +4. **Stability+reader-burden test result** — note which test passes and why + +**Both shapes also require:** + +5. **Citation state** — for each match, is the surrounding context "inline reproduction" or "citation to existing SSOT"? Count separately. For semantic shape: a paragraph that BOTH restates AND cites is form (d) — count as already-cited. +6. **SSOT existence check** — does a canonical file already exist? If yes, what % of call sites cite it? If 100% cite → REFUSE-already-cites-canonical. +7. **Language-native check** — is the cluster a shared library, helper module, build-tool import, JSON $ref? If yes → out-of-scope. + +A candidate without the appropriate evidence shape fields populated is REFUSED automatically. + +## Heuristic checklist (Pass A literal + Pass B semantic): + +**Pass A — literal grep aggressively (catches form a, partial e):** + +1. Repeated paragraphs / sentences ≥15 words across files +2. Repeated H2/H3 section bodies (same heading + similar content) +3. Repeated tables (same column headers + overlapping rows) +4. Repeated code/command snippets (bash idioms, gh CLI, build/test commands, jq, git) +5. Repeated frontmatter patterns (same YAML fields/values across skills) +6. Repeated lists (same bullets across multiple files) +7. Inlined concepts that already have an SSOT in the repo (verification vocabulary, + workflow primitives, naming schemes, citation form, encapsulation rule, + frontmatter field tables, env var tables) +8. Cross-skill convention duplication (multiple skills restating the same primitive) +9. ADR cross-reference duplication (same ADR with same explanation in 3+ files) +10. Issue # / PR # / version qualifier patterns (same number cited identically) +11. Recheck-trigger row duplication (same condition + action across files) +12. Acronym / glossary repetition (domain term defined in N files) +13. Permission/auth setup steps (CLI auth, token env vars repeated) +14. MCP server registration patterns (config shape / wrapper repeated) +15. Hook authoring boilerplate (kill switch, exit codes, JSON schemas) +16. Environment-detection logic (CI / remote / non-interactive checks repeated) +17. Worktree or branch setup / lifecycle ritual repeated +18. Script headers (`#!/usr/bin/env bash`, `set -euo pipefail`, source utils) +19. Platform quirks repeated (Windows/shell gotchas restated per file) +20. Skill description trigger phrases that overlap +21. "What this skill does NOT do" boilerplate items repeated across skills +22. Citation text — `per X.md` patterns where the same X.md "" is cited in 3+ files +23. Test framework setup (framework pattern explanations repeated) +24. PR title / commit format explained in N places +25. Branch naming prefix tables / lists repeated +26. Effort/verbosity levels explained across skills +27. Model selection rationale repeated +28. Subagent dispatch boilerplate (preamble repeated) +29. Common error message / status interpretations repeated +30. Recheck-triggers / cross-references H2 boilerplate (structure-only) + +**Pass B — semantic clustering (catches forms c2, i — REQUIRED, not optional):** + +For Pass B, the SUBAGENT MUST do reading-driven clustering, not phrase grep. Method: + +a. **Concept-axis enumeration.** Pre-seed by enumerating the concepts asserted in the + repo's always-loaded instruction surfaces (CLAUDE.md, AGENTS.md, always-loaded rules) — + those are the truths most likely to be restated elsewhere. High-likelihood reword + targets in most repos: + - Commit / stage / push policy (who commits, when, with what message shape) + - Environment / session detection (CI vs local, interactive vs autonomous) + - Merge mechanics and branch naming restated across workflow docs and skills + - The repo's workflow-stage chain restated across multiple stage skills + - Budget/limit conventions (token, time, size caps) restated across skills and rules + - Commit-message or PR conventions across contributor docs, rules, and skills + - Status/resume conventions for multi-session work restated across skills + - Trust/verification discipline ("subagent output must be re-verified") restated + across rules and skill bodies + - Session-hygiene guidance (clear/compact between stages) across multiple skills + - Cleanup-in-passing / Boy Scout rules across instruction files and skills + - Response-formatting or side-observation limits across instruction files and agents + - Per-prompt exclusion-list patterns (usually per-instance unique — form g, REFUSE — + but check) + - Hook/script authoring boilerplate across the rule that owns it + skills that author hooks + +b. **For each concept above, sample 3-5 candidate consumer files and READ the relevant + section** (not grep). Compare the paragraphs' assertions for semantic equivalence: + - Same canonical truth asserted? → semantic cluster (form c2 or i) + - Each file applies the rule to its own scope? → form (h) domain-specific application, REFUSE + - Each file teaches the rule for its own audience with intentionally different framing? + → intentional bifurcation, REFUSE + +c. **Don't stop at the pre-seeded list.** As reading progresses, surface NEW concept axes + the subagent notices being restated. Append them to the candidate list. + +For EACH candidate cluster (both passes), capture: +- Cluster name (kebab-case slug) +- File list with line ranges where possible +- Instance count +- 1-line description +- SSOT exists? (path or "no") +- Suggested output: rule-file | new-skill | new-action | edit-existing-rule + | trim-to-citation | code-extract-advisory | config-extract-advisory +- ROI: HIGH / MEDIUM / LOW +- Dependency on other candidates (so batch ordering is clear) +- File-overlap (which other candidates touch same files — for batch sequencing) + +**Existing-owner pre-check — route before suggesting a creation output.** Gate the `Suggested output` +field on the SSOT-existence check (the `SSOT exists?` capture field + per-candidate evidence item 6): if +an existing rule/skill/doc already owns the concept and ≥1 consumer still recaps it inline, suggest the +consolidation outputs — `edit-existing-rule` (extend the home only where a consumer carries nuance it lacks) +and/or `trim-to-citation` (replace each inline recap with a citation) — NOT a creation output. If the home +is complete and 100% of sites already cite it → no work (`REFUSE-already-cites-canonical` per `verify` +Gate 2). Reserve `rule-file` / `new-skill` / `new-action` for concepts with NO existing home. + +Output: ONE big ranked table (ROI desc, dependency-grouped). Then a batch-sequencing +recommendation grouping non-overlapping candidates that can run in parallel + +dependency chains that must run sequentially. + +Mark with ⭐ any cluster where an SSOT already exists but call sites STILL inline +(highest signal — quick wins). + +Time budget: large. Aim thoroughness > speed. Do NOT edit files. +``` + +## Output shape (exhaustive mode) + +Main session presents to user: + +```markdown +# Duplication survey — N candidates + +## HIGH ROI (no dependencies, ⭐ SSOT-exists-but-inlined) + + +## HIGH ROI (with dependencies) + + +## MEDIUM ROI + + +## LOW ROI / advisory + + +## Code/config advisory (out of scope) + + +## Dependency chains (must run sequential) + + +## Hot files (must NOT run candidates touching them in parallel) + + +## Batch dispatch plan + + +## Recommended next step +`/extract-ssot batch ` +``` + +The ranked table + wave plan is then persisted to working notes so the user can reset context and resume from durable state. + +## Targeted mode steps + +```text +1. Tier 0 grep across markdown for the named cluster's distinctive phrase +2. Capture: instance count, file list, line numbers +3. Run quick instance-stability check (Rule of Three; do they change together?) +4. Suggest output type per `context/decision-framework.md` +5. Return candidate spec ready for `/extract-ssot verify ` +``` + +No subagent dispatch. No batch sequencing. Single-cluster sanity check only. + +## Anti-patterns guarded + +- **Premature exhaustive mode** — dispatching a survey subagent when the user already has 1-2 clusters in mind wastes a dispatch. Detect via the argument. +- **Synthesis-only output** — a subagent return is unverified synthesis, not Tier 0 evidence. Each cluster MUST be promoted to Tier 0 (grep this turn) before `/extract-ssot plan` or `execute` runs. The `verify` action enforces this. +- **Skipping the user-review gate** — exhaustive mode can emit a roster of dozens of candidates. NEVER auto-dispatch the whole roster without user confirmation. Default policy: present roster + recommend top wave; user picks scope. +- **Roster decay** — the survey is point-in-time. If `/extract-ssot batch` partial-completes and the user resumes weeks later, re-run `identify` rather than trusting a stale roster. + +## Sanity checks + +| When | Check | Evidence | +|------|-------|----------| +| Pre-dispatch | No active working-notes candidate roster | Read of the notes | +| Post-dispatch | Subagent returned ≥10 candidates (an exhaustive survey should be productive) | Count | +| Post-dispatch | Each candidate has a Tier 0 grep evidence path | Spot check 3 candidates | +| Pre-handoff | Wave plan respects the file-overlap matrix (no parallel candidates touching the same file) | Cross-check matrix | +| Pre-handoff | User has reviewed the roster and picked scope | Explicit user response | + +## Cross-references + +- `actions/batch.md` — consumes the wave plan from this action's output +- `actions/verify.md` — promotes each candidate from synthesis to Tier 0 before `plan`/`execute` +- `context/decision-framework.md` — output type decision matrix consumed in survey output +- `context/lessons.md` — known-refused patterns deduplicated from new survey results +- SKILL.md "Evidence discipline" — Tier 0 definition; subagent return is synthesis by default diff --git a/plugins/docs-hygiene/skills/extract-ssot/actions/verify.md b/plugins/docs-hygiene/skills/extract-ssot/actions/verify.md new file mode 100644 index 000000000..dfcc2c12d --- /dev/null +++ b/plugins/docs-hygiene/skills/extract-ssot/actions/verify.md @@ -0,0 +1,270 @@ +# `verify` action — pre-extraction gate + +Cheap pre-extraction gate. Refuse-fast on candidates that wouldn't survive `plan`/`execute` anyway. Surfaces the refusal verdict from a single grep + citation check, without spawning a subagent. + +Private surface — external consumers invoke `/extract-ssot verify `, never cite this file directly (contract: `/encapsulation-audit`). + +## When to invoke + +| Use case | Invoke | +|----------|--------| +| `/extract-ssot identify` produced a ranked candidate list and you want to filter before planning | YES | +| User typed `/extract-ssot verify ` directly | YES | +| Pre-batch filter inside the `batch` action | YES — automatic | +| You already have HIGH confidence the cluster passes the 6+5 gate | OPTIONAL — `plan` will re-verify Tier 0 | +| Cluster has < 3 instances (Rule of Three obvious fail) | NO — refuse via `identify` instead | + +`verify` is OPTIONAL. It does NOT gate `plan`/`execute` automatically — preserves user agency. Skipping `verify` and going straight to `plan` is supported. + +## Inputs + +```text +/extract-ssot verify +``` + +`` matches a candidate from a recent `/extract-ssot identify` output OR is a free-form descriptor of the cluster the user wants to gate. + +## Output schema + +```yaml +status: PROCEED | REFUSE-{reason} | WARN +reason-code: +evidence: + - + - +blockers: + - +next-action: +notes: +``` + +Status values: + +| Status | Meaning | +|--------|---------| +| `PROCEED` | All 6 gates pass; safe to invoke `/extract-ssot plan ` | +| `REFUSE-rule-of-three-fails` | < 3 verbatim instances after discriminating-phrase grep (Gate 1) | +| `REFUSE-already-cites-canonical` | All call sites already cite an existing canonical SSOT (Gate 2) | +| `REFUSE-primary-source-citation-gate` | Sites cite a vendor/RFC/spec URL directly; internal SSOT can't improve (Gate 3) | +| `REFUSE-source-of-truth-bifurcation` | Top-tier instruction file ↔ rule-file pair both canonical at different tiers; forcing a single citation = cycle (Gate 4) | +| `REFUSE-off-by-one-different-concern` | Surface-similar but different step counts / variant shapes signal distinct concerns (Gate 5) | +| `REFUSE-low-roi` | Single short stable claim; inline beats abstraction maintenance (Gate 6) | +| `WARN-borderline` | Gates pass but evidence is marginal (e.g. 3 instances exactly, or one gate flagged) — `plan` should include an adversarial-review round | +| `REFUSE-not-found` | Cluster name doesn't resolve to any matching content (no instances grepped) | + +## The 6 gates (ordered checks) + +Each gate has Tier 0 evidence requirements — direct grep/read output captured this turn per SKILL.md "Evidence discipline". For Gates 1–3 partial facts, run: + +```bash +bash "${CLAUDE_SKILL_DIR}/scripts/emit-verify-facts.sh" --phrase "" +``` + +Map the script output to gate evidence; emit the `status: PROCEED | REFUSE-* | WARN` YAML in the skill — the script never emits verdicts. + +### Gate 0: Cluster resolution + +Before any gate runs, confirm the cluster exists in the repo. + +- Step 1: identify a discriminating phrase from the cluster body (≥ 8 words, unique enough to grep cleanly) +- Step 2: grep for the phrase across the repo's tracked markdown +- Step 3: if zero hits → `REFUSE-not-found` immediately + +### Gate 1: Rule-of-Three via discriminating-phrase grep + +**Lesson 1** — keyword density over-counts; use discriminating-phrase grep instead. + +- Identify a verbatim phrase that uniquely characterizes this cluster (NOT keywords like "subagent" or "rate limit" that appear everywhere) +- Multiline grep where appropriate (use `multiline: true` for cross-line patterns) +- Count distinct **full reproductions** (not paraphrase mentions, not 1-line teaching mentions, not citation-only references) +- If < 3 full reproductions → `REFUSE-rule-of-three-fails` + +Tier 0 evidence form: + +```text +Grep pattern: '' +Files matched (full reproductions): + - : + - : + - : +Files matched (teaching mentions, excluded): + - : (1-line mention, kept inline) +``` + +### Gate 2: Pre-existing canonical citation check + +**Lesson 2** — sites already citing canonical = no extraction work remains. + +- For each call site found in Gate 1, grep the surrounding ~10 lines for an existing citation pattern: `per .md ""` or backtick-`.md` references +- Count call sites already citing canonical +- If ALL call sites already cite a canonical SSOT → `REFUSE-already-cites-canonical` +- If SOME do but not all → continue (the `execute` action would sweep stragglers); proceed to Gate 3 + +Tier 0 evidence form: + +```text +Citation grep pattern: 'per [a-z-]+\.md \"' +Sites already citing canonical: / +Canonical file(s) referenced: +``` + +### Gate 3: Primary-source citation gate + +**Lesson 6** — sites citing a vendor doc / RFC / spec URL directly outrank any internal SSOT. + +- For each call site, grep ~5 lines around it for primary-source URLs: `code.claude.com`, `platform.claude.com`, `anthropic.com`, `tools.ietf.org/rfc`, `developer.mozilla.org`, `learn.microsoft.com`, or whatever primary hosts the repo's domain relies on +- If ALL call sites cite a primary-source URL within ~5 lines → `REFUSE-primary-source-citation-gate` + +Rationale: an internal SSOT cannot improve on a primary URL the consumer already inlines. Internal SSOT is for repeated *internal-vocabulary* claims, not re-statements of primary facts. + +Tier 0 evidence form: + +```text +Primary-URL grep pattern: '(code|platform)\.(claude|anthropic)\.com|tools\.ietf\.org/rfc|learn\.microsoft\.com' +Sites citing primary directly: / +Sample URL(s): +``` + +### Gate 4: Source-of-truth bifurcation check + +**Lesson 8** — a top-tier always-loaded instruction file as source + a rule-file aggregator are both first-class canonicals at different tiers. + +Detect the bifurcation pattern: + +- Did the cluster originate in `CLAUDE.md` or `AGENTS.md` (top-tier always-loaded)? +- Is the cluster ALSO present in a scoped rule file (deep-disclosure aggregator for hook/skill/script authors)? +- If both: forcing the instruction file to cite the rule = citation cycle. Each tier serves a different audience legitimately. + +If bifurcation detected → `REFUSE-source-of-truth-bifurcation`. Document both canonicals + their respective audiences in the output `notes:` field. + +Tier 0 evidence form: + +```text +Top-tier source: : "
" +Aggregator rule: .md: "" +Audiences: + Top-tier: + Aggregator: +Forcing single citation would create: cycle (instruction file → rule → instruction file) | over-aggregation +``` + +### Gate 5: Off-by-one heuristic — different concerns + +**Lesson 3** — different step counts / variant shapes signal distinct concerns. + +- For multi-step or numbered-list clusters, compare step count across instances +- For decision-table clusters, compare row count + column structure +- For workflow chain prose, compare step names + ordering +- If counts/shapes diverge non-trivially across instances → these are NOT the same cluster; `REFUSE-off-by-one-different-concern` + +Adjacent Lesson 4 — an intentional Path 1 / Path 2 bifurcation (two related but distinct lifecycles documented side by side) is the canonical example: 2 lifecycles, intentionally distinct, must NOT collapse. + +Tier 0 evidence form: + +```text +Instance shapes: + - : steps, named + - : steps, named + - : steps, named +Divergence: +``` + +### Gate 6: LOW-ROI threshold + +**Lesson 5** — single-sentence + low-drift = inline beats abstraction-maintenance cost. + +Two heuristics combined: + +- **Size**: cluster body ≤ 1 short paragraph (≤ ~80 words OR single sentence) +- **Drift rate**: cluster content changes ≤ 1×/year (verifiable via `git log -p ` if needed) + +If BOTH true → `REFUSE-low-roi`. Inline at each call site is the disciplined call; the abstraction's overhead would dominate. + +Tier 0 evidence form: + +```text +Cluster body size: ~ words +Drift signal: git log shows edits to the cluster prose in last 12 months +ROI verdict: LOW (size + drift indicate inline is cheaper) +``` + +## Workflow + +```text +1. Read your working notes (resume if mid-phase) +2. Gate 0 — cluster resolution +3. Gate 1 — discriminating-phrase grep, count full reproductions +4. Gate 2 — pre-existing citation check +5. Gate 3 — primary-source citation gate +6. Gate 4 — source-of-truth bifurcation check +7. Gate 5 — off-by-one shape divergence +8. Gate 6 — LOW-ROI threshold +9. Emit structured output (status + evidence + next-action) +10. Optionally append a dated verify entry to the working notes (audit trail) +``` + +If ANY gate REFUSES, stop and emit. Don't run remaining gates — output the first refusal reason. (Avoids overspecified output that obscures the actual blocker.) + +If ALL gates pass, emit `PROCEED` with summary evidence. User runs `/extract-ssot plan ` next. + +## Side observations + +When a gate REFUSES with high confidence, the cluster may still warrant action — just not the action `/extract-ssot` provides. Emit ONE side observation per refusal: + +| Refusal | Side observation form | +|---------|----------------------| +| `REFUSE-already-cites-canonical` | `Side note: cluster already extracted at ; no SSOT work remains. Sweep stragglers if any?` | +| `REFUSE-primary-source-citation-gate` | `Side note: sites cite ; internal SSOT redundant. Verify URL still resolves.` | +| `REFUSE-source-of-truth-bifurcation` | `Side note: bifurcated SSOT — document the two audiences in the rule file so the split reads as intentional.` | +| `REFUSE-low-roi` | `Side note: inline + cite primary if needed; surface to user only if drift starts.` | +| `REFUSE-off-by-one-different-concern` | `Side note: distinct concerns; consider /extract-ssot identify with a narrower discriminating phrase per concern.` | + +Hard limit ≤2 side notes per response. If multiple gates fire, batch the rest into the working-notes entry. + +## Audit trail (optional) + +When `verify` runs as part of `/extract-ssot batch`, it MUST append a verify audit entry to the batch working notes so the batch summary can aggregate verdicts. When run standalone, an audit entry is OPTIONAL but recommended for non-trivial clusters. + +Entry format: + +```markdown +--- +type: verify-evidence +date: +cluster: +verdict: +--- +## Cluster + + +## Gate results +| Gate | Result | Evidence | +|------|--------|----------| +| 0 — Cluster resolution | PASS | matches | +| 1 — Discriminating-phrase grep | | | +| 2 — Pre-existing citations | | | +| ... | | | + +## Verdict + + +## Side observations +<≤2 entries> +``` + +## Cross-references + +- `context/decision-framework.md` "Pre-extraction Tier 0 checklist" — documents these 6 gates as the formalized pre-extraction discipline +- `context/lessons.md` — the empirical batch-derived patterns the gates encode +- `context/anti-patterns.md` — pattern #11 (source-of-truth bifurcation), #12 (primary-source citation gate), #13 (Shape C dedup-by-deletion, positive) +- SKILL.md "Evidence discipline" — Tier 0 evidence requirements per gate +- `/extract-ssot identify` — produces the ranked candidate list; `verify` filters that list +- `/extract-ssot plan` — runs after `verify` returns PROCEED + +## Recheck triggers + +| Condition | Action | +|-----------|--------| +| Anthropic ships a canonical refuse-fast/dry-run convention for skill actions | Re-align the gate output schema; consider bringing it under the documented contract | +| A new empirical lesson lands in `context/lessons.md` | Evaluate adding a new gate to this action | +| A gate produces consistently wrong refusals (false-negative or false-positive across 3+ batches) | Tune the Tier 0 evidence threshold; document the tuning rationale in the working notes | diff --git a/plugins/docs-hygiene/skills/extract-ssot/context/anti-patterns.md b/plugins/docs-hygiene/skills/extract-ssot/context/anti-patterns.md new file mode 100644 index 000000000..574e3302e --- /dev/null +++ b/plugins/docs-hygiene/skills/extract-ssot/context/anti-patterns.md @@ -0,0 +1,214 @@ +# Anti-patterns guarded + +13-pattern taxonomy. Each entry: pattern + symptom + mitigation procedure. SKILL.md cites this file for the full taxonomy; the body lists pattern names only. + +Patterns are framed for markdown extraction (the dominant case) but apply to code and config extractions too — citation rot has a code analog (function rename = stale `import`), over-indirection has a code analog (re-export chains), wrong abstraction is the same Sandi Metz failure regardless of language. File-class adaptations are called out per pattern below. + +## 1. Citation rot + +**Pattern.** Heading rename in the extracted file silently breaks all references. Pure-token grep on the OLD heading text returns matches in caller files; nobody updates them; the agent reads the caller's stale citation and either follows a dead link or hallucinates plausible content matching the old heading name. + +**Symptom.** Caller text reads `per X.md "Old Heading"`; X.md no longer has an H3 with that exact text. A `/rename-references` sweep would have caught it. + +**Code/config analog.** Function/constant/anchor rename in the SSOT module breaks every `import`, `using`, or `$ref` that pinned the old name. Same failure shape; the mitigation is the same sweep + IDE rename refactor. + +**Mitigation.** + +1. Cite by EXACT heading text (markdown), exact identifier (code), exact anchor (config) — never by line number or section number +2. After ANY heading/identifier/anchor edit in an SSOT, run `/rename-references` immediately — it sweeps all 10 syntactic forms, not just pure-token grep +3. The SSOT file should include a `## Recheck triggers` section — a rename row triggers the sweep +4. For code: prefer language-aware refactor (IDE / Roslyn / ts-morph) over text grep; combine with `/rename-references` for non-source references (docs, configs) + +## 2. Over-indirection + +**Pattern.** A.md → B.md → C.md chains. The reader has to follow 2-3 links to assemble meaning. Per-link failure rate compounds. + +**Symptom.** A skill body cites a rule file; the rule file cites another rule file for the same domain. Anthropic's "head -100" partial-read failure mode triggers when the chain is deep. + +**Code/config analog.** Re-export chains: `module-A` re-exports from `module-B` which re-exports from `module-C`. IDE "go to definition" jumps through layers; refactor breakage cascades. The same one-level-deep rule applies — the call site imports the canonical SSOT directly. + +**Mitigation.** + +1. Enforce "one level deep" — refuse to ship the extraction if the SSOT itself references another extracted SSOT for the same domain +2. If two SSOT files cover related domains, either merge them OR cite both directly from the caller (one level each) +3. Lint check: grep the extracted SSOT for `\.md "` patterns; warn on >2 references to OTHER `.md` files +4. Code: ban re-export-only modules (`export * from "./other"`); each module owns its own surface + +## 3. Leaky abstraction + +**Pattern.** The extracted file uses pronouns or references that assume caller context — "the prior step", "as discussed earlier", "this command above", "that flag we mentioned". + +**Symptom.** Reading the extracted file in isolation produces nonsense. Joel Spolsky's law applied to docs. + +**Code/config analog.** A helper depends on global state the caller happens to set (mutable singleton, ambient context, env var only one caller exports). A config include references variables only the includer happens to define. Same failure shape; the mitigation is to make dependencies explicit (parameters, dependency injection, required-input declaration). + +**Mitigation.** + +1. Self-test: read the extracted file in isolation and ask "does this make sense without the surrounding context I just came from?" +2. Lint pattern (markdown): detect context-assuming phrases via grep — `prior`, `earlier`, `above`, `previous`, `as mentioned`, `as discussed`, `the X we`, `that step` +3. Code: pure-function preference; explicit parameters over ambient context; document required setup at the import site +4. Config: declared inputs at the include site; no implicit variable inheritance +5. Rewrite to self-contained form OR refuse extraction + +## 4. Loss of locality + +**Pattern.** The reader has to jump 3+ files to understand intent at the call site. Cite-by-name without inline context strips ALL meaning from the caller. + +**Symptom.** Caller text reads `per X.md "Y"` and nothing else. The reader has no idea what Y does or why it matters here. + +**Code/config analog.** Bare `import { someThing } from "./util"` with no usage context near the call site, or YAML `<<: *anchor` with no comment naming what the anchor encapsulates. The reader has to chase the import to understand intent. + +**Mitigation.** + +1. Cite-by-name AND inline 1-line summary at every call site. Template: `per .md "" — <≤80 char shape description>` +2. The 1-line summary should let a reader skim the caller and understand the SHAPE of the cited rule without clicking through +3. Code: name imports for what they do, not where they live; cluster related imports; brief comment at non-obvious call sites +4. Config: name anchors descriptively (`&dotnet-build-defaults` not `&base`); short comment above the alias if intent isn't obvious +5. Full markdown contract in `citation-form.md` + +## 5. Reference resolution failure + +**Pattern.** Heading renamed in the SSOT; the agent searches the caller's old citation text against the SSOT, fails to find it, hallucinates plausible content matching the old heading name. + +**Symptom.** Same observable as citation rot (#1) but from the AGENT's perspective at read time. The agent confidently produces output as if the citation resolved successfully when it didn't. + +**Mitigation.** + +1. The SSOT file ships with a `## Stable headings — change requires sweep-references` section listing exact anchor text + dependent call sites +2. Verify before acting: when a citation says `per X.md "Y"`, the agent MUST grep X.md for the literal heading "Y" before acting on assumed content +3. If citation-resolution hallucination becomes measurable, add resolution-time verification tooling (a hook or lint that greps the cited heading on read/write) + +## 6. Wrong abstraction (Sandi Metz failure mode) + +**Pattern.** The extracted SSOT has 5+ callers, each passing different boolean flags or conditional branches. Params + conditionals proliferating IS the failure signal. + +**Symptom.** Caller code reads `/ --variant=Q` with each caller using a unique combination. The SSOT body is a switch-statement of `if flag-A then ... else if flag-B then ...`. + +**Mitigation.** + +1. The `unwind` action implements Metz's 3-step recovery: + - Re-introduce duplication by inlining the SSOT body back into every caller + - Within each caller, keep only the subset that caller actually needs + - Delete unneeded bits per caller + - Re-isolate genuine duplication and re-extract with corrected shape +2. Record the unwind decision in the working notes (it is a hard-to-reverse call worth documenting) +3. After unwind, re-run the `identify` action; only re-extract if Rule of Three still holds with the corrected shape + +## 7. Premature extraction + +**Pattern.** Extracting at the first or second instance of perceived duplication. Two prompts do similar (not identical) things; the agent extracts to a "shared helper" before a third instance proves the pattern is real. + +**Symptom.** The SSOT has 2 callers; one of them is awkward because the SSOT shape was guessed from one strong example + one weak example. + +**Mitigation.** + +1. The `identify` action requires evidence of 3+ instances before recommending extraction — Tier 0 grep output captured this turn, not recall +2. Refuse extraction when only 2 instances exist; cite Rule of Three with author attribution (Don Roberts / Fowler) +3. Offer to record a tracking note in the working notes so future-self knows to revisit when the third instance lands + +## 8. Self-generated SSOT + +**Pattern.** The model authors a skill or rule that the model itself cannot reliably consume. SkillsBench negative finding: self-generated skills provide NO benefit on average; human curation is the only reliable path. + +**Symptom.** The SSOT was written end-to-end by an agent without human review at any phase boundary. Eval cases (if any) were also model-authored. Failure rate higher than ad-hoc inline. + +**Mitigation.** + +1. SSOT output goes through human review — the user stages, commits, and reviews the diff +2. Phase boundaries surface the diff to the user explicitly; never auto-stage/commit/push +3. Eval cases for any new skill MUST be human-reviewed against expected output before declaring done + +## 9. Cache invalidation cascade + +**Pattern.** Extraction creates a new always-loaded file that gets edited often; downstream sessions' prompt caches invalidate on every edit; token cost rises for every session that loads it. + +**Symptom.** Cache-creation token volume rises relative to baseline in usage telemetry; cache hit rate for sessions in the repo drops after the SSOT lands. + +**Mitigation.** + +1. Decision-framework test #3 (Stable — content changes <1×/quarter) is the up-front gate +2. If the SSOT must be edited frequently, split it: stable categorical bits stay in the SSOT, volatile narrative goes back inline +3. A Recheck-triggers section in the SSOT documents anticipated edit frequency; if it drifts >1×/month, raise it as a side observation + +## 10. Encapsulation violation + +**Pattern.** A caller references skill internals (`.claude/skills//scripts/`, `/context/.md`, `/actions/.md`, `/reference/.md`) instead of the `/X` invocation. Bypasses the skill's public-API contract; ties the caller to internal layout that may move. + +**Symptom.** Grep `.claude/skills/[^/]+/(context|actions|reference)` against caller files returns matches. The caller reads file content directly rather than invoking the skill action that uses it. + +**Mitigation.** + +1. The `execute` action converts external skill-internals refs back to `/X` invocations as part of the work, NOT preserved +2. `scripts/*.sh` is the documented public-API exception — those CAN be cited externally (see `/encapsulation-audit`) +3. If the caller's use case has no public action covering it, surface as a side observation (NOT fix-in-passing) — the skill needs an action added before the caller can route through the public API +4. Detection grep + remediation paths: `/encapsulation-audit` + +## 11. Source-of-truth bifurcation (REFUSE trigger) + +**Pattern.** A concept legitimately exists at TWO tiers — a top-level always-loaded source (`CLAUDE.md` / `AGENTS.md`) for the every-session audience, AND a deep-disclosure aggregator rule file for hook/skill/script authors who need detection mechanics or implementation detail. Both are first-class canonicals serving distinct audiences. Forcing the instruction file to cite the rule creates a citation cycle. + +**Symptom.** A survey claims "N inline reproductions of " but Tier 0 grep shows 1 reproduction in the instruction file (top-tier canonical) + 1 in a rule file (aggregator) + N-2 single-concern call sites that need only a slice. Forcing single-citation extraction collapses two legitimate canonicals into one. + +**Code/config analog.** The same fact lives in a public README (top-tier audience) AND a developer-guide reference doc (deeper audience); collapsing the README to cite the dev-guide breaks the README's stand-alone value for the entry-point audience. + +**Mitigation.** + +1. The `verify` action Gate 4 detects bifurcation; refuses extraction with `REFUSE-source-of-truth-bifurcation` +2. Document both canonicals + their respective audiences in the rule file if not already explicit +3. Single-concern call sites can still cite either canonical (whichever serves their narrower scope) — keep their narrow-slice usage rather than forcing whole-fact citation +4. **Verbatim source.** `lessons.md` Lesson 8. + +## 12. Primary-source citation gate (REFUSE trigger) + +**Pattern.** All call sites already cite a primary-source URL directly (vendor doc, RFC, language spec). An internal SSOT cannot improve on a primary URL the consumer already inlines. + +**Symptom.** Pre-extraction grep shows every call site contains a primary-source URL within ~5 lines. The verbatim fraction across sites is near 0% because each site states the primary fact in its concern-specific framing. + +**Code/config analog.** Code already imports a typed constant from a third-party package's exported API; introducing a wrapper file that re-exports the constant adds indirection without value (the compiler already pins the name). + +**Mitigation.** + +1. The `verify` action Gate 3 detects primary-URL coverage; refuses with `REFUSE-primary-source-citation-gate` +2. Internal SSOT is for repeated *internal-vocabulary* claims (where there IS no primary source), NOT re-statements of facts the primary publisher owns +3. If centralization is desired anyway, extract at MOST a 1-line "prescribed upstream: " statement, NOT the multi-form derivation +4. **Recheck trigger:** if the primary URL goes 404, all sites need a fallback; that's the moment to revisit +5. **Verbatim source.** `lessons.md` Lesson 6. + +## 13. Shape C — dedup-by-deletion (POSITIVE pattern) + +**Pattern (positive — applies when the cluster IS already SSOT-shaped).** When an existing canonical SSOT already documents the full content and consumer files paraphrase that content as a TL;DR, the right action is NOT extraction (it already exists) but DELETION of the redundant paraphrasers. Keep the load-bearing directive (e.g. `Read X.md first`); delete the redundant TL;DR tail prose. + +**Symptom.** The cluster body across N consumer files reads as a TL;DR / restatement of an existing canonical's intro paragraph. Consumers cite or reference the canonical but ALSO restate its content nearby. Extraction would be a no-op because the SSOT exists; the redundancy is in the consumers. + +**Code/config analog.** A code helper exists; consumers `import` it AND inline a copy of the body "for clarity"; the inline copy is dead weight — delete it, the import is sufficient. + +**When to apply.** + +- An existing rule file or skill body already serves as the SSOT +- 3+ consumers paraphrase that SSOT's content while ALSO referencing the SSOT by name +- The paraphrase adds zero unique signal (it's strictly a restatement) +- Per-consumer intentional deltas (e.g. prompt-specific phase pointers, custom constraints) are NOT in the redundant body and stay inline + +**Mitigation / execution.** + +1. The `verify` action Gate 2 (pre-existing canonical citation) is the entry point — if it returns `REFUSE-already-cites-canonical` AND the consumer ALSO has a redundant paraphrase nearby, that's the Shape C signal +2. Run a deletion-only sweep: for each consumer, identify the redundant TL;DR tail; delete; preserve load-bearing directives + per-consumer intentional deltas +3. No new file. No `/rename-references` sweep (no identifier change). Pure dead-text removal +4. **Verbatim source.** `lessons.md` Lesson 9. Canonical example shape: a dozen automation prompts each carried a redundant one-line descriptor tail restating a shared doc that was already canonical with the full content; deleting the tails was correct + +**When NOT to apply.** + +- The consumer paraphrase carries unique framing (concern-driven drift per Lesson 7) — keep inline +- The "SSOT" is itself just a paraphrase of a primary-source URL (Lesson 6) — don't grow the redundancy +- The consumer is intentionally short-form (1-line teaching mention, not full restatement) — leave as-is per Lesson 1's exclusion + +## Cross-references + +- `decision-framework.md` — when to extract (avoiding patterns 6-9 up front); "Pre-extraction Tier 0 checklist" formalizes #11/#12/#13 +- `citation-form.md` — anti-patterns 1, 4, 5 mitigation contract for markdown call sites; for code/config see SKILL.md "Output type" +- `/encapsulation-audit` — anti-pattern 10 detection + remediation matrix (separate skill) +- `execution-checklist.md` — per-phase sanity checks that catch each anti-pattern +- `lessons.md` — empirical batch-derived patterns; #11 ↔ Lesson 8, #12 ↔ Lesson 6, #13 ↔ Lesson 9 +- `actions/verify.md` — refuse-fast gates implementing anti-patterns #11 (Gate 4), #12 (Gate 3), #13 (Gate 2 entry-point); Gates 1/5/6 implement Lessons 1/3+4/5 (informational, not refuse-anti-patterns) +- SKILL.md "Evidence discipline" — verify-before-acting for citation resolution (#5); Tier 0 evidence requirement for the #11/#12/#13 detection greps +- `/rename-references` — the 10-pattern sweep that catches #1, #4, #5 diff --git a/plugins/docs-hygiene/skills/extract-ssot/context/citation-form.md b/plugins/docs-hygiene/skills/extract-ssot/context/citation-form.md new file mode 100644 index 000000000..1229d79e3 --- /dev/null +++ b/plugins/docs-hygiene/skills/extract-ssot/context/citation-form.md @@ -0,0 +1,138 @@ +# Citation form + +Exact contract for how callers cite an SSOT after extraction. Applies to markdown citations between rule files / skills / docs. (Code and config callers use the language's idiomatic form — `import`, `using`, YAML anchor, JSON `$ref` — not this contract.) + +SKILL.md cites the headline form; this file covers the full template, line-wrap edge case, and rename discipline. + +## Headline contract + +Cite by **exact H3 heading text** + **1-line inline summary** at every call site. Template: + +```text + per `.md` "". +``` + +Concrete shapes the template produces: + +```text + per `.md` "". + per `.md` "". + per `.md` "". +``` + +Three rules baked in: + +1. **Backticked filename** — `` `.md` `` not `.md`. Disambiguates filename from prose; lets grep find the citation deterministically +2. **Quoted heading text** — `""` not `` or `''`. Quote characters are stable; heading capitalization matches the SSOT exactly +3. **One level deep** — never chain `A.md` → `B.md` → `C.md`. If the SSOT references another SSOT for the same domain, fix that first (anti-pattern #2 over-indirection) + +## 1-line inline summary template + +For non-trivial citations, append a 1-line summary AFTER the citation so the reader can skim the caller and understand the shape without clicking through: + +```text + per `.md` "" — <≤80 char shape description>. +``` + +Examples of the shape pattern (substitute the actual rule file and heading at the call site): + +- A workflow step citation might look like: ` per .md "" — short cadence/cycle description.` +- A naming citation might look like: ` per .md "" — short rule shape (kebab-case, 40-char cap, etc).` + +Summary format: `— <≤80 char description>`. Em-dash separator preferred over colon (visual scan). Aim for the SHAPE of the cited rule, not its full content. + +When to inline a summary: + +| Caller context | Summary needed? | +|----------------|-----------------| +| Citation appears once in a body paragraph | YES — reader hits cold context | +| Citation appears in a Cross-references section list | NO — section context already orients the reader | +| Citation is repeated within ~50 lines of the same caller | First citation YES, subsequent NO (reader has the context) | +| Citation is in a table cell | YES if the cell is the caller's only reference; NO if the cell is one of many short references | + +## Line-wrap edge case + +Heading text MUST stay on one line at the citation site, even if it pushes the surrounding sentence past line width. The literal heading is the grep token; line-wrapped citations are unrecoverable. + +❌ Wrong (heading wraps across lines): + +```text + per `.md` ". +``` + +✅ Right (heading on one line; rest wraps freely): + +```text + per `.md` +"" — . +``` + +If the heading itself contains characters that confuse grep (parens, em-dashes, quote marks), the SSOT author should rename the heading to something simpler. Heading text should be greppable as-is. + +## Rename discipline + +Headings in an SSOT file are stable contracts. After ANY heading edit in an SSOT, run `/rename-references` immediately — it sweeps all 10 syntactic forms including: + +| Form | Example | Pure-token grep catches? | +|------|---------|--------------------------| +| Direct citation | `per X.md "Y"` | YES | +| Chain prose | `the Y rule (X.md "Y")` | NO — wrap form | +| Comma-list | `X.md headings "Y", "Z", "W"` | NO — needs context match | +| Numbered table row | `\| 3 \| Y \| ...` | NO — table-row form | +| Frontmatter chain | `extends: ../X.md#Y` | NO — frontmatter form | +| Frontmatter glob | `paths: [".claude/rules/X.md"]` | NO if heading-scoped | +| Cross-skill mode | `/ mode Y` | NO — verb form | +| Mention-only | `the Y heading` | NO — context-dependent | +| Heading definition | `## Y` (the SSOT itself) | YES | +| Anchor URL | `X.md#y` | YES if exact-case | + +`/rename-references` runs all 10. Pure-token grep alone misses 6+ forms. + +## Worked examples + +### Headline form across multiple call sites (good) + +When several call sites cite different verbs from the same vocabulary rule, every citation uses the same shape: + +```text + per `.md` "". + per `.md` "". + per `.md` "". + per `.md` "". +``` + +Same shape across all — easy to grep, easy to rename, easy to skim. + +### With 1-line summary (good) + +When a skill orchestrates a multi-skill flow and cites another skill's mode: + +```text + per `/SKILL.md` "" — . +``` + +Reader skims, sees the scope phrase, understands what the cited mode will do without clicking. + +### Refused (anti-pattern: chain prose) + +❌ Don't write: + +```text +The pattern (covered in `.md` "", +which itself cites `.md` for the underlying shapes) ... +``` + +The chain prose form hides the citation in narrative; pure-token grep can find the heading but the rename sweep needs context match. Rewrite as a direct citation to the canonical SSOT: + +```text + per `.md` "". +``` + +## Cross-references + +- `decision-framework.md` — 6-test extraction gate + 5-test keep-inline gate + output-type criteria +- `anti-patterns.md` #1 (citation rot), #4 (loss of locality), #5 (reference resolution failure) — failure modes this contract guards against +- `/encapsulation-audit` — citation form for skill internals (cite the `/skill-name` invocation, not an internal file path) +- `/rename-references` — owns the full 10-pattern sweep specification +- Numbered references — write the full `docs//-.md` path, not a bare number shorthand like "ADR-NNNN" (number-only shorthand collides) diff --git a/plugins/docs-hygiene/skills/extract-ssot/context/decision-framework.md b/plugins/docs-hygiene/skills/extract-ssot/context/decision-framework.md new file mode 100644 index 000000000..ef43e9067 --- /dev/null +++ b/plugins/docs-hygiene/skills/extract-ssot/context/decision-framework.md @@ -0,0 +1,163 @@ +# Decision framework: extract vs inline vs skill vs rule + +Full 6-test extraction gate + 5-test keep-inline gate, plus output-type criteria. SKILL.md cites the headline gate (Rule of Three) and links here for the full matrix. + +Applies to any repeated text content — markdown (rules, skills, docs), code (constants, helpers, types), config (CI workflows, settings, MCP entries), or mixed clusters that span all three. The principle is the coding Rule of Three / DRY: extract when 3+ instances exist; otherwise inline. + +## EXTRACT into shared SSOT only when ALL six tests pass + +| # | Test | Why | Evidence | +|---|------|-----|----------| +| 1 | **Rule of Three** — duplication appears in 3+ places | Premature abstraction creates the wrong-abstraction trap; 1-2 instances are usually coincidence, not pattern. Same principle whether the duplicated unit is a markdown heading, a string constant, a helper function, or a CI step | Don Roberts / Fowler *Refactoring* §1; Sandi Metz "The Wrong Abstraction" | +| 2 | **Namable as a stable canonical unit** — the cluster has an identity that can be given one name and referenced by that name | For markdown: a heading or rule name. For code: a function/constant/type identifier. For config: an anchor/include/`$ref` target. Without a stable name, callers can't cite/import unambiguously and the SSOT becomes a grab-bag. For markdown specifically, the unit should also be categorical (vocabulary, constraints, IF-THEN) rather than nuanced reasoning — MDEval finding: providing an external markdown reference does NOT improve a model's Markdown Awareness vs well-designed inline rules ("feeding a reference to an LLM does not bring any benefit for Markdown Awareness; this unexpected finding challenges prevalent assumptions") | Anthropic best-practices "Avoid offering too many options"; MDEval arxiv 2501.15000; Endor Labs anti-pattern avoidance (64% reduction with categorical extraction) | +| 3 | **Stable** — content does NOT change more than 1×/quarter | Volatile SSOTs invalidate downstream sessions' prompt caches on every edit (an always-loaded file that changes often is a cache-miss generator); high churn also drives heading/identifier rename frequency, which compounds the citation-rot risk captured in test #6 below. Code-side equivalent: high churn means callers chase signature changes constantly | Anthropic prompt caching docs (cache TTL hinges on stability — [platform.claude.com/docs/en/build-with-claude/prompt-caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)); Sandi Metz wrong-abstraction (volatile = signal that the abstraction shape is not yet stable) | +| 4 | **Self-contained** — content has no implicit dependency on caller context | Leaky abstraction = silent failure. For markdown: the extracted block must not say "the prior step" or "as discussed earlier". For code: the helper must not depend on global state the caller happens to set. For config: the include must not reference variables the includer happens to define | Joel Spolsky "Law of Leaky Abstractions"; elements.cloud agent-instruction antipatterns | +| 5 | **Bounded size** — extracted markdown file < 500 lines; extracted code module sized per language idiom | Anthropic's documented best-practice guideline ("Keep SKILL.md body under 500 lines for optimal performance"); over-long files force partial reads and downstream-session cache pressure. For code: each language has its own conventions (small composable modules over monoliths) | Anthropic best-practices "Keep SKILL.md body under 500 lines for optimal performance"; GitHub Copilot 4000-char hard truncation | +| 6 | **One level deep** — referenced directly from caller, never via another reference | Markdown: A.md → B.md → C.md chains compound failure rates (5-20% per step). Code/config: the equivalent rule is no transitive re-export chains; the call site imports/cites the canonical SSOT directly | Anthropic best-practices "Avoid deeply nested references" | + +ALL six must pass. Failing one means: keep inline, OR refactor the candidate before extracting (e.g. split categorical bits from reasoning bits, then extract only the categorical bits). + +## KEEP INLINE when ANY of these is true + +| # | Test | Why | +|---|------|-----| +| A | Single use site OR < 3 instances of duplication | Premature abstraction; wait for the third instance | +| B | Markdown content is reasoning, decision logic, or tradeoff explanation | Indirection breaks down on nuanced content (MDEval finding); for code/config this test usually doesn't apply because the unit is mechanical | +| C | Content is volatile (changes more than monthly) | Citation rot guaranteed (heading/function/anchor renames); cache-invalidation cascade for downstream sessions | +| D | Instances differ in non-trivial ways and would force conditionals/flags inside the SSOT | Sandi Metz wrong-abstraction trap; params/conditionals proliferating IS the signal that the unit is NOT yet stable | +| E | Cluster is small (< ~10 lines) AND specific to one consumer's behavior | Indirection cost > duplication cost; the cite-by-name / import overhead wins | + +Any ONE failure here = keep inline. Don't extract just because you can. + +## Pre-extraction Tier 0 checklist (lessons-derived discipline) + +After the 6-test gate passes a-priori, run these 6 empirical checks BEFORE writing any SSOT file. Each maps to a `context/lessons.md` lesson + an `actions/verify.md` gate. They're additive to the 6+5 framework above — the framework is "is this extractable in principle?", this checklist is "should we actually extract right now, given how the codebase already cites things?". + +Run via `/extract-ssot verify ` for a one-shot result, or apply manually for fast informal checks. + +| # | Check | Lesson | Verify gate | Refuse signal | +|---|-------|--------|-------------|---------------| +| A | **Discriminating-phrase grep, not keyword density** | Lesson 1 | Gate 1 | Keyword count > 3 but full-reproduction count < 3 → REFUSE-rule-of-three-fails | +| B | **Pre-existing canonical citation check** | Lesson 2 | Gate 2 | All N call sites already cite `per .md ""` → REFUSE-already-cites-canonical (no work remains) | +| C | **Primary-source citation gate** | Lesson 6 | Gate 3 | All call sites cite a vendor/RFC/spec URL within ~5 lines → REFUSE-primary-source-citation-gate (internal SSOT can't improve) | +| D | **Source-of-truth bifurcation check** | Lesson 8 | Gate 4 | Cluster originates in a top-tier instruction file AND an aggregator rule file with deeper detail → REFUSE-source-of-truth-bifurcation | +| E | **Off-by-one heuristic — different concerns** | Lessons 3+4 | Gate 5 | Step counts / variant shapes diverge non-trivially across instances → REFUSE-off-by-one-different-concern; intentional Path 1/Path 2 bifurcations are preserved | +| F | **LOW-ROI threshold** | Lesson 5 | Gate 6 | Single-sentence body (≤80 words) AND drift ≤ 1×/year → REFUSE-low-roi (inline beats abstraction maintenance) | + +If a check fails → REFUSE the extraction with the matching reason code; do NOT silently proceed. Document the refusal in your working notes so future-self knows the cluster was evaluated and rejected with cause. + +If ALL checks pass → proceed to the `architect-plan` phase. + +## REFACTOR when extraction is wrong (Metz unwind) + +Per Sandi Metz "The Wrong Abstraction" (2016), reverse the extraction in three steps: + +1. Re-introduce duplication by inlining the abstracted content back into every caller +2. Within each caller, keep ONLY the subset that caller actually needs (delete bits that aren't used) +3. Re-isolate genuine duplication and re-extract with corrected shape + +The `unwind` action implements this. Trigger signal: the SSOT has 5+ callers passing different boolean flags or conditional branches — params + conditionals proliferating IS the wrong-abstraction signal. + +## Output type: rule file vs skill + +Once the 6-test gate passes, choose the SSOT shape. **First check whether an existing file already owns the concept** (top row) — if so, consolidate into it rather than creating anything. The first four rows are the markdown branch this skill ships a citation contract for; the bottom three rows are escape-hatch cases the skill flags during `identify` but defers to language-idiomatic tooling (compiler / linter / IDE refactor / schema-validate are the rename safety net there, not this skill's `/rename-references` sweep). + +| Shape | Target | Trigger signals | +|-------|--------|-----------------| +| **Consolidate into existing SSOT home** (markdown) | The existing rule / skill body / doc that already owns the concept — extend it only where a consumer carries nuance the home lacks; create no new file | An existing canonical already documents the concept AND consumers recap it inline instead of citing. Positive output-type form of what `verify` Gate 2 (`REFUSE-already-cites-canonical` fires only when ALL sites already cite) and anti-pattern Shape C (dedup-by-deletion) describe remedially; `identify` flags it as `edit-existing-rule` / `trim-to-citation`. Migration = add citations + delete the recaps; the `/rename-references` sweep is a no-op unless a heading changes | +| **Rule file** (markdown) | Wherever the consuming repository's own conventions place shared rules — default `.claude/rules/.md` (always-loaded) OR a path-scoped rule file | Vocabulary, IF-THEN rules, hard constraints, ≤500 lines, consumers cite by H3 heading and don't need procedural orchestration | +| **New skill** (markdown + workflow) | `.claude/skills//SKILL.md`, authored via the consumer's skill-authoring workflow (e.g. the skill-creator plugin) | Workflow with 3+ discrete actions, has its own anti-patterns/evals, consumers invoke `/` to run the workflow rather than read content | +| **Extend existing skill** | New action on an existing action-router skill | The workflow maps cleanly onto an existing skill's concern — same domain, same triggers, same output surface — rather than warranting a new top-level skill | +| **Code module / constants file** | Idiomatic location per language (constants file, shared module, helper class) | Repeated literal, magic number, regex, helper function in source code; callers import by name | +| **Config include / anchor** | Reusable workflow, composite action, YAML anchor + alias, JSON `$ref`, settings include | Repeated stanza in CI / MCP / settings; the tooling supports the include construct | +| **Mixed-canonical** | One canonical owner (usually code or schema), with cross-references from other file classes | Cluster spans 2+ file classes for the same conceptual unit; the canonical definition lives where the runtime authority lives | + +Skill-vs-rule heuristic: if the SSOT body is mostly nouns (named units the caller cites), it's a rule file or constants module. If the SSOT body is mostly verbs (steps the caller invokes), it's a skill. + +## Worked examples + +Each example is generic — pattern-shaped, not tied to one specific extraction. Substitute the actual cluster names and counts when running the framework on a real cluster. + +### Example 1: markdown vocabulary → rule file (PASSES gate) + +**Cluster:** A vocabulary of CLI verbs / API shapes appears inlined across many prompts, skills, and rules. Each call site reproduces some variation of the same command shape with the same flags. + +**6-test gate:** + +1. ✅ Rule of Three — well above 3 instances +2. ✅ Namable — each verb has a stable name (the CLI subcommand); content is categorical +3. ✅ Stable — verb signatures change roughly yearly +4. ✅ Self-contained — each verb is independently usable +5. ✅ Bounded — verb table fits well under 500 lines +6. ✅ One level deep — callers cite directly + +**Output:** new rule file at the repo's conventional shared-rule location (default `.claude/rules/.md`) with one H3 per verb. Callers migrate to `per .md "".` Local literal values (PR numbers, run IDs, paths) stay at the call site; only the verb shape moves to the SSOT. + +### Example 2: code constants → shared module (PASSES gate) + +**Cluster:** The same string literal (or magic number, or regex) appears in many source files. Examples: an error message text, a default timeout, a path prefix, a header name. + +**6-test gate:** + +1. ✅ Rule of Three — 5+ source files +2. ✅ Namable — gets a clear identifier (e.g. `DefaultTimeout`, `AuthHeaderName`) +3. ✅ Stable — value rarely changes; when it does, all callers must change together +4. ✅ Self-contained — value depends only on itself +5. ✅ Bounded — single declaration +6. ✅ One level deep — callers import the constant directly, no re-export chains + +**Output:** constants module / static class / enum at the language's idiomatic location. Callers replace literals with the named import. Compiler/linter catches missed call sites — citation rot is structurally prevented in code, unlike markdown. + +### Example 3: mixed cluster spanning code, doc, and config (PASSES gate) + +**Cluster:** The same identifier (e.g. an environment variable name, default port, feature-flag key) appears as a string literal in source files, an example in documentation, and a default value in CI / settings YAML. + +**6-test gate:** + +1. ✅ Rule of Three — at least one instance per file class +2. ✅ Namable — the identifier is the name; the canonical definition lives wherever the runtime owner is (usually code) +3. ✅ Stable — env var / port / flag key changes rarely +4. ✅ Self-contained — the identifier means the same thing in every context +5. ✅ Bounded — the canonical definition is one line / one row +6. ✅ One level deep — each call site references the canonical location directly + +**Output:** ONE canonical definition (typically in code as a constant or in a settings schema), THEN call sites in other file classes cite/reference it in their native form — code via import, doc via inline mention, config via include / environment substitution. The SSOT artifact may be small (one constant), but the migration touches every file class. + +### Example 4: large cluster with workflow shape → skill (PASSES gate, but procedural) + +**Cluster:** Multiple prompts or scripts duplicate a multi-step orchestration (e.g. poll-for-event → classify → react → confirm). Variants differ in params and side-step ordering. + +**6-test gate:** + +1. ✅ Rule of Three — 5+ instances +2. ⚠️ Namable — yes, but the unit is procedural (verbs), not categorical (nouns) +3. ✅ Stable — workflow shape stable across variants +4. ✅ Self-contained — given params at invocation +5. ⚠️ Bounded — body pushes against 500 lines if all variants are captured inline; needs progressive disclosure to `context/` +6. ✅ One level deep + +**Output:** new skill at `.claude/skills//SKILL.md` with an action menu — NOT a rule file. The skill body holds the orchestration shape; longer reference content goes to `context/`. Callers refactor to `/` invocations with per-instance params. + +Skill-vs-rule heuristic restated: if the SSOT body is mostly nouns (named units the caller cites), it's a rule file or constants module. If the SSOT body is mostly verbs (steps the caller invokes), it's a skill. + +### Example 5: borderline → defer (FAILS gate) + +**Cluster:** Two files have similar (not identical) snippets. A third instance might appear later, or might not. + +**6-test gate:** + +1. ❌ Rule of Three — only 2 instances +2. ✅ Namable +3. ✅ Stable +4. ✅ Self-contained +5. ✅ Bounded +6. ✅ One level deep + +**Output:** REFUSE extraction. Cite Rule of Three. Offer to record a tracking note in the working notes so future-self knows to revisit when the third instance lands. Do NOT silently proceed — premature abstraction is a wrong-abstraction trap that's expensive to reverse. + +## Cross-references + +- SKILL.md "Output type" — the canonical markdown summary of the output-type table +- SKILL.md "Evidence discipline" — Rule of Three evidence MUST be grep output captured this turn, not recall +- `context/lessons.md` — the empirical observations behind the Pre-extraction Tier 0 checklist +- `actions/verify.md` — the refuse-fast gates implementing the checklist diff --git a/plugins/docs-hygiene/skills/extract-ssot/context/execution-checklist.md b/plugins/docs-hygiene/skills/extract-ssot/context/execution-checklist.md new file mode 100644 index 000000000..daf5f1c67 --- /dev/null +++ b/plugins/docs-hygiene/skills/extract-ssot/context/execution-checklist.md @@ -0,0 +1,107 @@ +# Execution checklist + +Per-phase checks for the `execute` action. SKILL.md ships the phase header (identify-cluster → architect-plan → execute-migration → sweep-references → verify); this file covers what to verify before, during, and after each call site is migrated. + +Composes with `decision-framework.md` (gate before extraction), `citation-form.md` (form at each call site), `/encapsulation-audit` (remediation paths for violations), `anti-patterns.md` (failure modes to guard against), and SKILL.md "Evidence discipline" (Tier 0 evidence). + +## Pre-extraction (before writing or extending the SSOT) + +Run ALL of these. Any failure means STOP — do not proceed to writing or extending the SSOT. + +| # | Check | Evidence required | Source | +|---|-------|-------------------|--------| +| 1 | 3+ instances confirmed via grep | Tier 0 grep output captured this turn (NOT recall); count + file list recorded in the working notes | SKILL.md "Evidence discipline" + `decision-framework.md` test #1 | +| 2 | All 6 extraction-gate tests pass (Rule of Three, namable, stable, self-contained, bounded size, one level deep) | Decision-framework checklist marked in the working notes; one bullet per test with PASS/FAIL annotation | `decision-framework.md` "EXTRACT into shared SSOT only when ALL six tests pass" | +| 3 | Output type chosen | Consolidate-into-existing-home / rule file / skill / new action decided; rationale recorded in the working notes | `decision-framework.md` "Output type: rule file vs skill" | +| 4 | Stable headings list drafted | Headings the SSOT will expose are pre-named; the working notes capture them so callers can be migrated atomically | `citation-form.md` "Rename discipline" | +| 5 | Encapsulation classification done | Each consumer marked as "vocabulary citer" (cite-by-name OK) vs "behavior caller" (route via `/name` needed) | `/encapsulation-audit` | +| 6 | User has reviewed the plan | Phase-boundary user gate: the user diff-reviewed the plan before execute; never auto-stage or auto-commit | SKILL.md "What this skill does NOT do" | + +If any check fails, record a defer decision in the working notes and stop. Do NOT silently proceed. + +## Per-callsite (during migration of each consumer) + +Run for EACH call site. The callsite list is locked in the working notes from the pre-extraction phase. + +| # | Check | Evidence | Source | +|---|-------|----------|--------| +| 1 | Citation/import in form native to the call site's file class | Markdown: `` per `.md` "Y" `` form. Code: native `import` / `using` / `source`. Config: YAML anchor / JSON `$ref` / build-tool include | `citation-form.md` "Headline contract" (markdown form); language-idiomatic for code/config | +| 2 | Exact identifier match (heading / function / anchor) — no fuzzy or positional refs | Diff inspection | `anti-patterns.md` #1 (citation rot) | +| 3 | 1-line inline summary present where context needed | Markdown: `— ` after the citation in body paragraphs (Cross-references sections OK without summary). Code: descriptive import name + brief comment at non-obvious call sites. Config: descriptive anchor name | `citation-form.md` "1-line inline summary template" | +| 4 | One level deep — citation does NOT chain through another SSOT | Diff inspection; the SSOT itself does not cite another SSOT for the same domain. Code: no re-export-only modules | `anti-patterns.md` #2 (over-indirection) | +| 5 | Heading text on one line at the call site (markdown only) | Diff inspection | `citation-form.md` "Line-wrap edge case" | +| 6 | Encapsulation violation handled | If the consumer's content was promoted out of a skill: citation rewritten to the new home. If routed: the caller invokes `/ ` instead of reading an internal file. If no public action exists: side observation filed, NOT a silent workaround | `/encapsulation-audit` | +| 7 | No leaky-abstraction context-bleed introduced | Markdown: no `prior`, `earlier`, `above`, `as discussed`, `the X we mentioned` referring outside the call site. Code: no implicit dependency on caller-side global state. Config: no implicit variable inheritance | `anti-patterns.md` #3 (leaky abstraction) | +| 8 | Lint clean on the edited file (per file class) | Markdown: `npx markdownlint-cli2 ` (or the repo's markdown linter) exits 0. Code/config: the repo's language-native linter | The consuming repository's lint conventions | + +If a callsite fails any check, fix in place before moving to the next callsite. Do NOT batch failures across callsites — single-callsite review is the smallest reviewable unit. + +## Sweep references (after all callsites migrated) + +After every callsite is migrated, run the rename sweep across the WHOLE repo to catch citations that weren't in the pre-extraction inventory. + +**Consolidate-into-existing-home branch:** when the migration only adds citations to a stable existing heading (no identifier renamed), the `/rename-references` sweep is a no-op — skip it; the work is straggler-migration + de-recap only (consistent with `verify` Gate 2's SOME-cite straggler path and anti-pattern Shape C "no identifier change → no sweep"). Gates below that assume a freshly-written file (size bound, "SSOT file exists") apply to creation outputs only. + +| # | Check | Command | Source | +|---|-------|---------|--------| +| 1 | All 10 syntactic forms swept | `/rename-references` invoked with the SSOT name + each new identifier (heading / function / anchor) as renames-of-record | `/rename-references` (owns the 10-pattern sweep) | +| 2 | Pure-token grep returns no orphans | `grep -rn 'OldText\|OldIdentifier'` across all tracked files returns clean | `citation-form.md` "Rename discipline" | +| 3 | New SSOT is grep-discoverable | `grep -rn ''` across tracked files shows the expected callsites | Tier 0 verification | +| 4 | No violation patterns reintroduced | Re-run `/encapsulation-audit detect` | `/encapsulation-audit` | +| 5 | Code/config: language-aware refactor cross-checked | If applicable, run the IDE rename refactor and confirm the result matches the grep sweep — the IDE catches typed call sites grep misses | `anti-patterns.md` #1 | + +## Post-extraction (before declaring done) + +Final gates before the phase-boundary user gate. + +| # | Check | Evidence | Source | +|---|-------|----------|--------| +| 1 | SSOT reads sensibly in isolation (leaky-abstraction self-test) | Open the SSOT fresh; read top-to-bottom; confirm meaning is clear without surrounding context | `anti-patterns.md` #3 | +| 2 | All cross-references / imports resolve | For each `per X.md "Y"` in the new SSOT, grep X.md for the literal heading "Y" — exact match. For code: build/typecheck pass. For config: schema-validate passes | Tier 0 verification at citation resolution | +| 3 | SSOT file size within bound | Markdown: `wc -l ` < 500. Code/config: per language idiom | `decision-framework.md` test #5 | +| 4 | Lint clean across all edited files | Markdown: `npx markdownlint-cli2` (or the repo's markdown linter). Code/config: the repo's per-ecosystem linter | The consuming repository's lint conventions | +| 5 | The repo's own verification reports green for all changed ecosystems | Build + test + lint pass per the consuming repository's verification workflow | The consuming repository's verification conventions | +| 6 | Working notes updated: phase marked done + next action recorded | Status entry in the working notes | SKILL.md "Phases per invocation" | +| 7 | Handoff entry written so a fresh session can resume | Dated entry in the working notes: what changed, what's next | SKILL.md "Phases per invocation" | +| 8 | Side observations surfaced | If new candidates were discovered during execution, surface as one-line callouts (≤2 per response; never block the current task) | `actions/verify.md` "Side observations" | + +## Sanity-check format for the working notes + +Every phase ends with a Sanity Check item in the working notes. For an `execute` phase the format is: + +```markdown +- [ ] **Sanity Check:** + - SSOT file at `` exists and is < 500 lines + - All N callsites migrated (list in the phase handoff entry) + - `/rename-references` sweep ran clean + - markdownlint clean + - The repo's verification reports green + - Cross-references in the SSOT resolve to real headings +``` + +Tick the box only when ALL bullets are confirmed via direct evidence — Tier 0 (tool output captured this turn), not recall. + +## Failure recovery + +If post-extraction gates fail: + +| Failure | Action | +|---------|--------| +| Gate 1 (leaky abstraction) | Edit the SSOT to be self-contained; re-run the gate; if irrecoverable → `unwind` action | +| Gate 2 (cross-reference doesn't resolve) | Either fix the citation OR fix the SSOT heading; re-run the sweep | +| Gate 3 (>500 lines) | Split the SSOT into multiple files (one per coherent topic) OR push detail to a `context/.md` | +| Gate 4 (lint failure) | Fix lint; re-run | +| Gate 5 (repo verification red) | The failure is not out of scope — fix it before proceeding, never defer | +| Gate 6-7 (working notes not updated) | Update; re-run the gate | +| Gate 8 (no side observations surfaced when new candidates were found) | Surface as one-line callouts at the end of the response (≤2 per response) | + +If failure compounds (3+ gates fail), invoke the `unwind` action and re-evaluate via `identify` — the extraction shape was probably wrong. + +## Cross-references + +- `decision-framework.md` — pre-extraction gate that should have been passed before reaching this checklist +- `citation-form.md` — per-callsite citation contract +- `/encapsulation-audit` — per-callsite promote-vs-route decision (separate skill) +- `anti-patterns.md` — failure modes the per-callsite checks guard against +- SKILL.md "Evidence discipline" — evidence discipline for "all checks pass" claims +- SKILL.md "Phases per invocation" — working-notes persistence model diff --git a/plugins/docs-hygiene/skills/extract-ssot/context/lessons.md b/plugins/docs-hygiene/skills/extract-ssot/context/lessons.md new file mode 100644 index 000000000..55c2b5a5b --- /dev/null +++ b/plugins/docs-hygiene/skills/extract-ssot/context/lessons.md @@ -0,0 +1,197 @@ +--- +type: lessons-empirical +format: append-only +source: /extract-ssot batch executions +schema-version: 1 +--- + +# Empirical lessons — `/extract-ssot` + +Append-only record of patterns observed during real `/extract-ssot` runs. Distinct from `decision-framework.md` (a-priori 6+5 gate) and `anti-patterns.md` (failure-mode taxonomy with mitigations) — this file captures **empirical observations** from running the skill on real candidates and learning what the survey heuristic over-counts, what categorical-shape signals indicate distinct concerns, and what extraction shapes succeed vs refuse. + +Lessons 1-11 were seeded from batch runs in the repository where this skill was developed (sources below are genericized). Consuming repositories append their own lessons as batches run. + +Each lesson has a stable identifier (`Lesson N`) so anti-patterns + decision-framework + verify gates can cite by number. Appending a new lesson means adding `## Lesson N+1` at the bottom — never re-number. + +## How this file is consumed + +- `verify` action (`actions/verify.md`): the Tier 0 gates implement these lessons as ordered checks +- `anti-patterns.md` patterns #11/#12/#13: Lessons 8, 6, 9 are codified as named anti-patterns / positive patterns +- `decision-framework.md` "Pre-extraction Tier 0 checklist": lessons surfaced as a-priori discipline AFTER the 6-test gate +- Manual review: a human surveying a candidate cluster reads these to short-circuit obvious refusals + +This file is PRIVATE surface — external consumers don't cite `lessons.md "Lesson N"` directly. They invoke `/extract-ssot verify` or read the codified anti-pattern. Keeps audience boundaries clean. + +## Lesson 1: Discriminating-phrase grep beats keyword density + +**Observation.** Survey heuristics that count "any mention of " (e.g. `subagent`, `rate limit`, `worktree`) systematically over-count duplication. A keyword appearing in 21 files does NOT imply 21 reproductions of the same cluster — most are 1-line teaching mentions, not full reproductions. + +**Trigger.** Pre-extraction surveys that count via keyword density. + +**Mitigation.** Identify a verbatim phrase ≥ 8 words that uniquely characterizes the cluster body. Grep for that. Count distinct full reproductions, NOT teaching mentions. + +**Source.** A 13-candidate batch — 10 REFUSED in part because the keyword-density survey over-counted by 4-5×. Examples: workflow-chain prose (claimed 18+ instances; 1 full reproduction in the canonical), a resume-protocol cluster (claimed 6 instances; 1 full reproduction), an environment-detection cluster (claimed 21 mentions; 2 distinct full reproductions). + +**Encoded in.** `verify` action Gate 1; `decision-framework.md` Pre-extraction Tier 0 checklist; `anti-patterns.md` #7 (premature extraction) supplement. + +## Lesson 2: Pre-existing canonical citation = no extraction work remains + +**Observation.** When call sites ALREADY cite the canonical SSOT (e.g. `per X.md "Y"`), the SSOT-extraction work is already done. The candidate appears in the survey because the keyword surfaces, not because new extraction is needed. + +**Trigger.** Surveys produced from `identify` runs against repos with mature SSOT layers. + +**Mitigation.** For each call site, grep ~10 lines surrounding for `per .md ""` patterns. If ALL sites already cite canonical → REFUSE-already-cites-canonical. + +**Source.** Same batch — a merge-policy candidate (4 sites, all citing the canonical with bidirectional links); a budget-math candidate (5 sites, all citing the owning rule file); an auth-degradation-chain candidate (2 of 3 sites already citing the owning doc by heading). + +**Encoded in.** `verify` action Gate 2. + +## Lesson 3: Off-by-one step count signals distinct concerns + +**Observation.** When two clusters look superficially similar but differ in step count or variant shape, they're typically NOT the same cluster — they're distinct concerns the survey conflated. + +**Trigger.** Multi-step or numbered-list clusters that "look the same" by keyword overlap. + +**Mitigation.** Compare step counts, step names, and ordering across instances. If counts diverge non-trivially, these are different concerns; refuse extraction; flag for a narrower discriminating-phrase grep. + +**Source.** Same batch — a claimed "7-step lifecycle" candidate; the canonical lifecycle rule was 6-step. The survey's "7" came from a sibling rule documenting a different lifecycle entirely (naming gates, not the state machine). Off-by-one was the diagnostic. + +**Encoded in.** `verify` action Gate 5. + +## Lesson 4: Path 1 / Path 2 bifurcation is intentional — preserve + +**Observation.** Some sibling files document INTENTIONAL bifurcation — two related but distinct lifecycles, two related but distinct workflows. They look like duplicates to a naive sweep but collapsing them loses semantic distinction. + +**Trigger.** A cluster spans 2 sibling files in the same rules directory with similar headings but distinct subject names. + +**Mitigation.** Read both in full. If one says "Path 1" / "Path 2", or names two distinct flows in its own intro, REFUSE extraction; the bifurcation is intentional. Document why preservation matters in the working notes. + +**Source.** Same batch — a branch-naming rule's Path 1 (rename lifecycle) ↔ Path 2 (post-merge reuse lifecycle) pair. The survey conflated them with a separate 6-step lifecycle canonical; preservation was correct. + +**Encoded in.** `verify` action Gate 5 adjacent rationale; `anti-patterns.md` #6 (wrong abstraction) supplement. + +## Lesson 5: LOW-ROI threshold — single-sentence + low-drift = inline + +**Observation.** Some clusters technically pass Rule of Three (3+ instances) and pass categorical-shape (same single sentence verbatim) but the cluster body is so small + stable that abstraction maintenance dominates the duplication cost. + +**Trigger.** Cluster body ≤ 1 short paragraph (~80 words / single sentence) AND drift rate ≤ 1×/year. + +**Mitigation.** Refuse extraction. Inline at each call site. Document the LOW-ROI verdict + a recheck trigger ("if drift increases"). + +**Source.** Same batch — a squash-merge-derives-commit-from-PR-title fact (3 instances of a single sentence describing vendor product behavior with near-zero drift); a second candidate at n=2 (below Rule of Three, but the LOW-ROI argument would have applied had it passed). + +**Encoded in.** `verify` action Gate 6; `decision-framework.md` keep-inline test E supplement. + +## Lesson 6: Primary-source citation gate — don't replace primary URLs with internal SSOT + +**Observation.** When all call sites cite a primary-source URL (vendor doc, RFC, language spec) directly, an internal SSOT cannot improve on that. Internal SSOT is for repeated *internal-vocabulary* claims, not re-statements of primary facts. + +**Trigger.** Call sites contain canonical URLs from `code.claude.com`, `platform.claude.com`, `anthropic.com`, `tools.ietf.org/rfc`, `learn.microsoft.com`, etc. + +**Mitigation.** REFUSE-primary-source-citation-gate. Surface a side note that the URL should be re-verified for resolution + suggest documenting the primary-source dependency at the rule top. + +**Source.** Same batch — a metric-formula candidate: 3/3 sites cited the vendor doc URL directly; the verbatim fraction was 0% because each site stated the formula differently per its concern context. + +**Encoded in.** `verify` action Gate 3; `anti-patterns.md` #12 (primary-source citation gate). + +## Lesson 7: Concern-driven drift is intentional + +**Observation.** Multiple sites can carry the "same fact" with different surface phrasings because each site addresses a different concern (schema field shape vs framing prose vs query pipeline operator). The drift is signal, not noise — collapsing forces wrong-abstraction. + +**Trigger.** Cluster instances differ in framing but share an underlying fact; consumers serve distinct audiences (schema validators vs cost-attribution writers vs query authors). + +**Mitigation.** REFUSE single-unit extraction. Preserve each consumer's framing inline. If centralization is desired, extract at MOST a 1-line "prescribed upstream" statement + URL, NOT the multi-form derivation. + +**Source.** Same batch — the metric-formula candidate again: 3 instances each in a distinct shape (a schema row with `× 100` integer percent, a query with full field names, ratio prose with short field names; mathematically distinct re aggregation: single-sample vs sum-aggregation). + +**Encoded in.** `decision-framework.md` keep-inline test D (instances differ in non-trivial ways) supplement; `anti-patterns.md` #6 (wrong abstraction) supplement. + +## Lesson 8: Source-of-truth bifurcation across tiers is legitimate + +**Observation.** A concept can originate in the top-tier always-loaded instruction file (`CLAUDE.md`/`AGENTS.md`) AND be aggregated in a scoped rule file (deep-disclosure for hook/skill/script authors). Both are first-class canonicals. Each serves a distinct audience. Forcing the instruction file to cite the rule creates a citation cycle (instruction file → rule → instruction file). + +**Trigger.** A cluster originates in CLAUDE.md/AGENTS.md AND is also documented in a rule file with deeper detail. + +**Mitigation.** REFUSE-source-of-truth-bifurcation. Document both canonicals + their respective audiences in the rule file if not already explicit. + +**Source.** Same batch — an environment-detection cluster: the instruction file carried the session-facing sentinels for the full-session audience; a scoped rule aggregated the env-var matrix for hook/script authors who need the detection ladder. + +**Encoded in.** `verify` action Gate 4; `anti-patterns.md` #11 (source-of-truth bifurcation). + +## Lesson 9: Shape C dedup-by-deletion (positive pattern) + +**Observation.** When the SSOT already exists and consumers paraphrase it, the right action is NOT extraction (the SSOT is already there) — it's DELETION of the redundant paraphrasers in consumer files. Keep load-bearing directives (e.g. `Read X.md first`); delete redundant tail prose. + +**Trigger.** The cluster body in consumers reads as a TL;DR of an existing canonical SSOT, not as inlined content. + +**Mitigation.** Shape C action: delete the redundant tails from each consumer; preserve the load-bearing directive. The SSOT is unchanged. No new file; no migration sweep needed (just deletion). Per-consumer deltas (intentional variations) preserved inline. + +**Source.** Same batch — a bootstrap-preamble candidate: a dozen automation prompts carried the same descriptor tail; a shared doc was already canonical with the full content; deletion was correct vs. extraction-and-citation. + +**Encoded in.** `anti-patterns.md` #13 (Shape C dedup-by-deletion — positive pattern). + +## Lesson 10: Identify subagent over-counts ~95% on broad surveys without verbatim-block discrimination + +**Observation.** When `/extract-ssot identify` runs in exhaustive mode (read-only subagent over 30+ heuristics), the subagent's roster routinely flags 60+ candidates with a ~95% false-positive rate at Tier 0 verify. Failure modes: + +1. **Section-header presence** counted as duplicate (e.g. 18 skills have a `## What this skill does NOT do` header; bodies are unique per skill — template, not duplication) +2. **Concept mention** counted as block reproduction (a file mentions a verification tier once → flagged as a duplicate of the full tier-ladder definition) +3. **Correct citation to SSOT** counted as "still inlined" (a `per X.md "Y"` token treated as inline duplication) +4. **Language-native dedup** flagged as duplication (bash `source utils.sh` in 34 files counted as code duplication; that IS the dedup mechanism) +5. **Per-prompt/per-skill unique lists** confused with shared boilerplate (an exclusion list per prompt has UNIQUE scope-specific entries; template structure ≠ content duplication) + +The subagent ALSO under-counts in some cases — a verbatim 5-place reproduction of a dependency-direction rule was flagged as 2 instances. Heuristic asymmetry: keyword-density inflates structural matches, deflates verbatim-rule clusters. + +**Trigger.** Bare-subagent survey dispatch with broad heuristics and no Tier 0 verification at identify-time. + +**Mitigation.** + +1. The identify prompt MUST require a verbatim-body excerpt per cluster + reproduction-count via discriminating-phrase grep +2. The identify prompt MUST distinguish: (a) verbatim block reproduction, (b) section-header presence, (c) concept mention, (d) correct citation, (e) per-instance unique data with shared template. Only (a) and (e)+(framing-only) count +3. Identify scope MUST be `git ls-files` only — exclude gitignored, untracked, and ephemeral/vendored/fixture content dirs +4. Per-language native dedup (bash `source`, Python `import`, JSON `$ref`, MSBuild ``) is already-extracted; flag as out-of-scope, not as a candidate +5. The `verify` action becomes a HARD GATE for batches ≥5 candidates (not an optional pre-filter) — refuse-fast before any plan/execute + +**Source.** An exhaustive-mode session — the survey subagent produced a 60-candidate roster. Tier 0 manual verify on the top 15 candidates yielded 1 PROCEED (a verbatim 5-instance dependency-direction rule), 14 REFUSE. Spot checks on the remaining ~45 candidates confirmed the pattern (header-only matches, concept mentions, SSOT-cites, language-native dedup). Overall ~95% FP rate. Cost: ~30+ Tier 0 grep calls to disprove the subagent synthesis. + +**Encoded in.** `actions/identify.md` prompt template (Discrimination rules + scope hardening); `actions/batch.md` Step 2 (verify-as-hard-gate for size ≥5). + +## Lesson 11: Stability + reader-burden combined test for semantic-equivalent paraphrase + +**Observation.** Verbatim-only Rule of Three misses semantic-equivalent paraphrases — the same canonical truth restated in different wording across N files. User feedback: *"Anything that is really a maintenance burden that would cause more than one place to update if changed... we want one source of truth and everything else points to that"*. A verbatim-only test under-counts semantic dupes; a pure semantic-similarity test over-counts coincidental similarity. + +**Trigger.** A cluster surfaces N≥3 instances that share canonical meaning but differ in surface phrasing. + +**Mitigation.** Combined test — extract iff EITHER: + +- **Stability test:** changing the canonical truth would force updates in 3+ places in lockstep (maintenance burden), OR +- **Reader-burden test:** a reader trying to understand the rule cannot tell which instance is canonical (ambiguity) + +If only ONE passes, extraction is borderline — run an adversarial-review round. If NEITHER passes, REFUSE-low-roi (coincidental similarity). + +**Counter-test (when NOT to extract despite semantic equivalence):** + +- Each instance applies the rule to its own domain-specific scope (e.g. planning vs implementation vs testing skills each apply a testing default in their own framing) — keep; this is context-specific application +- A section-header pattern is shared but the bodies are skill-specific data (e.g. "What this skill does NOT do" lists) — keep; this is convention/template +- The SSOT designer EXPLICITLY chose to keep a portion inline (e.g. a shared template that keeps examples per-consumer by design) — respect the architectural decision; surface as a side observation if you disagree + +**Source.** A duplication-taxonomy review session. A 5-consumer identical framing template with unique per-consumer examples was considered for extraction and deferred per the architectural intent stated in the SSOT body. A 5-instance verbatim dependency-direction rule was extracted successfully. A "What this does NOT do" candidate was REFUSED — section-header pattern with unique non-goals per skill. + +**Encoded in.** `decision-framework.md` 6+5 gate (extends Rule of Three with the combined test); `actions/identify.md` prompt template (distinguishing form (e) per-instance unique data with shared framing). + +## Append guidance for future batches + +When a future `/extract-ssot batch` execution surfaces a new empirical pattern: + +1. **Confirm novelty.** Cross-check the existing lessons. If the new observation is a variant of an existing lesson, expand that lesson's scope rather than adding a new one. +2. **Confirm Tier 0 evidence.** An empirical lesson requires concrete batch references — date + cluster name + outcome. NOT speculation. +3. **Append `## Lesson N+1`** at the end of the file. Schema: name (one-line), Observation, Trigger, Mitigation, Source (date + cluster + outcome), Encoded in (which downstream artifacts cite this lesson). +4. **Cross-reference.** If the lesson should drive a new `verify` gate, anti-pattern, or decision-framework row, append that addition in the same PR. +5. **Cap.** When the file approaches 400 lines, propose a split: archive older lessons to the working notes, retain the most-recent-N + greatest-impact-M in this file. Document the recheck trigger with the split proposal. + +## Cross-references + +- `decision-framework.md` "Pre-extraction Tier 0 checklist" — codifies lessons as a-priori discipline +- `anti-patterns.md` patterns #11 (Lesson 8), #12 (Lesson 6), #13 (Lesson 9) +- `actions/verify.md` Gates 1, 2, 3, 4, 5, 6 — implementation of Lessons 1, 2, 6, 8, 3+4, 5 +- SKILL.md "Evidence discipline" — Tier 0 evidence requirement for new lesson appends diff --git a/plugins/docs-hygiene/skills/extract-ssot/scripts/emit-verify-facts.sh b/plugins/docs-hygiene/skills/extract-ssot/scripts/emit-verify-facts.sh new file mode 100755 index 000000000..42a88011a --- /dev/null +++ b/plugins/docs-hygiene/skills/extract-ssot/scripts/emit-verify-facts.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Partial extract-ssot verify facts. No PROCEED/REFUSE verdict. +# +# Output: Phrase, Instance/Citation/Primary URL counts and samples, scoped to +# the tracked *.md files of the repository the script runs in. +# Exit: 0 for fact runs (fact-emitter must never fail the caller; -e and +# pipefail omitted so partial grep failures don't abort the run); 2 on +# argument errors. +set -u + +PHRASE="" +MAX_SAMPLES=20 + +usage() { + cat <<'EOF' +emit-verify-facts.sh — Tier-0 facts for /extract-ssot verify gates 1–3. + +Usage: + emit-verify-facts.sh --phrase "" + emit-verify-facts.sh --help + +Emits instance, citation-pattern, and primary-URL hit counts across the +consuming repository's tracked markdown. Verdict stays in the skill. +Exit: 0 (fact runs) / 2 (argument errors). +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --phrase) + PHRASE="${2:-}" + shift + [[ $# -gt 0 ]] && shift + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "emit-verify-facts.sh: unknown arg '$1'" >&2 + exit 2 + ;; + esac +done + +if [[ -z "$PHRASE" ]]; then + echo "emit-verify-facts.sh: --phrase is required" >&2 + exit 2 +fi + +repo_root="$(git rev-parse --show-toplevel 2>/dev/null | tr -d '\r')" +if [[ -n "$repo_root" ]]; then + cd "$repo_root" 2>/dev/null || true +fi + +# git pathspec '*.md' matches tracked markdown at any depth. +SCOPE='*.md' + +# Count grep hits piped on stdin, echoing the first MAX_SAMPLES with a per-hit +# label and a trailing count line. Empty lines (no matches) don't count. +emit_facts() { + local sample_label="$1" count_label="$2" + local count=0 hit + while IFS= read -r hit; do + [[ -z "$hit" ]] && continue + count=$((count + 1)) + [[ $count -le $MAX_SAMPLES ]] && printf '%s: %s\n' "$sample_label" "$hit" + done + printf '%s: %s\n' "$count_label" "$count" +} + +printf 'Phrase: %s\n' "$PHRASE" +printf 'Scope: tracked %s files\n' "$SCOPE" + +citation_regex='per [a-z0-9./_-]+\.md' +url_regex='(code|platform)\.(claude|anthropic)\.com|tools\.ietf\.org/rfc|learn\.microsoft\.com' + +# -e guards dash-prefixed phrases (e.g. a repeated CLI flag like --no-verify) +# from being parsed as git grep options. +git grep -nF -e "$PHRASE" -- "$SCOPE" 2>/dev/null | tr -d '\r' | + emit_facts 'Instance' 'Instance hit count' +git grep -nE -e "$citation_regex" -- "$SCOPE" 2>/dev/null | tr -d '\r' | + emit_facts 'Citation sample' 'Citation pattern hit count' +git grep -nE -e "$url_regex" -- "$SCOPE" 2>/dev/null | tr -d '\r' | + emit_facts 'Primary URL sample' 'Primary URL hit count' + +exit 0 diff --git a/plugins/docs-hygiene/skills/extract-ssot/scripts/emit-verify-facts.test.sh b/plugins/docs-hygiene/skills/extract-ssot/scripts/emit-verify-facts.test.sh new file mode 100755 index 000000000..55bb70a15 --- /dev/null +++ b/plugins/docs-hygiene/skills/extract-ssot/scripts/emit-verify-facts.test.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# Self-contained tests for emit-verify-facts.sh (no external test lib — ships with the plugin). +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EMIT="$SCRIPT_DIR/emit-verify-facts.sh" +TEST_TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TEST_TMPDIR"' EXIT + +FAILED=0 +CASE=0 + +pass() { + CASE=$((CASE + 1)) + printf 'PASS: %s\n' "$1" +} +fail() { + CASE=$((CASE + 1)) + FAILED=$((FAILED + 1)) + printf 'FAIL: %s\n expected: %s\n' "$1" "$2" >&2 +} +assert_contains() { + case "$2" in + *"$3"*) pass "$1" ;; + *) fail "$1" "to contain: $3" ;; + esac +} +assert_not_contains() { + case "$2" in + *"$3"*) fail "$1" "NOT to contain: $3" ;; + *) pass "$1" ;; + esac +} +assert_exit() { + if [[ "$2" == "$3" ]]; then + pass "$1" + else + fail "$1" "exit $2, got $3" + fi +} + +# --- Build a fixture consumer repo with a known duplication cluster. --- +repo="$TEST_TMPDIR/repo" +mkdir -p "$repo/docs" "$repo/.claude/rules" +git -C "$repo" init -q +git -C "$repo" config user.email t@t.t +git -C "$repo" config user.name t + +phrase="the quick brown fox jumps over the lazy dog" + +cat >"$repo/docs/alpha.md" <"$repo/docs/beta.md" <"$repo/.claude/rules/gamma.md" <"$repo/script.sh" </dev/null 2>&1 + echo $? +)" +assert_exit "no args exits 2" 2 "$( + bash "$EMIT" 2>/dev/null + echo $? +)" +assert_exit "unknown arg exits 2" 2 "$( + bash "$EMIT" --unknown 2>/dev/null + echo $? +)" +assert_exit "--phrase with empty value exits 2" 2 "$( + bash "$EMIT" --phrase 2>/dev/null + echo $? +)" + +# --- Fact emission against the fixture repo --- +out="$(cd "$repo" && bash "$EMIT" --phrase "$phrase")" +assert_contains "phrase echoed" "$out" "Phrase: $phrase" +assert_contains "markdown instances counted" "$out" "Instance hit count: 3" +assert_not_contains "non-markdown file excluded from scope" "$out" "script.sh" +assert_contains "citation pattern counted" "$out" "Citation pattern hit count: 1" +assert_contains "citation sample surfaced" "$out" "docs/beta.md" +assert_contains "primary URL counted" "$out" "Primary URL hit count: 1" +assert_contains "primary URL sample surfaced" "$out" ".claude/rules/gamma.md" +assert_not_contains "no verdict emitted" "$out" "PROCEED" +assert_not_contains "no refusal emitted" "$out" "REFUSE" + +# --- Dash-prefixed phrase is not parsed as a git grep option --- +cat >>"$repo/docs/alpha.md" <<'EOF' +Never bypass hooks with --no-verify on commits. +EOF +git -C "$repo" add -A +git -C "$repo" commit -qm dash-fixture +dash_rc=0 +dash_out="$(cd "$repo" && bash "$EMIT" --phrase "--no-verify")" || dash_rc=$? +assert_exit "dash-prefixed phrase exits 0" 0 "$dash_rc" +assert_contains "dash-prefixed phrase counted" "$dash_out" "Instance hit count: 1" + +# --- Zero-match phrase still exits 0 with zero counts --- +zero_rc=0 +zero_out="$(cd "$repo" && bash "$EMIT" --phrase "phrase that matches nothing at all")" || zero_rc=$? +assert_exit "zero-match run exits 0" 0 "$zero_rc" +assert_contains "zero-match instance count" "$zero_out" "Instance hit count: 0" + +if [[ $FAILED -eq 0 ]]; then + printf '\nAll %d checks passed.\n' "$CASE" + exit 0 +fi +printf '\n%d/%d checks failed.\n' "$FAILED" "$CASE" >&2 +exit 1 diff --git a/plugins/docs-hygiene/skills/rename-references/SKILL.md b/plugins/docs-hygiene/skills/rename-references/SKILL.md new file mode 100644 index 000000000..0273022e1 --- /dev/null +++ b/plugins/docs-hygiene/skills/rename-references/SKILL.md @@ -0,0 +1,166 @@ +--- +name: rename-references +description: "Sweep stale references after renames — the syntactic forms token-only grep misses (slash-tokens, paths, chain prose, numbered table rows, frontmatter chains and globs). Use when: 'rename X to Y', 'I renamed X', 'audit rename', 'find stale refs', 'check for stragglers', 'after git mv', 'sweep references', 'rename impact preview', 'find half-renamed state', 'broken refs after rename', 'pre-PR rename check' — actions: audit, audit blast, audit half-rename, audit orphans, apply, preview, blocklist; not for framework migrations or repo-wide dead-reference audits." +argument-hint: "[action] [ [to ]] [--include-historical|--include-memory|--include-plan-docs] (e.g., /rename-references audit, /rename-references audit blast /verify to /verify-changes, /rename-references audit half-rename /a to /b, /rename-references audit orphans /a to /b, /rename-references blocklist)" +user-invocable: true +disable-model-invocation: false +--- + +## Pre-computed context + +Working tree status: !`git status --porcelain 2>/dev/null | head -20 || echo "clean"` +Rename pairs (git): !`{ git diff --name-status -M HEAD 2>/dev/null; git diff --cached --name-status -M 2>/dev/null; } | grep '^R' | head -15 || echo "none"` +Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"` + +## Purpose + +Renames are deceptively hard. After renaming a skill, file, or identifier, references survive in 7+ syntactic forms beyond the obvious token. Token-only grep (`/old`) catches 50–70%; the rest hide in chain prose (`→ old →`), comma-lists (`Test, Old, Retro`), numbered table rows (`| 7. Old |`), frontmatter chain strings (`description: "...→ old → retro process."`), frontmatter globs (`{a,b,old,c}`), cross-skill mode references, and content-file paths (context/old.md style). + +This skill makes "find every reference" one invocation instead of 4 manual sweep passes. Runs the full pattern library, triages matches into 3 buckets, surfaces ambiguity (English-verb collisions like `confirm`/`test`/`review`) for user confirmation rather than auto-applying blindly. + +## Adapting to your environment + +This skill is self-contained — the pattern library, triage classifier, and audit modes below need only git and the Grep tool. Where prose names an adjacent capability (a verification workflow, an issue tracker, a codebase-audit routine), treat it as optional: if your environment provides it, invoke it; otherwise proceed without. Consumer-specific conventions (work-notes locations, commit policy, naming rules) come from the consuming repository's own CLAUDE.md and rules — read them; this skill does not assume them. + +## Action router + +Parse `$ARGUMENTS` first token to determine action. Subsequent tokens are the rename pair (with `to`/`→`/`->`/`into` separator) or single token for reverse-mode. + +| Argument shape | Action | Read | +|---|---|---| +| *(empty)* | **Smart default** | inline below — detect rename pair from conversation/git/staged | +| `audit` | **Audit** (read-only sweep — alias for `audit blast`) | [context/audit.md](context/audit.md) + [audit-modes.md](context/audit-modes.md) | +| `audit ` or `audit to ` | **Audit** with explicit pair | [context/audit.md](context/audit.md) | +| `audit blast [ [to ]]` | **Audit Blast** — pre-rename impact (counts + per-file + bucket distribution; inline-only) | [audit-modes.md](context/audit-modes.md) | +| `audit half-rename to ` | **Audit Half-Rename** — files mentioning BOTH old AND new (incomplete-rename hygiene) | [audit-modes.md](context/audit-modes.md) | +| `audit orphans to ` | **Audit Orphans** — refs at old name OR vanished path AFTER rename (REQUIRES pair) | [audit-modes.md](context/audit-modes.md) | +| ` to ` | **Apply** full rename | [context/apply.md](context/apply.md) | +| `preview to ` | **Preview** dry-run | [context/apply.md](context/apply.md) (skip Edit phase) | +| `` (single token, no separator) | **Reverse** — find refs, ask what to replace with | [context/audit.md](context/audit.md) | +| `blocklist` | **Print English-verb blocklist** (read-only introspection of triage-bucket safety mechanism) | inline below | + +**Pattern library is the load-bearing component** — the full form registry lives in [context/patterns.md](context/patterns.md); execute sweeps with the Grep tool. Read it before any sweep. Triage logic is in [context/triage.md](context/triage.md). Audit sub-mode detail (Blast / Half-rename / Orphans) is in [context/audit-modes.md](context/audit-modes.md). + +## Override flags (audit modes) + +Three flags toggle auto-exclusions ON. Defaults preserve safety (rename-documenting plan docs / frozen historical notes / memory entries excluded). Long-form only, position-agnostic. + +| Flag | Effect | Apply mode? | +|---|---|---| +| `--include-historical` | Sweep archived/completed work notes and frozen records of past work | OK | +| `--include-memory` | Sweep Claude Code auto-memory files (`~/.claude/projects/*/memory/*.md`) and `MEMORY.md` indices | OK | +| `--include-plan-docs` | Sweep the active plan/work-notes documents that document THIS rename | **AUDIT-MODE ONLY** — apply mode rejects with explicit error (footgun prevention; the plan doc documents the rename — both names appear by design) | + +Multiple flags compose. Unknown flags raise an error. Detail in [audit-modes.md](context/audit-modes.md) "Override flags" section. + +## Blocklist action + +`/rename-references blocklist` prints the English-verb blocklist literal from [context/triage.md](context/triage.md). Read-only — no edits, no sweeps. Use to inspect which tokens force the ambiguous-bucket safety path. To extend, edit `triage.md` directly. + +## Smart default (no arguments) + +Detect rename pairs in this priority order: + +1. **Explicit args** (highest precedence — never overridden) +2. **Recent conversation** — scan last 20 turns for prose like "I renamed X to Y", "rename X → Y", "git mv X Y", or assistant edits replacing identifier X with Y across multiple files +3. **Git rename detection** — `git diff --name-status -M HEAD` and `git diff --cached --name-status -M`; `R` lines map old path → new path +4. **Paired `D ` + `?? `** in `git status --porcelain` (heuristic — Levenshtein-similar basenames within same dir) + +If multiple candidates surface, present via `AskUserQuestion` — user picks which pair to audit. + +If zero candidates: report "No rename detected in conversation or git state. Provide `/rename-references to ` or `/rename-references audit ` to invoke explicitly." + +## Natural-language parser + +The argument string after the action keyword can be: + +| Form | Example | Parses to | +|---|---|---| +| ` to ` | `/verify to /verify-changes` | old=`/verify`, new=`/verify-changes` | +| ` to ` (mode) | `/verify to /verify-changes outcome` | old=`/verify outcome`, new=`/verify-changes outcome` | +| `` | `test live → test e2e` | old=`test live`, new=`test e2e` | +| ` -> ` | `foo -> bar` | old=`foo`, new=`bar` | +| ` into ` | `legacy into modern` | old=`legacy`, new=`modern` | +| ` to ` | `confirm/SKILL.md to verify/SKILL.md` | path rename — skill applies extra path-form patterns | + +Multi-word old/new is fine — separator is the only delimiter. Quote characters (`"foo bar"`) optional. Old/new taken verbatim — leading slashes preserved (`/confirm` stays `/confirm`, not `confirm`). + +## Workflow phases (apply mode) + +1. **Detect** — gather rename pair (args / conversation / git per Smart default) +2. **Survey** — run all patterns from `context/patterns.md` in parallel against tracked text files; aggregate per-file hit counts. For a skill/identifier rename, FIRST enumerate coupled-sibling renames (dot-form behavior point-IDs, internal mode names, content-file basenames that changed in lockstep) and queue EACH as its own rename pair — they carry no primary token, so the primary sweep never reaches them (see Gotchas "coupled-rename") +3. **Triage** — classify each match into 3 buckets per `context/triage.md`: + - **Certain** — slash-token, path, frontmatter glob (high precision, auto-apply candidate) + - **Chain-context** — preceded/followed by other known skill names; numbered table rows; chain prose with multiple skill tokens + - **Ambiguous** — bare-token matches where token is in English-verb blocklist (`confirm`/`test`/`review`/`fix`/`clean`/`build`/`lint`/`verify`/`plan`/etc.) +4. **Confirm** — present buckets via `AskUserQuestion`: + - Certain → "auto-apply N matches?" — yes/no + - Chain-context → "review N matches one-by-one?" — yes/skip-bucket/auto-apply-all + - Ambiguous → per-match yes/no with surrounding context +5. **Apply** — Edit each accepted match. Idempotent — running twice is safe. +6. **Re-sweep** — re-run Phase 2; exit when count == 0. If non-zero, NEW form not in pattern library — report to user, add to `context/patterns.md`, re-iterate. +7. **Hand off** — suggest running the consuming repository's verification workflow (build + test + lint) to confirm no semantic regression in the rename target. + +Audit mode runs phases 1-3 only and reports — no Edit calls. Preview mode runs 1-4 and reports planned edits — no Edit. + +## Auto-exclusions + +Paths skipped from sweeps automatically: + +- **Rename-documenting plan/work-notes documents** — an active plan file, migration notes, or changelog entry drafted for THIS rename references the rename pair as part of documenting the migration; both names appear by design. Identify these from conversation context and the consuming repository's work-notes conventions (its CLAUDE.md / rules). Skipping prevents self-reference loops. +- **Archived/historical work notes** — completed plan documents and frozen records of past work are deliberately preserved as-is; past renames documented there are historical record, not stale references. +- `.git/`, `node_modules/`, `bin/`, `obj/`, `.venv/`, `dist/`, `build/` +- Claude Code auto-memory files in `~/.claude/projects/*/memory/` describing past renames + +## Composition + +| Stage | How | Why | +|---|---|---| +| Pre-rename impact analysis | `/rename-references audit ` | Read-only sweep, see blast radius before changing anything | +| Rename execution | `/rename-references to ` | Sweep → triage → edit → re-sweep | +| Post-rename verification | the consuming repository's build/test/lint workflow | Confirm no semantic regression | +| Cleanup near rename sites | a separate simplification/refactor pass | Opportunistic refactor (separate concern) | + +## Edge cases + +- **English-verb collision** — tokens like `confirm`, `test`, `review`, `clean`, `fix`, `build`, `lint`, `plan`, `run`, `view`, `start`, `stop`, `merge`, `split`, `sort`, `filter`, `group`, `head`, `body`, `link`, `list`, `work`, `log`, `watch`, `monitor`. Triage forces these into ambiguous bucket regardless of regex hit position. User confirms each. +- **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). +- **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. + +## What this skill does NOT do + +- **Does not perform AST-level renames** — Python class names, C# type names, JS function refactors. Use IDE refactor tools or language-aware refactoring tooling. This skill handles documentation, configuration, and identifier-string renames in text files. +- **Does not rename git branches** — use `git branch -m`. Operates on file content, not git refs. +- **Does not handle framework version migrations** — use dedicated migration tooling. Different concern: behavioral upgrade, not text rename. +- **Does not auto-fix conversation history or memory entries** — past mentions of the old name in conversation/memory are deliberately preserved as historical record. Future renames are the user's responsibility to invoke this skill for. +- **Does not run builds or tests** — hand off to the consuming repository's build/test/verification workflow after the rename completes. +- **Does not perform general dead-reference scanning** — `audit orphans` is STRICTLY pair-driven (post-rename hygiene only). For repo-wide dead-link / dead-reference checks unrelated to a specific rename, use a codebase-audit workflow or documentation link checker if your environment provides one. Charter boundary preserves single responsibility. + +## Gotchas + +- **NEVER trust a single-pattern grep as "clean."** That was the bug this skill exists to prevent. In the skill-rename incident that motivated the pattern library, token-only grep returned 0 matches across 4 sweep passes; chain prose, comma-lists, numbered rows, and frontmatter forms surfaced one round at a time. Run all patterns or invoke `/rename-references audit`. +- **Ambiguous bucket is mandatory triage, not optional.** English-verb collisions are the highest false-positive vector. If a token is in the blocklist, force into ambiguous regardless of position. Cost of one extra confirmation prompt is far lower than silently mangling prose. +- **Re-sweep until count == 0.** Don't trust Phase 5 ended cleanly without verification. Phase 6 is the gate. +- **Plan-doc exclusion is mandatory.** The active plan/work-notes document *documents the rename* and contains both old and new names by design. Editing it would break the documentation narrative. +- **Pattern library evolves.** When Phase 6 finds a NEW form, treat as a learning event: extend `context/patterns.md`. Future renames benefit immediately. +- **A file MOVE breaks the moved files' own relative paths — sweep INSIDE the moved set, not just refs TO it.** When `git mv` changes directory depth, relative refs *inside* the moved files (`source ../../lib.sh`, `# shellcheck source=../../../../tests/...`, relative markdown links) silently break — they carry no renamed token, so every token-keyed pattern returns clean while the moved file itself is broken. After any depth-changing move: `grep -nE '\.\./' ` + re-run the moved code from its new location (tests, `--help`). Real example: a directory promotion left a `# shellcheck source=` directive pointing four levels up when the new home was two. +- **A rename couples sibling renames — sweep each as its own pair.** Renaming a skill or identifier usually drags coupled siblings that do NOT contain the primary token: dot-form action/mode IDs (`verify.runtime-affecting-paths` — Form 12), internal mode names (`quality` mode), content-file basenames (context/quality.md style paths). A phase-scoped, skill-only grep on the primary token (`/verify`) leaves these EXTERNAL refs — in skill bodies, config files, and other skills' dispatch tables — unverified. A slash-anchored token sweep can return "clean" while `.id` / `` / `.md`-path refs survive elsewhere. Before declaring a rename complete: enumerate the coupled identifiers (Survey phase) and run a sweep per pair. + +## Integration with workflow + +`/rename-references` is invocable mid-workflow whenever a rename happens — typically during implementation (when the work includes a rename) or as a precursor to final verification, to confirm no stragglers before declaring done. + +**Skill chaining:** + +| Condition | Action | +|---|---| +| User says "I renamed X to Y" mid-implementation | Invoke `/rename-references to ` to sweep before continuing | +| `git mv` just executed | Invoke `/rename-references audit` to surface stragglers | +| Pre-PR: working tree contains R-status files | Suggest `/rename-references audit` before final verification | +| `/rename-references` finds 0 matches | Proceed to verification (or done if already past it) | +| `/rename-references` finds NEW form not in pattern library | Update `context/patterns.md`, re-iterate | diff --git a/plugins/docs-hygiene/skills/rename-references/context/apply.md b/plugins/docs-hygiene/skills/rename-references/context/apply.md new file mode 100644 index 000000000..1b7ccb119 --- /dev/null +++ b/plugins/docs-hygiene/skills/rename-references/context/apply.md @@ -0,0 +1,173 @@ +# Apply Mode — Full Rename Workflow + +Apply mode performs the complete rename: sweep → triage → user confirmation → Edit → re-sweep → handoff. Primary workflow when the user has decided to rename and wants the audit + edit done in one invocation. + +Preview mode is the same workflow with the Edit phase replaced by "report planned edits." Use preview when the user wants to see exactly what would change before committing. + +## Inputs + +| Form | Behavior | +|---|---| +| `/rename-references to ` | Apply mode — full pipeline through Edit + re-sweep | +| `/rename-references preview to ` | Preview mode — same pipeline, planned edits reported instead of applied | + +The natural-language parser per `SKILL.md` accepts `to`/`→`/`->`/`into` separators. Multi-word old/new is fine. Path renames (`a/old.md` to `a/new.md`) trigger extra path-form patterns. + +## Workflow (7 phases) + +### Phase 1: Detect + +Parse the rename pair `(old, new)` from arguments. If parse fails, abort with parser examples from `SKILL.md`. Validate: + +- `old` and `new` are non-empty +- `old != new` +- Path forms have matching extensions (warn if `a/old.md` to `a/new.txt`) + +### Phase 2: Survey + +Identical to audit-mode Phase 2 — run all patterns from [patterns.md](patterns.md) in parallel, aggregate into match list, apply auto-exclusions. + +### Phase 3: Triage + +Classify per [triage.md](triage.md) into Certain / Chain-context / Ambiguous / Excluded buckets. + +### Phase 4: Confirm + +Present bucket counts via the report template from [triage.md](triage.md) "Reporting bucket counts." Then gate the proceed/abort decision via `AskUserQuestion`. + +Per-bucket confirmation flow: + +**Certain bucket:** + +- Show count + 1-2 example matches +- `AskUserQuestion`: "Auto-apply N matches?" — options: "auto-apply", "review one-by-one" +- Default: auto-apply (these forms have empirically near-zero false-positive rate) + +**Chain-context bucket:** + +- Show count + matches grouped in batches of up to 10 with 2-line context per match +- `AskUserQuestion`: "Apply N chain-context matches?" — options: "auto-apply all", "review one-by-one", "skip bucket" +- Default: review one-by-one (chain prose drift in user-facing docs is loud and embarrassing) + +**Ambiguous bucket:** + +- Show each match individually with 3 lines of surrounding context +- `AskUserQuestion` per match: "Rename this?" — options: "rename this", "skip this", "skip remaining ambiguous" +- ALWAYS one-by-one — batched confirmation defeats the safety purpose + +If user picks "abort" at any prompt, halt and report partial state (no matches edited yet — Edit phase not started). + +### Phase 5: Apply + +For each accepted match (from Phase 4 user confirmations): + +- Use Edit tool with exact old/new substitution +- For matches where the form requires partial replacement (e.g., chain prose `→ confirm →` becomes `→ verify →`), construct precise old_string and new_string +- Idempotency: running twice MUST be safe. Edit tool's `replace_all: false` (default) ensures only one instance changes per call; re-sweep catches anything missed + +**Edit ordering for overlapping renames:** + +If `old` is a substring of `new` (e.g., `test` to `test e2e`), apply most-specific match first to prevent double-edit. Track edited ranges per file to avoid re-matching. + +**Path renames (Form 3):** + +If args were path forms (`a/old.md` to `a/new.md`), the actual file rename is OUT OF SCOPE for this skill — that's `git mv`. This skill only updates *references* to paths in other files. Report clearly so the user runs `git mv` separately if needed. + +**Preview mode:** instead of calling Edit, render the planned diff per file: + +```text +File: +- : ++ : +``` + +Then exit. Do not proceed to Phase 6. + +### Phase 6: Re-sweep + +Re-run Phase 2 (survey) with the same pattern library and ``. Three possible outcomes: + +**Outcome A — count == 0:** rename complete. Proceed to Phase 7. + +**Outcome B — count > 0, all in already-triaged buckets:** Phase 4 user choices missed some matches. Re-present bucket counts and re-confirm. Loop back to Phase 4. + +**Outcome C — count > 0, NEW pattern form not in library:** Phase 6 pattern-library-evolution trigger. STOP — do not silently apply. + +Pattern evolution protocol: + +1. Report the new form to user with example match +2. Document the pattern in [patterns.md](patterns.md) with all 5 fields (form name, regex, triage default, example, false-positives) +3. Re-run sweep with extended pattern library (back to Phase 2) + +Ask user before automatic re-iteration — they may want to inspect manually first. + +### Phase 7: Hand off + +When Phase 6 reports count == 0: + +1. Summarize what changed: + + ```text + Rename complete: + + Edits applied: + - : changes + - : changes + + Total: matches across files. + Excluded: plan-doc/historical/memory paths (preserved). + ``` + +2. Suggest follow-up per `../SKILL.md` "Skill chaining": + + - Always: run the consuming repository's verification workflow (build + test + lint) to confirm no semantic regression + - If user is mid-implementation under another skill or plan: return control to that flow + - Do NOT `git add`/`commit`/`push` automatically. Report status only — the consuming repository's own commit policy governs. + +## Special cases + +### Self-reference exclusion + +The active plan/work-notes document records the rename — both `` and `` appear in scope tables, success criteria, and decision logs. Auto-excluded. Apply mode rejects `--include-plan-docs` with an explicit error (see [audit-modes.md](audit-modes.md) "Override flags"); inspect via audit modes instead. + +### Idempotency under partial completion + +If Edit phase is interrupted (user cancels mid-flow, tool error), partial edits remain in the working tree. Re-invoke `/rename-references to ` to resume — the survey will find only remaining matches, and re-applying succeeds because each Edit is targeted. + +### Concurrent session conflicts + +Edit tool's read-before-write guard catches files modified by another session. If guard fails, abort and report the conflicting file. User resolves manually before re-invoking. + +### 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). + +### Frontmatter multi-line + +Form 7 (frontmatter chain string) uses `multiline: true` on the Grep tool because YAML description strings can span lines: + +```yaml +description: "Long description with → confirm → + chain on second line" +``` + +Without multiline, this would be missed. + +### Empty Certain bucket but full Chain-context + +Common pattern when rename is purely conceptual (chain ordering changed but no token rename happened). Verify with user that the rename pair is correct — Chain-context-only matches often signal the user actually wants a semantic refactor, not a text rename. + +## Hand-off summary + +Default handoff after success: + +```text +Rename `` → `` complete. +- Phase 5 applied edits across files +- Phase 6 re-sweep: 0 stragglers +- Excluded plan-doc/historical/memory paths (preserved by design) + +Next: run your verification workflow (build + test + lint) to confirm no semantic regression. +``` + +User decides whether to commit immediately or continue work first (the consuming repository's own commit policy governs). diff --git a/plugins/docs-hygiene/skills/rename-references/context/audit-modes.md b/plugins/docs-hygiene/skills/rename-references/context/audit-modes.md new file mode 100644 index 000000000..23a2d3897 --- /dev/null +++ b/plugins/docs-hygiene/skills/rename-references/context/audit-modes.md @@ -0,0 +1,204 @@ +# Audit Sub-Modes — Blast / Half-Rename / Orphans + +`/rename-references audit` supports three sub-modes sharing the survey + triage pipeline from [audit.md](audit.md) but differing in input requirements, algorithm, and output format. Bare `/rename-references audit` (no sub-mode) defaults to **Blast** for backward compatibility. + +| Sub-mode | Charter | Pair required? | Output | +|---|---|---|---| +| `audit blast` | Pre-rename impact: counts + per-file + bucket distribution | Optional (smart-default if absent) | Inline report (no file artifact) | +| `audit half-rename` | Find files containing BOTH old AND new (incomplete-rename hygiene) | Required | Inline table grouped by file | +| `audit orphans` | Refs at old name OR vanished path AFTER rename | Required (single-token rejected) | Two sub-tables: orphan + stale-but-functional | + +Override flags `--include-historical`, `--include-memory`, `--include-plan-docs` apply to all three sub-modes. `--include-plan-docs` is **audit-mode only** — apply mode rejects with explicit error. + +--- + +## Audit Blast + +**Purpose:** pre-rename impact preview. Inform go/no-go before committing edits. Cheaper than `preview` mode (no Edit-phase planning) — surfaces counts, bucket distribution, top-affected files. + +**When to invoke:** + +- User asks "how big is this rename?" +- Pre-rename impact analysis on uncertain blast radius +- After `git mv` to size the post-move sweep + +**Inputs:** + +| Form | Behavior | +|---|---| +| `/rename-references audit blast` (no args) | Smart-default detection per `SKILL.md` (conversation / git R-lines / paired D+??) | +| `/rename-references audit blast ` | Single-token reverse mode — sweep without ``; report still useful for pre-rename radius | +| `/rename-references audit blast to ` | Explicit pair | + +**Algorithm:** run the full pattern library from [patterns.md](patterns.md) via the Grep tool for match facts, then apply triage from [triage.md](triage.md). Phases 1–3 (Detect → Survey → Triage) follow [audit.md](audit.md). NO Edit phase. NO file artifact. + +**Output format:** identical to audit.md "Phase 4: Report". Counts table + top-5 affected files + pattern-form breakdown. Inline only. + +**Defaults / aliasing:** bare `/rename-references audit` (no sub-mode keyword) routes to blast — Blast preserves the original audit behavior verbatim. + +**Why no file artifact:** Beck (Tidy First, 2024) and Fowler (Refactoring, 2nd ed.) explicitly reject upfront-analysis artifacts in favor of inline IDE Find Usages. Inline is the right primitive; a plan-file artifact is deferred until cross-team review demand surfaces. + +--- + +## Audit Half-Rename + +**Purpose:** detect incomplete rename state — files mentioning BOTH old AND new identifier. Common after partial PR work or interrupted `/rename-references` apply runs. di Penta et al. (IEEE TSE 2020): ~80% of code smells (incl. half-renames) persist indefinitely once introduced — early detection prevents calcification. + +**When to invoke:** + +- After a partial rename PR was merged but left stragglers +- Mid-rename pause: "did I miss anything?" +- Pre-PR safety check post `/rename-references to ` apply mode + +**Inputs:** + +| Form | Behavior | +|---|---| +| `/rename-references audit half-rename to ` | Required — both names needed for intersection algorithm | +| `/rename-references audit half-rename` (no pair) | Smart-default per SKILL.md; if zero candidates, error: "audit half-rename requires a rename pair (old → new)" | + +**Algorithm:** + +1. Sweep for `` and `` separately with the pattern library from [patterns.md](patterns.md) via the Grep tool; intersect per file — files with hits for BOTH tokens are in half-rename state +2. **English-verb blocklist filter** (judgment): if either token is in the blocklist (per [triage.md](triage.md)), elevate matches to ambiguous bucket — bare `confirm` + bare `verify` co-occurrence in prose is NOT half-rename evidence. Require at least one high-signal match per token (e.g. slash-token `/confirm` AND slash-token `/verify`) +3. Report files in descending order of total hits (old + new) + +**Output format:** + +```text +Half-rename audit: +Files containing BOTH (incomplete rename state): + +| File | Old hits | New hits | Sample | +|-----------------------------------------|----------|----------|----------------------------------------------| +| skills/foo/SKILL.md | 3 | 2 | "...→ →..." (line 12) | +| docs/conventions.md | 1 | 4 | "use instead of " (line 45) | + + files in half-rename state. Run /rename-references to to complete. +``` + +**Edge cases:** + +- **Zero half-rename hits:** report "No half-rename state detected — clean." Positive signal, not error. Common when prior rename was thorough +- **Self-reference (plan docs):** the active plan/work-notes document mentions BOTH names by design (documenting the rename). Auto-excluded by default; `--include-plan-docs` opts in (read-only — apply mode block applies) +- **Documentation references:** memory files often describe historical renames — both old + new appear by design. Memory paths auto-excluded by default + +**No Edits.** Half-rename is an audit — once findings reported, user invokes `/rename-references to ` apply mode if they want to fix. + +--- + +## Audit Orphans + +**Purpose:** find references THIS rename would orphan. NOT a general dead-ref check — that belongs to a repo-wide codebase-audit or link-check workflow. Scope is strictly post-rename hygiene. + +**When to invoke:** + +- After `/rename-references to ` apply phase, double-check no file paths went stale +- After `git mv `, sweep for `[text]()` markdown links and similar +- Before declaring rename done: orphan check is final safety net beyond apply.md Phase 6 re-sweep + +**Inputs:** + +| Form | Behavior | +|---|---| +| `/rename-references audit orphans to ` | Required pair | +| `/rename-references audit orphans ` (single token) | **REJECTED** with error: "audit orphans requires a rename pair (old → new). Charter is post-rename orphan check, not general dead-ref scan. For a repo-wide dead-reference check, use a codebase-audit workflow or documentation link checker." | + +**Algorithm:** + +1. Sweep for `` references using Form 1 (slash-token `\B/\b`) and Form 3 (path `context/.md`, `skills//`) 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) + - **Stale-but-functional:** `` matched, file still exists at old path. Rename was started but old artifact wasn't deleted. User decision: complete the rename or revert +3. Build two sub-reports + +**Output format:** + +```text +Orphans audit: + +Orphan (broken — refs point at vanished path/skill): +| Reference | File:Line | Reason | +|----------------------------------------|----------------------------|----------------------------------| +| [text](context/old.md) | docs/guide.md:42 | path does not exist | +| /old-skill | CLAUDE.md:128 | skills/old-skill/ gone | + +Stale-but-functional (refs at old name, file still exists): +| Reference | File:Line | Note | +|----------------------------------------|----------------------------|----------------------------------| +| skills/old/SKILL.md | docs/index.md:7 | rename incomplete; old still on disk | + + orphans, stale-but-functional. +Suggest: /rename-references to to apply, OR git rm to complete cleanup. +``` + +**Charter (strict):** + +- Orphans audit is PAIR-DRIVEN — what THIS rename orphaned +- NOT general dead-ref check. Dead `/skill-that-was-never-created` references belong to a repo-wide codebase-audit workflow +- NOT general dead-link check. Broken `[text](unrelated-path)` = doc lint territory +- Charter boundary preserves single responsibility — adding a general scan would conflate rename hygiene with codebase auditing + +**Edge cases:** + +- **Both old and new paths exist:** rename was duplicative (file COPIED not MOVED). Report as stale-but-functional with note "duplicate — old + new both present" +- **Slash-token in conversation logs:** memory paths auto-excluded by default; `--include-memory` overrides + +**No Edits.** Orphans audit reports findings; user fixes via `/rename-references to ` apply mode or `git rm`. + +--- + +## Override flags + +Three flags, applicable to ALL audit sub-modes (Blast / Half-rename / Orphans) and the base audit.md path: + +| Flag | Effect | Apply-mode availability | +|---|---|---| +| `--include-historical` | Sweep archived/completed work notes and frozen records of past work | Available | +| `--include-memory` | Sweep `~/.claude/projects/*/memory/*.md` and `MEMORY.md` indices | Available | +| `--include-plan-docs` | Sweep the active plan/work-notes documents that document THIS rename | **AUDIT-MODE ONLY** | + +**Hardcoded apply-mode block on `--include-plan-docs`:** if action is ` to ` (apply) AND `--include-plan-docs` is in args, halt with error: + +```text +Error: --include-plan-docs cannot be used in apply mode. +The active plan document records the rename — both old and new +names appear by design. Editing it would break the documentation +narrative. Use audit modes (audit blast / half-rename / orphans) +to inspect, then update the plan document by hand if needed. +``` + +**Why hardcoded, not warning:** the self-edit footgun is severe enough to block, not advise. Mitigation: AUDIT MODE remains available for inspection (read-only). + +**Defaults preserved:** without any flag, auto-exclusions match the default behavior (plan docs, frozen historical notes, memory all skipped). + +**Flag parsing:** + +- Long-form only (no short aliases — clarity beats brevity for safety-critical flags) +- Position-agnostic: accepted before/after action keyword and before/after rename pair +- Multiple flags compose: `/rename-references audit blast /old to /new --include-historical --include-memory` is valid +- Unknown flags: error with usage hint, never silently ignore + +**Cross-references:** + +- Auto-exclusion source-of-truth: `../SKILL.md` "Auto-exclusions" + `triage.md` "Special case" sections +- Frozen-historical detection: use the consuming repository's work-notes status conventions (frontmatter status fields, archive directories) when present; otherwise treat clearly-archived paths as frozen +- Memory paths: `~/.claude/projects/*/memory/*.md` (cross-platform — POSIX path on Git Bash, Windows path elsewhere) + +--- + +## Hand-off + +After any audit sub-mode completes: + +| Result | Suggestion | +|---|---| +| Blast: 0 matches | "No stragglers found. Safe to proceed (or rename target absent)." | +| Blast: matches found | Suggest `/rename-references to ` (apply) or `/rename-references preview to ` (dry-run) | +| Half-rename: 0 files | "No half-rename state — clean." | +| Half-rename: ≥1 file | Suggest `/rename-references to ` to complete | +| Orphans: 0 orphans + 0 stale | "No orphans — rename is clean." | +| Orphans: orphans found | Suggest `/rename-references to ` apply OR `git rm ` per case | + +Do NOT git add/commit/push automatically — report status; the user decides (the consuming repository's own commit policy governs). diff --git a/plugins/docs-hygiene/skills/rename-references/context/audit.md b/plugins/docs-hygiene/skills/rename-references/context/audit.md new file mode 100644 index 000000000..a8693fb3c --- /dev/null +++ b/plugins/docs-hygiene/skills/rename-references/context/audit.md @@ -0,0 +1,108 @@ +# Audit Mode — Read-Only Rename Sweep + +Audit mode runs the full pattern library + triage classifier against the codebase but performs NO Edit calls. Output is a findings report. Use when: + +- Pre-rename impact analysis ("how big is the blast radius if I rename X?") +- Verifying a just-completed rename has zero stragglers (post-rename gate) +- Investigating a suspected stale reference reported elsewhere +- User wants to think about the rename before committing to edits + +## Inputs + +| Form | Behavior | +|---|---| +| `/rename-references audit` (no args) | Smart default detection per `../SKILL.md` "Smart default" — pick rename pair from conversation/git/staged | +| `/rename-references audit ` | Single-token reverse mode — find references, ask user what `` would be | +| `/rename-references audit to ` | Explicit pair — sweep for `` references and report what would change to `` | + +## Workflow (Phases 1–3 only) + +### Phase 1: Detect + +Resolve the rename pair `(old, new)`: + +1. If args supplied with separator (`to`/`→`/`->`/`into`), parse per `../SKILL.md` "Natural-language parser" +2. If single-token arg, pair is `(arg, undetermined)` — ask via `AskUserQuestion` what new name would be (so triage can show "would change X to Y" diffs) +3. If no args, run Smart default detection. If multiple candidates, present via `AskUserQuestion`. If zero, abort with helpful message + +### Phase 2: Survey + +Run all patterns from [patterns.md](patterns.md) in parallel via Grep tool. For each pattern: + +- Substitute `` with actual old token (escape regex metacharacters) +- Use `output_mode: "content"` with `-n` for line numbers and `-C 1` for one line of surrounding context +- Use `multiline: true` for Form 7 (frontmatter chain string) +- Apply auto-exclusions per `../SKILL.md` "Auto-exclusions" via `glob` filter or post-filter + +Aggregate matches into a flat list of `{file, line, pattern_form, snippet}` tuples. + +### Phase 3: Triage + +Classify each match into one of three buckets per [triage.md](triage.md): + +- **Certain** — high-precision form (slash-token, path, frontmatter glob) +- **Chain-context** — high-precision form when neighbors confirm context (chain prose with known skill names, numbered rows) +- **Ambiguous** — bare-token form when `` is in English-verb blocklist, OR chain-form without confirming neighbors + +### Phase 4: Report (audit-mode terminal step) + +Present findings in this exact format: + +```text +Rename audit: +Sweep across tracked files (excluded: plan-doc/historical/memory paths). + +| Bucket | Count | Sample location | +|-----------------|-------|-----------------| +| Certain | | : | +| Chain-context | | : | +| Ambiguous | | : | +| Excluded | | (skipped) | + +Top 5 affected files: +- : matches +- : matches +... + +Pattern-form breakdown: +- Form 1 (slash-token): +- Form 2 (bare-token): +- Form 3 (path): +- Form 4 (chain forward): +- Form 5 (chain backward): +- Form 6 (numbered row): +- Form 7 (frontmatter): +- Form 8 (glob set): +- Form 9 (PascalCase list): +- Form 10 (cross-skill): +- Form 11 (line-number-citation): +- Form 12 (dot-form sub-identifier): + +Next: invoke `/rename-references to ` to apply, or `/rename-references preview to ` to dry-run. +``` + +If `` is undetermined (single-token reverse mode), omit the `→ ` and the "Next" line — instead suggest the user pick a target via `AskUserQuestion`. + +## Output discipline + +- Audit reports facts, not actions. NEVER call Edit/Write in audit mode +- If user implicitly authorizes edits ("yes apply") during audit, switch to apply mode (`/rename-references to `) — never silently start editing from within audit +- Audit is cheap to re-run; encourage iteration + +## Special cases + +- **Zero matches across all patterns** — report explicitly. Rename target either does not appear in codebase OR pattern library has a gap. If user expected matches, treat as Phase 6 pattern-library-evolution trigger +- **All matches in excluded paths** — report with breakdown showing why each was excluded. User may want to widen scope via the override flags (`--include-historical`, `--include-memory`, `--include-plan-docs`) +- **Ambiguous bucket is empty AND `` is in English-verb blocklist** — unusual. Re-run Form 2 without blocklist filter to verify; the blocklist demotes, not excludes +- **Audit invoked while another `/rename-references` apply is in progress** — abort. In-flight edits and rename-documenting plan docs would be misclassified mid-apply + +## Hand-off + +After audit completes, suggest the next action based on counts: + +| Result | Suggestion | +|---|---| +| 0 matches | "No stragglers found. Safe to proceed." If post-rename context, suggest running the consuming repository's verification workflow | +| Only Certain bucket non-zero | Suggest `/rename-references to ` — auto-apply will likely succeed cleanly | +| Chain-context or Ambiguous non-zero | Suggest `/rename-references preview to ` first — user reviews planned edits before committing | +| NEW form discovered (no pattern matched but user reports a stale ref) | Phase 6 evolution — extend `context/patterns.md`, re-audit | diff --git a/plugins/docs-hygiene/skills/rename-references/context/patterns.md b/plugins/docs-hygiene/skills/rename-references/context/patterns.md new file mode 100644 index 000000000..4b9c91272 --- /dev/null +++ b/plugins/docs-hygiene/skills/rename-references/context/patterns.md @@ -0,0 +1,173 @@ +# Pattern Library — Syntactic Forms + +The load-bearing component of `/rename-references`. Each pattern catches references that pure-token grep misses. Empirically derived from a real skill-rename incident where 11 stale references survived 3 sweep passes; each pass found a new form. + +## How to read this file + +Each pattern has: + +- **Form name** — the syntactic shape it catches +- **Regex** — ripgrep-compatible (use Grep tool, not raw shell) +- **Triage default** — which bucket matches land in (per `triage.md`) +- **Example match** — what it catches +- **Known false-positives** — what to expect and how triage handles it + +Substitute `` with the actual old token. Anchor patterns with word boundaries (`\b`) wherever possible; use `\B` only for slash-token form. + +## Form 1: Slash-prefixed token (skill name) + +```regex +\B/\b +``` + +- **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 + +## Form 2: Bare token with word boundary + +```regex +\b\b +``` + +- **Triage default:** Ambiguous if `` is in English-verb blocklist (see `triage.md`); Certain otherwise +- **Catches:** mode names without leading slash (`live`, `e2e`, `outcome`), bare identifiers in prose +- **False-positives:** English-verb collision is the dominant failure mode. Always force ambiguous bucket when token is a common English word. +- **Use case:** `/test live` rename — `live` appears in `context/live.md`, in mode dispatch tables, in prose. Bare-token catches what slash-token misses. + +## Form 3: Path references + +```regex +context/\.md +skills// +/SKILL\.md +/(?:context|reference|references|scripts|evals)/ +``` + +- **Triage default:** Certain +- **Catches:** filesystem path references in markdown links, prose mentions, frontmatter `paths:` globs +- **Examples:** `[outcome](context/outcome.md)`, `Read /confirm`, `skills/confirm/research/performance.md` +- **False-positives:** rare — paths are inherently specific +- **Note:** if rename includes a path component (e.g., `/test live` → `/test e2e` renamed `context/live.md` → `context/e2e.md`), include path-form patterns even when args don't explicitly mention paths + +## Form 4: Chain prose forward + +```regex +(?:→|->|,| and | then )\s*\b +``` + +- **Triage default:** Chain-context +- **Catches:** workflow chain prose like `→ confirm → retro`, `review, confirm`, `Test, Review, Confirm`, `test and confirm`, `review then confirm` +- **Why this matters:** workflow descriptions appear in frontmatter, README files, and architecture diagrams. Token-only grep misses them because the surrounding `→`/`,` characters anchor the rename context. +- **False-positives:** when `` is a common English word and the comma is enumeration of unrelated items (`apples, oranges, confirm`). Triage forces ambiguous when token is in blocklist. + +## Form 5: Chain prose backward + +```regex +\b\s*(?:→|->|,| and | then ) +``` + +- **Triage default:** Chain-context +- **Catches:** the other direction — `confirm → retro`, `confirm, retro`, `confirm and retro` +- **Pair with Form 4** in every sweep — they catch different positions in the chain + +## Form 6: Numbered table row + +```regex +\|\s*\d+\.\s*\s*\| +\|\s*\d+\.\s*\b +``` + +- **Triage default:** Chain-context +- **Catches:** workflow step tables like `| 7. Confirm |`, `| 7. Confirm | Yes/No/Partial |`, retrospective quick-mode tables, step lists +- **False-positives:** rare — numbered prefix + pipe boundaries are highly specific +- **Variant:** without trailing pipe for tables that don't close columns visibly + +## Form 7: Frontmatter chain string + +```regex +description:\s*"[^"]*\b\b[^"]*" +when_to_use:\s*"[^"]*\b\b[^"]*" +description:\s*'[^']*\b\b[^']*' +``` + +- **Triage default:** Certain (when token is enclosed in description chain like `→ →`); Ambiguous otherwise +- **Catches:** SKILL.md frontmatter `description` and `when_to_use` fields containing chain references like `description: "...explore → research → architect → implement → test → review → confirm → retro process..."` +- **Use Grep with `multiline: true`** — frontmatter strings can span lines +- **Why this matters:** frontmatter description is the SKILL discovery surface; stale chain references mean the AI sees outdated workflow vocabulary every time the skill loads + +## Form 8: Frontmatter glob set + +```regex +\{[^}]*\b\b[^}]*\} +``` + +- **Triage default:** Certain +- **Catches:** glob patterns enumerating skill/file names like `Sources: '.claude/skills/{explore,research,architect,implement,test,review,confirm,retro}/'`, `paths: ['skills/{a,b,old}/SKILL.md']` +- **False-positives:** rare — comma-separated brace expressions are uncommon outside glob contexts +- **Note:** brace expansion is shell-specific; this catches it in any text context + +## Form 9: PascalCase comma-list (workflow verb sequence) + +```regex +\b[A-Z][a-z]+(?:\s*,\s*\b[A-Z][a-z]+){2,} +``` + +- **Triage default:** Chain-context (filter further for known workflow verbs) +- **Catches:** comma-separated sequences of capitalized workflow verbs like `Test, Review, Confirm, Retrospective`, `Explore, Research, Architect, Implement` +- **Why separate from Form 4:** prose chains use lowercase + arrows; PascalCase comma-lists are formal lists in step-name tables, ADR Sources fields, and workflow-pattern documentation +- **Triage refinement:** only flag matches where at least 2 of the comma-separated tokens are known workflow verb names (the rest of the list provides contextual evidence) + +## Form 10: Cross-skill mode reference + +```regex +auto-triggers?\s+`?/\s+\b +chains?\s+(?:back\s+)?to\s+`?/\s+\b +invokes?\s+`?/\s+\b +``` + +- **Triage default:** Chain-context +- **Catches:** prose like `auto-triggers /test e2e`, `chains to /verify-changes outcome`, `invokes /test live` — references to a mode of *another* skill. When that other skill renames its mode, the references in the inviting skill go stale. +- **Why this matters:** skill renames (e.g. `/verify` → `/verify-changes`) and mode renames (e.g. `/test live` → `/test e2e`) leave chain prose stale; pure-token grep catches some but not all phrasings. + +## Form 11: Line-number-citation shapes + +```regex +\.md:[0-9]+ +\.md\s+L[0-9]+ +``` + +- **Triage default:** Certain +- **Catches:** `file.md:42` (colon shape) + `file.md L42` (space-L shape) literals in tracked prose. Forward-looking guard — line-number citations rot after ANY edit to the cited file, so they defend against drift after skill renames or doc edits. +- **Allow-listed contexts** (DO NOT flag): fenced code blocks (```); illustrative audit-output tables in skill docs (e.g. demo findings tables in audit-workflow documentation); work-notes paths excluded per `SKILL.md` "Auto-exclusions" +- **False-positives:** domain-specific `L` notation is not caught by the pattern shape unless it carries a `.md` prefix; exempt any path-scoped exceptions the consuming repository defines + +## Form 12: Dot-form sub-identifier + +```regex +\b\.[\w-]+ +``` + +- **Triage default:** Certain +- **Catches:** dotted identifiers built on the renamed token — action/mode IDs (`verify.runtime-affecting-paths`), dotted config keys, dotted mode/path references. These live in skill bodies, config files, and OTHER skills' dispatch tables. Slash-anchored (Form 1) and path-anchored (Form 3) patterns never reach them; bare-token (Form 2) would bury them in the ambiguous bucket whenever `` is an English verb. +- **Why separate from Form 2:** the trailing `.` disambiguates from the English-verb sense — `verify.runtime-affecting-paths` is unambiguously the identifier, so it lands Certain even when `` is in the blocklist. The `[\w-]+` char class excludes `.` so a match cannot gobble across sentence boundaries. +- **False-positives:** rare — `.` followed by a word char is specific. Sentence-end prose (`verify. Then…`) is excluded because `[\w-]+` requires a word char immediately after the dot (the space after the dot breaks it). +- **Coupled-rename note:** dot-form is one face of coupled-sibling renames. When a skill renames, ALSO enumerate its internal mode names and content-file basenames that changed in lockstep (`quality` mode, `context/quality.md`) and sweep EACH as its own rename pair — they carry no primary token, so a sweep keyed only on `` never reaches them. See SKILL.md "Gotchas" coupled-rename entry. + +## Phase 0 — pre-sweep blocklist load + +Before running any pattern, load the English-verb blocklist from `triage.md`. Any bare-token (Form 2) or chain-context match (Forms 4, 5, 6, 9) where the token is in the blocklist is forced into ambiguous bucket regardless of regex precision. + +## Phase 6 — pattern library evolution + +When the skill's re-sweep finds a NEW syntactic form not covered above: + +1. STOP — do not silently mangle. Report the new form to user. +2. Document the pattern in this file with all 5 fields (form name, regex, triage default, example, false-positives) +3. Re-run sweep with extended pattern library + +## Cross-platform note + +All patterns are ripgrep-compatible (PCRE2 subset). Invoke via the Grep tool, NOT raw shell — Grep handles cross-platform path quoting and is faster than spawning `rg`. If a shell fallback is unavoidable, use `git grep -nE` scoped to tracked files or `rg` from the repo root with the Auto-exclusions applied — never `grep -P` (Perl regex doesn't exist on macOS BSD grep). Do not use lookbehinds — Form 1 uses `\B` instead. diff --git a/plugins/docs-hygiene/skills/rename-references/context/triage.md b/plugins/docs-hygiene/skills/rename-references/context/triage.md new file mode 100644 index 000000000..9cdec2086 --- /dev/null +++ b/plugins/docs-hygiene/skills/rename-references/context/triage.md @@ -0,0 +1,101 @@ +# Triage — 3-Bucket Classifier + +After running the pattern library from `patterns.md`, every match goes into one of three buckets. Triage logic is the difference between mass-mangling prose and a clean rename. + +## Bucket 1: Certain (auto-apply candidate) + +Match patterns where the rename intent is unambiguous regardless of surrounding context. + +**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 3: path references (`context/.md`, `skills//`) — paths are inherently specific +- Form 8: frontmatter glob set (`{a,b,,c}`) — brace enumeration is a glob construct, not English prose + +**User flow:** present count and 1-2 example matches via `AskUserQuestion`. Two options: "auto-apply N matches" or "review one-by-one." Default: auto-apply. + +**Why default-apply:** these forms have near-zero false-positive rate empirically. Treating them as ambiguous burdens the user without value. + +## Bucket 2: Chain-context (review-recommended) + +Match patterns where rename intent is highly likely given surrounding context, but verification is cheap and the cost of a wrong rename in chain prose is high (workflow documentation drift). + +**Bucket criteria:** + +- Form 4: chain prose forward (`(?:→|->|,| and ) `) — plausibly a rename target if neighbors are also identifiers +- Form 5: chain prose backward (` (?:→|->|,| and )`) — same +- Form 6: numbered table row (`| . |`) — workflow step tables +- Form 7: frontmatter chain string — when token appears alongside other workflow tokens +- Form 9: PascalCase comma-list — comma-separated capitalized identifiers +- Form 10: cross-skill mode reference (`/ `) — references to a mode of another skill + +**Refinement — neighbor-aware classification:** + +For chain forms (4, 5, 6, 9), check whether at least one neighboring token (within 5 chars before or after the separator) matches a known skill or command name in the consuming repository (e.g., `name:` frontmatter across `.claude/skills/*/SKILL.md`, installed plugin skill listings). If yes, promote confidence — these are workflow chain references, near-certain rename targets. If no, demote to ambiguous. + +**User flow:** present matches in groups of up to 10 with 2-line context per match. Three options via `AskUserQuestion`: "auto-apply all", "review one-by-one", "skip bucket". + +**Why not auto-apply:** chain prose appears in user-facing documentation (CLAUDE.md, AGENTS.md, README files). A wrong rename in a workflow diagram is loud and embarrassing. The cost of one round of confirmation is much lower than the cost of breaking the diagram. + +## Bucket 3: Ambiguous (mandatory per-match confirmation) + +Match patterns where the token is a common English word AND surrounding context doesn't strongly disambiguate. These are the highest false-positive vector. + +**Bucket criteria:** + +- Form 2: bare token (`\b\b`) when `` is in the English-verb blocklist +- Form 4/5/6/9 (chain forms) when no neighbor is a known skill name (failed promotion check above) +- Form 7: frontmatter chain string when token appears alone in description without other workflow tokens + +**English-verb blocklist** (force ambiguous regardless of pattern position): + +```text +confirm, test, review, fix, clean, build, lint, verify, plan, explore, +research, implement, retro, render, view, list, run, sort, filter, group, +merge, split, drop, pop, push, get, set, post, delete, put, head, body, +link, work, stop, start, pause, resume, log, watch, monitor, check, +validate, audit, compare, debug, trace, profile, scan, search, find, +load, save, copy, move, write, read, parse, render, print, format, +``` + +(Extend as new collisions surface.) + +**User flow:** present each match individually via `AskUserQuestion` with 3 lines of surrounding context. Three options per match: "rename this", "skip this", "skip remaining ambiguous". Always one-by-one — batched confirmation defeats the safety purpose. + +**Why per-match:** "the user just renamed the `confirm` skill" does NOT mean every English use of "confirm" should be replaced. The incident that motivated this skill contained dozens of legitimate "confirm" verb uses (research vocabulary, user-confirmation prompts, domain logic) that MUST be preserved. Per-match confirmation lets the user catch each. + +## Special case: Rename-documenting plan docs + +Auto-exclude any match in the active plan/work-notes documents that document THIS rename (a plan file, migration notes, a changelog entry drafted for the rename) from ALL buckets. Those documents *document the rename*; both old and new names appear by design in scope tables, success criteria, and decision logs. + +Identify these documents from conversation context and the consuming repository's work-notes conventions (its CLAUDE.md / rules). When in doubt, exclude the file that documents the rename and report it in the Excluded row so the user can widen scope deliberately. + +## Special case: Frozen historical records + +Auto-exclude archived/completed work notes and frozen records of past work (finished plan documents, past changelog entries, retired design notes) — they are a frozen-in-time record of finished work. If the consuming repository marks work-notes status in frontmatter or by directory convention, use that signal; otherwise treat clearly-archived paths as frozen. + +## Special case: Memory entries + +Claude Code auto-memory entries at `~/.claude/projects/*/memory/*.md` and `MEMORY.md` indices describe past renames as part of the user's institutional knowledge. They are intentionally append-only historical record. + +Auto-exclude all memory paths from sweeps. If user explicitly wants to rename within memory (rare), they must opt in: `/rename-references to --include-memory`. + +## Reporting bucket counts + +After triage, present a summary before any Edit calls: + +```text +Rename: +Sweep results across tracked files: + +| Bucket | Count | Default action | +|-----------------|-------|----------------------| +| Certain | | Auto-apply | +| Chain-context | | Review-recommended | +| Ambiguous | | Per-match confirm | +| Excluded | | Skipped (plan-docs/historical/memory) | + +Proceed? +``` + +Use `AskUserQuestion` to gate the proceed/abort decision.