diff --git a/plugins/docs-hygiene/.claude-plugin/plugin.json b/plugins/docs-hygiene/.claude-plugin/plugin.json index 9d971f25b..45e6a1357 100644 --- a/plugins/docs-hygiene/.claude-plugin/plugin.json +++ b/plugins/docs-hygiene/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "docs-hygiene", - "version": "0.18.3", + "version": "0.19.0", "description": "Documentation-hygiene toolkit: compress (flavor-trim markdown with a semantic-diff safety net), audit-noise (classify markdown noise), extract-ssot (deduplicate repeated content into a single source of truth), audit-encapsulation (detect citations into skill-private surfaces), rename-references (sweep stale references after renames), audit-derivability (classify whether a whole document earns its existence \u2014 could a fresh agent re-derive it from the code?), audit-progressive-disclosure (grade instruction files against a load-tier model for split opportunities and hub/spoke disclosure defects), write-for-agents (authoring-time doctrine that fires while agent-consumed markdown is being written), and write-for-humans (the same moment for the other reader — end-user READMEs, RFCs, release notes and guides — resolving the consuming project's own style guide first).", "author": { "name": "Melodic Software", diff --git a/plugins/docs-hygiene/CHANGELOG.md b/plugins/docs-hygiene/CHANGELOG.md index b554bf338..5975e6da5 100644 --- a/plugins/docs-hygiene/CHANGELOG.md +++ b/plugins/docs-hygiene/CHANGELOG.md @@ -1,5 +1,84 @@ # Changelog — docs-hygiene plugin +## [0.19.0] + +### Changed + +- **`extract-ssot` reports duplication at every multiplicity; the Rule of Three now gates + artifact creation, not reporting (#3114).** One threshold had been doing two jobs. Gating + *creation of a new SSOT artifact* at three instances is what the cited evidence supports + (~19% failure on curated skills, ~50% on practitioner-authored ones) — but the same number + was also deciding whether the user heard about the duplication at all, so two real defect + classes were discarded in silence: a consumer inlining a recap of an SSOT that already + exists (N=1), and two files asserting the same contract with no declared owner, drifting + apart (N=2). + + `identify` now rosters candidates in three labelled buckets with the instance count shown + per candidate: **N=1** (inline recap of an existing SSOT), **N=2** (source-of-truth + bifurcation risk), **N≥3** (Rule of Three met). `verify` Gate 1 assigns that bucket from the + full-reproduction count and emits it in a new `bucket:` output field; + `REFUSE-rule-of-three-fails` is retained as the reason code but now fires only against an + *artifact-creating* remedy (`rule-file` / `new-skill` / `new-action`) below three — never + against reporting, and never against the non-abstracting remedies. Gate 4 gains the + intentional-vs-accidental split: a deliberate two-audience bifurcation still refuses, while + accidental bifurcation with no declared owner PROCEEDs as the N=2 bucket's own defect. + + **Lowering the reporting threshold does not lower the abstraction threshold**, because the + sub-three buckets offer only remedies that edit files already present. The 6-test gate is + untouched and still governs every N≥3 extraction. + +### Added + +- **Two non-abstracting remedies for `extract-ssot` (#3114).** `normalize-wording` (align + divergent phrasings onto the canonical or agreed wording in place) and `name-an-owner` + (declare one existing file the canonical owner and make the other cite it). Neither creates + a file. They join `trim-to-citation` and `edit-existing-rule` in the suggested-output + vocabulary, and they are what make a rule-of-one reporting default safe. +- **Five flags on the `identify` / `batch` surfaces (#3114).** `--min-instances=` (default + `1`; `--min-instances=3` is the regression guard that reproduces the pre-bucket behavior + exactly), `--buckets=`, `--fix` (applies only `trim-to-citation` and + `normalize-wording`, never creates an artifact), `--dry-run`, and `--yes`. Bare invocation + stays read-only: it reports the buckets and stops. +- **Four eval expectations and two new eval cases** covering the N=1 bucket and the + `--min-instances=3` regression guard; the former `refuse-below-rule-of-three` case is now + `two-instances-bucketed-no-new-artifact` and asserts both halves — the candidate is + rostered, and no new artifact is proposed below three. + +### Fixed + +Four defects in the bucket design above, surfaced by automated review of the shipping PR +(#3114): + +- **`trim-to-citation` is part of the N=2 permitted-remedy set.** The bucket contract and the + `verify` permitted-remedies schema had listed only `edit-existing-rule` / `name-an-owner` / + `normalize-wording`, none of which removes two redundant recaps when the canonical home + already exists and is complete — even though the routing rules already prescribed + `trim-to-citation` for that case. N=2 is now described as the two shapes it actually covers: + two consumers recapping an existing home (trim both to citations), or two files asserting one + contract with no declared owner (name one). `REFUSE-rule-of-three-fails` is now stated + positively — it fires only against `rule-file` / `new-skill` / `new-action` below N≥3 — + instead of enumerating the remedies it spares, which is what let the set drift incomplete. +- **Sibling routing thresholds match the new entry point.** `/docs-hygiene:compress`, + `/docs-hygiene:audit-noise`, `/docs-hygiene:audit-derivability`, + `/docs-hygiene:write-for-agents`, and `/docs-hygiene:write-for-humans` each routed cross-file + duplication to `/docs-hygiene:extract-ssot` only at 3+ files, so the sub-three buckets were + unreachable from the flows that feed them. They now route repeated content at any multiplicity; + creating a NEW artifact still waits for the third instance. `/docs-hygiene:compress`'s + `context/integration.md` boundary note, which restated the old 3+ threshold in prose, was + reconciled with the same rule. +- **`verify` Gate 1 counts semantic clusters by reading, not phrase grep.** A paraphrase cluster + (`identify` forms c2/i) shares no verbatim ≥8-word phrase, so a phrase grep found only the + file the phrase was lifted from — assigning a real N=2/N≥3 cluster to N=1 and, with no prior + canonical, returning `REFUSE-not-found`, after which the mandatory `batch` verify filter + dropped the candidate. Gate 1 now counts by evidence shape (phrase grep for literal clusters, + the reading-derived canonical-truth roster for semantic ones) and gained a semantic Tier 0 + evidence form; Gate 0's `REFUSE-not-found` fires only when neither grep nor reading resolves + any instance. +- **The `batch` per-dispatch verdict enum covers completed non-abstracting remedies.** Step 8's + schema offered only `EXTRACTED` / `REFUSED-*` / `DEFERRED`, none of which fits a sub-three + bucket that finished its work without creating an artifact; it gains `REMEDIED-{remedy}`, so + the schema and the Step 10 batch-summary example agree. + ## [0.18.3] ### Changed diff --git a/plugins/docs-hygiene/README.md b/plugins/docs-hygiene/README.md index 26aa5c842..8b48d2a14 100644 --- a/plugins/docs-hygiene/README.md +++ b/plugins/docs-hygiene/README.md @@ -12,7 +12,7 @@ and authoring axes of doc upkeep. |---|---| | `/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:audit-noise` | 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:extract-ssot` | Deduplicates repeated content into a single named source of truth and migrates call sites to cite it by heading. Reports duplication at every multiplicity in three labelled buckets — a lone recap of an existing SSOT, a drifting pair with no declared owner, and clusters that meet the Rule of Three — while refuse-fast verification gates (Rule of Three, Tier-0 evidence) keep *creating* a new artifact reserved for 3+ instances; below that, only non-abstracting remedies are offered. | | `/docs-hygiene:audit-encapsulation` | 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. | | `/docs-hygiene:audit-derivability` | Read-only, document-level worth classifier: could a fresh agent re-derive this whole document from the code, config, and structure? Weighs derivability, re-derivation cost, drift risk, and fact ownership into a verdict (delete, convert-to-pointer, keep-as-derivation-cache, keep-owns-facts), splits it by audience, and confirms load-bearing deletions with a fresh-context spot-test. Where the other five trim *inside* a doc, this decides whether the doc should exist. | diff --git a/plugins/docs-hygiene/skills/audit-derivability/SKILL.md b/plugins/docs-hygiene/skills/audit-derivability/SKILL.md index cd209a2cf..a6c017ec5 100644 --- a/plugins/docs-hygiene/skills/audit-derivability/SKILL.md +++ b/plugins/docs-hygiene/skills/audit-derivability/SKILL.md @@ -147,7 +147,7 @@ After the ledger, OFFER to route actionable verdicts (delete / convert-to-pointe ## What this skill is NOT - **Not `/docs-hygiene:audit-noise`.** That classifies line-level noise *inside* a document worth keeping. This decides whether the whole document is worth keeping. A doc can pass audit-derivability (`keep-owns-facts`) and still have noise lines for audit-noise to trim. -- **Not `/docs-hygiene:extract-ssot`.** That deduplicates a unit repeated across 3+ files into one home. Derivability is re-derivation from CODE/config/structure, not from another markdown file. A doc that duplicates *another doc* is extract-ssot's; a doc that restates *the code* is this skill's. +- **Not `/docs-hygiene:extract-ssot`.** That deduplicates a unit repeated across files into one home, at any multiplicity (a new home is created only at 3+). Derivability is re-derivation from CODE/config/structure, not from another markdown file. A doc that duplicates *another doc* is extract-ssot's; a doc that restates *the code* is this skill's. - **Not `/docs-hygiene:compress`.** That trims prose flavor within a doc that stays. This deletes or repoints whole docs. - **Not a doc-drift / staleness detector.** Those ask "does this doc still match the code?" This asks "should this doc exist even when it is perfectly accurate?" — a currently-correct doc can still be dead weight because it is trivially re-derivable and carries drift risk. - **Not a doc generator or an Edit operation.** It recommends; the author acts. diff --git a/plugins/docs-hygiene/skills/audit-noise/SKILL.md b/plugins/docs-hygiene/skills/audit-noise/SKILL.md index 3cea92c5b..fa773d745 100644 --- a/plugins/docs-hygiene/skills/audit-noise/SKILL.md +++ b/plugins/docs-hygiene/skills/audit-noise/SKILL.md @@ -133,7 +133,7 @@ Total: file(s) audited, Tier 1, Tier 2, Tier 3 findings. - **Not `/docs-hygiene:compress`.** The sibling `/docs-hygiene:compress` owns FLAVOR (filler, hedging, articles, redundant restatement); `/docs-hygiene:audit-noise` 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); `/docs-hygiene:audit-noise` 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 `/docs-hygiene:extract-ssot`'s territory. +- **Not a content deduplicator.** When the noise is the same concept repeated across files, that is the sibling `/docs-hygiene:extract-ssot`'s territory at any multiplicity — sub-three repetition lands in its non-abstracting buckets, and only minting a new SSOT artifact waits for 3+. ## Sources diff --git a/plugins/docs-hygiene/skills/compress/SKILL.md b/plugins/docs-hygiene/skills/compress/SKILL.md index 0793d2387..9ee91355f 100644 --- a/plugins/docs-hygiene/skills/compress/SKILL.md +++ b/plugins/docs-hygiene/skills/compress/SKILL.md @@ -139,7 +139,7 @@ Observed failure points — each traces to a real incident; grown iteratively. - **Not a code-comment compressor.** Out of scope - **Not a `/code-review` / `/simplify` shadow.** The bundled `/code-review` and `/simplify` skills review code changes; `/docs-hygiene:compress` rewrites markdown prose. Different concerns - **Not `/docs-hygiene:audit-noise`.** `/docs-hygiene:compress` owns FLAVOR (filler, hedging, articles, redundant restatement). `/docs-hygiene:audit-noise` 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 `/docs-hygiene:extract-ssot` when the duplicated cluster spans 3+ files +- **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); route the duplicated cluster to `/docs-hygiene:extract-ssot` at any multiplicity (it rosters rule-of-one / -two / -three buckets; only extraction into a NEW artifact waits for 3+ files) ## Sources diff --git a/plugins/docs-hygiene/skills/compress/context/integration.md b/plugins/docs-hygiene/skills/compress/context/integration.md index 09850d52d..7336a0892 100644 --- a/plugins/docs-hygiene/skills/compress/context/integration.md +++ b/plugins/docs-hygiene/skills/compress/context/integration.md @@ -12,7 +12,7 @@ How `/docs-hygiene:compress` composes with sibling skills in this plugin and wit | `/docs-hygiene:audit-encapsulation` | parallel concern | No invocation either direction. `/docs-hygiene:audit-encapsulation` detects external citations into skill-private surfaces; `/docs-hygiene: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 `/docs-hygiene:compress` | When a pre-PR check surfaces uncommitted `.md` files in the working tree, the user may invoke `/docs-hygiene:compress` (empty arg auto-detects) before PR prep. `/docs-hygiene:compress` does not auto-trigger from any gate; user-gated | -Boundaries with the other bundled siblings — `/docs-hygiene:audit-noise` (noise classification, not flavor) and `/docs-hygiene:extract-ssot` (content relocation across 3+ files, not flavor) — are defined in `../SKILL.md` "What this skill is NOT". +Boundaries with the other bundled siblings — `/docs-hygiene:audit-noise` (noise classification, not flavor) and `/docs-hygiene:extract-ssot` (content relocation at any multiplicity, not flavor — it rosters rule-of-one / -two / -three buckets; only extraction into a NEW artifact waits for 3+ files) — are defined in `../SKILL.md` "What this skill is NOT". ## Public-surface invocation forms diff --git a/plugins/docs-hygiene/skills/extract-ssot/SKILL.md b/plugins/docs-hygiene/skills/extract-ssot/SKILL.md index 7e11b5a8d..b4b9e8bfe 100644 --- a/plugins/docs-hygiene/skills/extract-ssot/SKILL.md +++ b/plugins/docs-hygiene/skills/extract-ssot/SKILL.md @@ -1,6 +1,6 @@ --- -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] []" +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 files: 'DRY this prose', 'extract a shared rule', 'single source of truth for X', a value-bump diff touching several files. Reports duplication at every multiplicity in rule-of-one / rule-of-two / rule-of-three buckets, offering only non-abstracting remedies below three — creating a NEW SSOT artifact still refuses below the Rule of Three." +argument-hint: "[identify|verify|plan|execute|batch|unwind] [] [--min-instances=] [--buckets=] [--fix] [--dry-run] [--yes]" user-invocable: true disable-model-invocation: false metadata: @@ -14,7 +14,9 @@ metadata: 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. +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, minting a new SSOT artifact is premature abstraction — the dominant failure mode — and the skill refuses it. + +Refusing to *create* is not refusing to *report*. A lone consumer that recaps an SSOT it should be citing, and a pair of files asserting the same contract with no declared owner, are real defects that drift. Both are rostered in their own multiplicity bucket and remedied in place, with no new artifact. Typical markdown extraction shapes the workflow handles: @@ -51,7 +53,7 @@ Boundary with single-file refactoring: a rename, inline, or extract confined to ## 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. +**Use** when: the same unit appears more than once across files (3+ instances is the bar for *creating* a new SSOT artifact; 1 and 2 route to the non-abstracting buckets); 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 tracked files, (b) value-bump diff touches multiple unrelated files, (c) same concept reworded across 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:** @@ -70,8 +72,8 @@ Full decision matrix: `context/decision-framework.md` (6+5 checklist with worked | Argument | Action | Purpose | |----------|--------|---------| | *(empty)* | Smart default | Auto-detect: working notes from a prior run hold an active candidate roster → resume the current phase; the invocation or conversation already names a scope → `identify`; otherwise → confirm scope with the user first (see "Bare invocation — confirm scope first") | -| `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 `/docs-hygiene: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` | +| `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 `/docs-hygiene:extract-ssot batch` invocation. Rosters every surviving candidate in a labelled multiplicity bucket (N=1 / N=2 / N≥3) with its instance count; artifact-creating outputs stay reserved for N≥3. Single-cluster mode (`identify `) skips the subagent for a targeted Tier 0 grep | +| `verify ` | Refuse-fast pre-extraction gate | 6-gate cheap check (bucket assignment + Tier 0 grep, citation state, primary-source URL gate, bifurcation check, off-by-one heuristic, LOW-ROI threshold). Output: `PROCEED \| REFUSE-{reason} \| WARN` plus the assigned `bucket:`. 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 by invoking `/docs-hygiene:rename-references` via the Skill tool 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` | @@ -79,6 +81,20 @@ Full decision matrix: `context/decision-framework.md` (6+5 checklist with worked One action per response; actions don't chain implicitly. +### Flags + +Accepted by `identify` and `batch` (the roster-producing surfaces); `batch` passes them through to each dispatched `identify`/`verify`. + +| Flag | Default | Behavior | +|------|---------|----------| +| `--min-instances=` | `1` | Lowest bucket to roster. `--min-instances=3` is the regression guard — it reproduces the pre-bucket behavior where sub-three clusters never reach the user | +| `--buckets=` | all | Filter the roster to the named buckets, e.g. `--buckets=1,2` for the non-abstracting work only | +| `--fix` | off | Apply ONLY the non-abstracting remedies (`trim-to-citation`, `normalize-wording`). Never writes a new artifact; still honors the per-bucket review gate unless `--yes` | +| `--dry-run` | off | Print the diff `--fix` would apply; write nothing | +| `--yes` | off | Non-interactive; skip the per-bucket review gate. Only meaningful with `--fix` | + +Bare invocation (no flags) stays read-only: it reports the buckets and stops, matching `/docs-hygiene:audit-noise` and `/docs-hygiene:audit-derivability`. Full flag semantics: `actions/identify.md`. + ## Bare invocation — confirm scope first Shared clean-tree / no-scope shape: [`../../context/clean-tree-fallback.md`](../../context/clean-tree-fallback.md). @@ -109,9 +125,17 @@ whole-repo intent. ## 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`. +Before recommending a NEW SSOT artifact, 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 *creation of a new SSOT artifact* at <3 instances. Premature abstraction is the dominant failure mode. Rule of Three gates artifact creation, NOT reporting: every candidate is rostered at its own multiplicity, and the bucket decides which remedies are on the table. + +| Bucket | Rostered? | Permitted remedies | Creates a new artifact? | +|---|---|---|---| +| N=1 — inline recap of an existing SSOT | always | `trim-to-citation`, `normalize-wording` | never | +| N=2 — two consumers recap an existing home, or two files assert one contract with no declared owner (bifurcation risk) | always | `trim-to-citation`, `edit-existing-rule`, `name-an-owner`, `normalize-wording` | never | +| N≥3 — Rule of Three met | always | all of the above, plus `rule-file` / `new-skill` / `new-action` | only behind the 6-test gate | -Headline gate: **Rule of Three** (Don Roberts / Fowler) — refuse extraction at <3 instances. Premature abstraction is the dominant failure mode. +Lowering the reporting threshold does not lower the abstraction threshold: the sub-three buckets offer only remedies that edit files already present. Full rationale: `context/decision-framework.md` "Reporting gate vs abstraction gate". ## Output type @@ -120,9 +144,11 @@ 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 | +| Divergent phrasings of one agreed truth | `normalize-wording` — align every site onto the canonical/agreed wording in place; **no new artifact** | Instances say the same thing differently and the drift itself is the defect. Available in every bucket, including N=1 and N=2 | +| Two files assert the same contract, neither declared canonical | `name-an-owner` — declare one existing file the owner; the other cites it | Accidental source-of-truth bifurcation (anti-pattern #11, accidental branch); the N=2 bucket's default remedy. **No new artifact** | +| 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. **Artifact-creating — N≥3 only, behind the 6-test gate** | +| 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 `/`. **Artifact-creating — N≥3 only, behind the 6-test gate** | +| 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. **Artifact-creating — N≥3 only, behind the 6-test gate** | 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. @@ -171,7 +197,8 @@ Per-phase checklist: `context/execution-checklist.md`. | When | Check | Evidence | |------|-------|----------| -| Pre-extraction | 3+ instances confirmed via grep (Tier 0) | Grep output captured in the plan/working notes | +| Pre-roster | Instance count confirmed via grep (Tier 0) and the bucket assigned from it | Grep output captured in the plan/working notes | +| Pre-new-artifact | 3+ full reproductions confirmed via grep (Tier 0); at 1-2 the remedy stays non-abstracting | 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 | @@ -209,4 +236,4 @@ Per-phase checklist: `context/execution-checklist.md`. | 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 | | `/docs-hygiene: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 `/docs-hygiene: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 | +| Practitioner-authored skill failure rate drops below 20% (SkillsBench refresh) | Reduce gate strictness; consider relaxing the artifact-creation gate from Rule of Three to Two for low-risk vocabulary (the reporting buckets are unaffected — they already roster at one) | diff --git a/plugins/docs-hygiene/skills/extract-ssot/actions/batch.md b/plugins/docs-hygiene/skills/extract-ssot/actions/batch.md index 63a558378..2a3ba628f 100644 --- a/plugins/docs-hygiene/skills/extract-ssot/actions/batch.md +++ b/plugins/docs-hygiene/skills/extract-ssot/actions/batch.md @@ -38,12 +38,18 @@ This is NOT the bundled Claude Code `/batch` skill. Bundled `/batch` is polyglot ```text /docs-hygiene:extract-ssot batch [ ... ] + [--min-instances=] [--buckets=] [--fix] [--dry-run] [--yes] ``` OR resume from working notes if a `batch` phase is mid-flight. Candidate names match `/docs-hygiene:extract-ssot identify` output's cluster names. +`--min-instances`, `--buckets`, `--fix`, `--dry-run`, and `--yes` pass through to the batch surface +with the semantics defined in `actions/identify.md` "Flags": `--min-instances` / `--buckets` filter +which buckets enter the dispatch list, and `--fix` / `--dry-run` / `--yes` govern the +non-abstracting remedy sweep. `--fix` never creates an artifact in any wave. + ## Steps ```text @@ -71,17 +77,32 @@ For each candidate, invoke `verify` (private action — see `actions/verify.md`) 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. +Each verdict carries the bucket `verify` Gate 1 assigned. A sub-three bucket is not a refusal — an +N=1 or N=2 candidate PROCEEDs with its non-abstracting remedies and stays in the dispatch list. +A semantic candidate (`identify` forms c2/i) is counted by `verify` Gate 1's reading-derived roster, +not by phrase grep, so this filter must not drop it as `REFUSE-not-found` on a one-file grep hit. + ```yaml candidate: verify-status: PROCEED | REFUSE-{reason} | WARN +bucket: N=1 | N=2 | N>=3 +permitted-remedies: [...] verify-evidence: [...] ``` -Output forms the batch summary's first column. +Output forms the batch summary's first two columns. ## 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. +Drop candidates with `REFUSE-*` status from the dispatch list. Keep `PROCEED` + `WARN` at every +bucket — N=1 and N=2 candidates survive the filter and dispatch with the non-abstracting remedies +their bucket permits, never an artifact-creating one. Apply `--min-instances` / `--buckets` here as +a second, caller-chosen filter; record what they excluded so a suppressed bucket does not read as an +empty one. Surface the dropped candidates with reasons in the batch audit log so the user sees the +refuse-fast savings. + +The ≥80%-refusal abort check counts only `REFUSE-*` verdicts. Bucket distribution is a reporting +fact, not a refusal — a roster that is mostly N=1 is a healthy finding, not a failed identify pass. ## Step 4 — File-overlap matrix @@ -167,12 +188,17 @@ Each subagent return value contains: ```yaml candidate: -verdict: EXTRACTED | REFUSED-{reason} | DEFERRED +verdict: EXTRACTED | REMEDIED-{remedy} | REFUSED-{reason} | DEFERRED files-modified: [...] new-lessons: [free-form patterns observed] sanity-check-evidence: [...] ``` +`REMEDIED-{remedy}` is the verdict for a completed non-abstracting remedy, `{remedy}` one of +`trim-to-citation` / `normalize-wording` / `name-an-owner` / `edit-existing-rule` — the outcome a +sub-three bucket produces, since none of those creates an artifact. `EXTRACTED` remains the N≥3 +artifact-creation outcome. + `new-lessons` is the field where empirical patterns surface for the orchestrator to codify. ## Step 9 — Lesson append @@ -201,12 +227,13 @@ 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) | -| ... | | | | | | +| # | Candidate | Bucket | Verify | Verdict | Wave | Files modified | +|---|-----------|--------|--------|---------|------|----------------| +| 1 | C1 | N≥3 | PROCEED | EXTRACTED | 1 | path1, path2 | +| 2 | C2 | N≥3 | PROCEED | REFUSED-low-roi | 1 | (none) | +| 3 | C3 | N=1 | REFUSE-already-cites-canonical | (skipped) | (n/a) | (none) | +| 4 | C4 | N=2 | PROCEED | REMEDIED-name-an-owner | 2 | path3, path4 | +| ... | | | | | | | ## File-overlap matrix diff --git a/plugins/docs-hygiene/skills/extract-ssot/actions/identify.md b/plugins/docs-hygiene/skills/extract-ssot/actions/identify.md index db3242177..9022fd530 100644 --- a/plugins/docs-hygiene/skills/extract-ssot/actions/identify.md +++ b/plugins/docs-hygiene/skills/extract-ssot/actions/identify.md @@ -4,7 +4,9 @@ - [Two modes](#two-modes) - [When to invoke](#when-to-invoke) +- [Multiplicity buckets](#multiplicity-buckets) - [Inputs](#inputs) +- [Flags](#flags) - [Exhaustive mode steps](#exhaustive-mode-steps) - [Subagent prompt template](#subagent-prompt-template) - [Output shape (exhaustive mode)](#output-shape-exhaustive-mode) @@ -36,6 +38,30 @@ User signals like "find ANY and ALL", "deep dive", "exhaustive", "full list", or | User has one cluster in mind already | Targeted | | Resume from working notes holding a candidate roster | Skip identify; route to `verify` / `plan` / `execute` / `batch` | +## Multiplicity buckets + +`identify` rosters candidates at EVERY multiplicity. The Rule of Three gates which remedies a +candidate may be offered — never whether the candidate reaches the user. `N` is the count of full +reproductions under the evidence discipline below (discriminating-phrase grep for literal clusters, +reading-driven canonical-truth clustering for semantic ones) — not keyword density, not +section-header count. + +| Bucket | What it means | Permitted `Suggested output` | Creates a new artifact? | +|---|---|---|---| +| **N=1** | An inline recap of an SSOT that ALREADY EXISTS — one consumer restates the canonical instead of citing it | `trim-to-citation`, `normalize-wording` | never | +| **N=2** | Two shapes: two consumers recap a canonical home that already exists (remedy: trim both to citations), OR two files assert the same contract and neither is the declared owner, so they drift — bifurcation risk (remedy: name-an-owner) | `trim-to-citation`, `edit-existing-rule`, `name-an-owner`, `normalize-wording` | never | +| **N≥3** | Rule of Three met | all of the above, plus `rule-file` / `new-skill` / `new-action` | only behind the 6-test gate (`context/decision-framework.md`) | + +**The N=1 bucket is NOT "report every paragraph".** A lone paragraph with no existing canonical +home is not duplication — nothing is being duplicated — and is NOT rostered. The N=1 bucket admits +a candidate only when the SSOT-existence check finds a canonical home the site should be citing. +That precondition is what keeps a rule-of-one default from degenerating into report-everything. + +**N=1 and N=2 candidates can NEVER be routed to `rule-file` / `new-skill` / `new-action`.** A +sub-three candidate carrying an artifact-creating suggested output is a roster defect; correct it +to the bucket's permitted set before emitting. `verify` Gate 1 refuses it independently +(`REFUSE-rule-of-three-fails`). + ## Inputs ```text @@ -43,6 +69,23 @@ User signals like "find ANY and ALL", "deep dive", "exhaustive", "full list", or /docs-hygiene:extract-ssot identify # targeted ``` +## Flags + +Read-only is the default. A bare invocation (no flags) rosters the buckets, reports, and stops — +it applies no edits. `batch` accepts the same flags and passes them through. + +| Flag | Default | Behavior | +|------|---------|----------| +| `--min-instances=` | `1` | Lowest bucket to roster. `--min-instances=2` drops the N=1 bucket; `--min-instances=3` is the **regression guard** — it reproduces the pre-bucket behavior exactly, rostering only N≥3 clusters and refusing sub-three candidates outright | +| `--buckets=` | all | Comma-separated bucket filter applied to the roster, e.g. `--buckets=1,2` for the non-abstracting work only. Composes with `--min-instances`; the narrower of the two wins | +| `--fix` | off | Apply ONLY the non-abstracting remedies — `trim-to-citation` and `normalize-wording`. It NEVER creates a new artifact and never applies `name-an-owner` / `edit-existing-rule` (those change which file is canonical — a judgment call that stays with the user). Honors the per-bucket review gate unless `--yes` | +| `--dry-run` | off | Print the diff `--fix` would apply; write nothing. Implies no edits even if `--fix` is also passed | +| `--yes` | off | Non-interactive; skip the per-bucket review gate. Only meaningful alongside `--fix` | + +**Per-bucket review gate.** With `--fix` and without `--yes`, present the proposed edits one bucket +at a time and take the user's decision per bucket before writing. This keeps the N=1 sweep — the +highest-volume bucket — from landing as one unreviewable diff. + ## Exhaustive mode steps ```text @@ -55,10 +98,14 @@ User signals like "find ANY and ALL", "deep dive", "exhaustive", "full list", or 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 +5. Main session classifies output: assign each candidate its bucket from the instance count; apply + --min-instances / --buckets; deduplicate against context/lessons.md known-refused patterns; + downgrade any sub-three candidate carrying an artifact-creating suggested output 6. Main session emits batch-sequencing recommendation (waves, sequential vs parallel, hot files) -7. Main session offers user: dispatch /docs-hygiene: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 +7. Main session offers user: dispatch /docs-hygiene:extract-ssot batch with top-N waves, or pick + specific clusters. With --fix, walk the non-abstracting remedies one bucket at a time through + the review gate (skipped by --yes); without --fix, report and stop +8. Persist the roster (buckets included) to working notes so the user can resume from durable state ``` ## Subagent prompt template @@ -95,9 +142,13 @@ In-scope authoring surfaces (adapt to what this repo actually has): ## Discrimination rules 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 +(i) count as duplication candidates. Form (c2) full-paragraph semantic reword also counts when the stability+reader-burden test passes. +A form's YES/NO decides whether the thing is duplication at all. The instance count then decides +the BUCKET, never whether the candidate is dropped: a YES at 1 or 2 instances lands in the N=1 or +N=2 bucket with that bucket's permitted remedies. A NO form is still discarded at any count. + | Form | Counts as duplication? | Example | |------|------------------------|---------| | (a) Verbatim block reproduction (≥15 words, copy-paste) | YES | The same dependency-direction rule text in 5 files | @@ -109,13 +160,14 @@ when the stability+reader-burden test passes. | (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 | +| (i) Semantic-paraphrase cluster — 2+ instances assert same canonical truth in different wording; no shared verbatim ≥8 word phrase but reader could not tell which is canonical | YES — roster 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: +**Stability + reader-burden combined test — applies to forms (c2), (e), (i).** Roster 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. +If only ONE passes: borderline (mark WARN). If NEITHER: REFUSE-low-roi. At N=2 only the +reader-burden branch can pass — which is exactly the N=2 bucket's defect (no declared owner). **Two-pass survey required.** Run BOTH: - **Pass A — literal:** verbatim discriminating-phrase grep. Catches (a). @@ -146,7 +198,7 @@ For EACH candidate, capture (NOT optional). Use ONE of two evidence shapes depen **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. +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. This check is also the N=1 bucket's admission gate: a single site is rostered ONLY when a canonical home exists that it recaps instead of cites; with no existing home, a lone paragraph is not duplication and is dropped. 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. @@ -225,11 +277,17 @@ c. **Don't stop at the pre-seeded list.** As reading progresses, surface NEW con For EACH candidate cluster (both passes), capture: - Cluster name (kebab-case slug) - File list with line ranges where possible -- Instance count +- Instance count (full reproductions) +- Bucket: N=1 | N=2 | N≥3 — assigned from the instance count; MUST be emitted with every candidate - 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 +- Suggested output, constrained to the bucket's permitted set: + - any bucket: `trim-to-citation` | `normalize-wording` (align divergent phrasings onto the + canonical/agreed wording in place; no new file) + - N=2 and up: `edit-existing-rule` | `name-an-owner` (declare one existing file the canonical + owner and make the other cite it; no new file) + - N≥3 only: `rule-file` | `new-skill` | `new-action` + - any bucket, out-of-scope advisory: `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) @@ -240,11 +298,22 @@ an existing rule/skill/doc already owns the concept and ≥1 consumer still reca 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. +Gate 2). Reserve `rule-file` / `new-skill` / `new-action` for concepts with NO existing home **and** N≥3. + +Bucket routing of the pre-check: + +- **N=1, home exists** → `trim-to-citation` (replace the recap with a citation); add + `normalize-wording` when the recap has drifted from the home's wording. +- **N=2, home exists** → `trim-to-citation` / `edit-existing-rule` against that home. +- **N=2, no home** → `name-an-owner`: pick the better-placed of the two files as canonical, make + the other cite it. `normalize-wording` first if the two have already drifted. This is the + accidental branch of anti-pattern #11; the intentional two-audience case still refuses (`verify` + Gate 4). +- **N≥3, no home** → creation output, subject to the 6-test gate. -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. +Output: ONE ranked table PER BUCKET (three labelled sections, ROI desc, dependency-grouped within +each). 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). @@ -256,21 +325,34 @@ Time budget: large. Aim thoroughness > speed. Do NOT edit files. Main session presents to user: +Every candidate table carries the bucket and the instance count per row, and the roster is grouped +into the three labelled bucket sections. Bucket sections the flags filtered out are still named, +with a one-line note saying they were suppressed and by which flag — a silently missing bucket +reads as "nothing found there". + ```markdown -# Duplication survey — N candidates +# Duplication survey — N candidates (N=1: a | N=2: b | N≥3: c) + +## Bucket N≥3 — Rule of Three met (artifact creation permitted, 6-test gate applies) -## HIGH ROI (no dependencies, ⭐ SSOT-exists-but-inlined) - +### HIGH ROI (no dependencies, ⭐ SSOT-exists-but-inlined) + -## HIGH ROI (with dependencies) +### HIGH ROI (with dependencies) -## MEDIUM ROI +### MEDIUM ROI -## LOW ROI / advisory +### LOW ROI / advisory +## Bucket N=2 — recap pair or source-of-truth bifurcation risk (no new artifact) + + +## Bucket N=1 — inline recap of an existing SSOT (no new artifact) + + ## Code/config advisory (out of scope) @@ -294,9 +376,12 @@ The ranked table + wave plan is then persisted to working notes so the user can ```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 `/docs-hygiene:extract-ssot verify ` +3. Assign the bucket from the full-reproduction count (N=1 / N=2 / N>=3). At N=1, confirm the + admission gate: a canonical home exists that this site recaps instead of cites +4. Run quick instance-stability check (do they change together?). At N>=3 the Rule of Three is met + and artifact creation is on the table; below it, the remedy set is the bucket's non-abstracting one +5. Suggest output type per `context/decision-framework.md`, constrained to the bucket +6. Return candidate spec (bucket included) ready for `/docs-hygiene:extract-ssot verify ` ``` No subagent dispatch. No batch sequencing. Single-cluster sanity check only. @@ -307,6 +392,8 @@ No subagent dispatch. No batch sequencing. Single-cluster sanity check only. - **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 `/docs-hygiene: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 `/docs-hygiene:extract-ssot batch` partial-completes and the user resumes weeks later, re-run `identify` rather than trusting a stale roster. +- **Rule-of-one as report-everything** — rostering a lone paragraph that no canonical home duplicates. The N=1 bucket admits a candidate only when the SSOT-existence check finds the home it should be citing; without that, there is no duplication to report. +- **Bucket leakage** — offering `rule-file` / `new-skill` / `new-action` to an N=1 or N=2 candidate. The reporting threshold moved; the abstraction threshold did not. Constrain the suggested output to the bucket's permitted set before emitting. ## Sanity checks @@ -315,6 +402,8 @@ No subagent dispatch. No batch sequencing. Single-cluster sanity check only. | 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 | +| Post-dispatch | Every candidate carries a bucket + instance count, and no sub-three candidate carries an artifact-creating suggested output | Scan the roster's bucket column | +| Pre-handoff | With `--fix`, only `trim-to-citation` / `normalize-wording` edits are staged, and no new file appears in the diff | `git status` / diff review | | 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 | diff --git a/plugins/docs-hygiene/skills/extract-ssot/actions/verify.md b/plugins/docs-hygiene/skills/extract-ssot/actions/verify.md index c1c5a39fb..8d324a197 100644 --- a/plugins/docs-hygiene/skills/extract-ssot/actions/verify.md +++ b/plugins/docs-hygiene/skills/extract-ssot/actions/verify.md @@ -12,7 +12,7 @@ - [Cross-references](#cross-references) - [Recheck triggers](#recheck-triggers) -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. +Cheap pre-extraction gate. Assigns the candidate's multiplicity bucket, then refuses fast on candidates that wouldn't survive `plan`/`execute` anyway. Surfaces the bucket plus a PROCEED/refusal verdict from a single grep — or, for a semantic cluster, a short read of the candidate files — plus a citation check, without spawning a subagent. A sub-three bucket is a remedy constraint, not a refusal. Private surface — external consumers invoke `/docs-hygiene:extract-ssot verify `, never cite this file directly (contract: `/docs-hygiene:audit-encapsulation`). @@ -24,7 +24,7 @@ Private surface — external consumers invoke `/docs-hygiene:extract-ssot verify | User typed `/docs-hygiene: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 | +| Cluster has 1 or 2 instances | YES — `identify` no longer refuses these; `verify` assigns the bucket and returns the bucket-appropriate non-abstracting remedies | `verify` is OPTIONAL. It does NOT gate `plan`/`execute` automatically — preserves user agency. Skipping `verify` and going straight to `plan` is supported. @@ -40,7 +40,10 @@ Private surface — external consumers invoke `/docs-hygiene:extract-ssot verify ```yaml status: PROCEED | REFUSE-{reason} | WARN +bucket: N=1 | N=2 | N>=3 # assigned by Gate 1 from the full-reproduction count reason-code: +permitted-remedies: # the bucket's remedy set; empty on REFUSE + - evidence: - - @@ -50,19 +53,22 @@ next-action: notes: ``` +`bucket:` is emitted on EVERY verdict, refusals included — the caller needs it to file the candidate +in the right roster section. + Status values: | Status | Meaning | |--------|---------| -| `PROCEED` | All 6 gates pass; safe to invoke `/docs-hygiene:extract-ssot plan ` | -| `REFUSE-rule-of-three-fails` | < 3 verbatim instances after discriminating-phrase grep (Gate 1) | +| `PROCEED` | All 6 gates pass. `permitted-remedies` lists what the assigned bucket allows: N=1 → `trim-to-citation` / `normalize-wording`; N=2 → `trim-to-citation` / `edit-existing-rule` / `name-an-owner` / `normalize-wording` (trim both recaps when a canonical home exists; name an owner when neither file is one); N≥3 → those plus `rule-file` / `new-skill` / `new-action` behind the 6-test gate. Safe to invoke `/docs-hygiene:extract-ssot plan ` | +| `REFUSE-rule-of-three-fails` | An **artifact-creating** output (`rule-file` / `new-skill` / `new-action`) was suggested or requested at N < 3 (Gate 1). Fires ONLY against those three artifact-creating outputs below N≥3 — never against reporting, and never against any non-abstracting remedy. (The issue vocabulary calls this `REFUSE-premature`; the code here is the canonical one) | | `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-source-of-truth-bifurcation` | **Intentional** bifurcation — top-tier instruction file ↔ rule-file pair both canonical at different tiers for different audiences; forcing a single citation = cycle (Gate 4). Accidental bifurcation does NOT refuse here; it is the N=2 bucket | | `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) | +| `REFUSE-not-found` | Cluster name doesn't resolve to any matching content — neither the literal grep nor the semantic reading found an instance (Gate 0) | ## The 6 gates (ordered checks) @@ -72,26 +78,59 @@ Each gate has Tier 0 evidence requirements — direct grep/read output captured 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. +Map the script output to gate evidence; emit the `status: PROCEED | REFUSE-* | WARN` YAML in the skill — the script never emits verdicts. The script takes a phrase, so it serves literal clusters; a semantic cluster has no shared phrase, and its Gate 1 count comes from reading the candidate files (Gate 1, semantic shape). + +Gate 1 assigns the bucket and gates artifact-creating remedies against it. Gate 4 splits intentional +bifurcation (refuses, any bucket) from accidental (the N=2 bucket's own defect). Gates 0, 3, 5, and 6 +are unchanged and multiplicity-independent: they judge cluster validity and ROI, so their refusals +apply in every bucket. ### Gate 0: Cluster resolution -Before any gate runs, confirm the cluster exists in the repo. +Before any gate runs, confirm the cluster exists in the repo. Resolution is evidence-shape-aware, +matching the two shapes `actions/identify.md` "Per-candidate evidence requirement" defines. -- 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 +- Step 1: classify the cluster's shape. **Literal** (identify forms a, e) — identify a + discriminating phrase from the cluster body (≥ 8 words, unique enough to grep cleanly). + **Semantic** (identify forms c2, i — reproductions share no verbatim ≥ 8-word phrase) — state the + canonical truth in one sentence instead; there is no phrase to grep for +- Step 2: literal → grep the phrase across the repo's tracked markdown. Semantic → read the + candidate files and collect the paragraphs that reproduce that canonical truth in any phrasing +- Step 3: `REFUSE-not-found` fires ONLY when NEITHER the literal grep NOR the semantic reading + resolves any instance. A semantic cluster is **not** "not found" merely because its phrase grep + hits one file — that is the expected grep result for a paraphrase cluster; carry it to Gate 1 and + count it there by reading -### Gate 1: Rule-of-Three via discriminating-phrase grep +### Gate 1: Bucket assignment from an evidence-shape-aware count **Lesson 1** — keyword density over-counts; use discriminating-phrase grep instead. +This gate ASSIGNS the bucket. It does not refuse on count alone. Count by the cluster's evidence +shape, mirroring `actions/identify.md` — discriminating-phrase grep for literal clusters, +reading-driven canonical-truth clustering for semantic ones. Phrase grep applied to a semantic +cluster undercounts it to 1 and files a real N=2/N≥3 paraphrase cluster in the wrong bucket. + +**Literal clusters (identify forms a, e):** + - 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: +**Semantic clusters (identify forms c2, i — no shared verbatim ≥ 8-word phrase):** + +- Read the candidate files; do not rely on phrase grep, which finds only the instance the phrase was lifted from +- Count distinct files whose paragraph reproduces the canonical truth in ANY phrasing — the same reading-derived canonical-truth roster `identify` Pass B builds +- Grep still helps as a file-shortlist (topic keywords, cited concept names); the COUNT comes from the reading +- Exclusions are unchanged: 1-line teaching mentions, citation-only references, and form (h) domain-specific applications are not reproductions + +**Both shapes:** + +- Assign `bucket:` from that count — 1 → `N=1`, 2 → `N=2`, ≥3 → `N>=3` — and emit it +- At `N=1`, confirm the bucket's admission gate: a canonical home exists that this site recaps instead of cites. No existing home + a single site = not duplication → `REFUSE-not-found` (nothing is being reproduced) +- Refuse ONLY on remedy mismatch: if the suggested or user-requested output is artifact-creating (`rule-file` / `new-skill` / `new-action`) and the bucket is `N=1` or `N=2` → `REFUSE-rule-of-three-fails`, naming the bucket's permitted remedies in `next-action` +- Otherwise continue to Gate 2 with `permitted-remedies` set from the bucket + +Tier 0 evidence form — literal shape: ```text Grep pattern: '' @@ -101,6 +140,23 @@ Files matched (full reproductions): - : Files matched (teaching mentions, excluded): - : (1-line mention, kept inline) +Bucket assigned: N=1 | N=2 | N>=3 +Artifact creation: permitted (N>=3, subject to the 6-test gate) | refused (N<3) +``` + +Tier 0 evidence form — semantic shape (no shared verbatim phrase to grep): + +```text +Canonical truth: '' +Files read: +Files reproducing the truth (full reproductions): + - : — "" + - : — "" + - : — "" +Files excluded: + - : (1-line mention | citation-only | form (h) domain-specific application) +Bucket assigned: N=1 | N=2 | N>=3 +Artifact creation: permitted (N>=3, subject to the 6-test gate) | refused (N<3) ``` ### Gate 2: Pre-existing canonical citation check @@ -112,6 +168,10 @@ Files matched (teaching mentions, excluded): - 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 +This gate is the natural terminal for the N=1 bucket: a single site that recaps a canonical home +PROCEEDs with `trim-to-citation`; a single site that already cites it correctly has no work left and +refuses here. + Tier 0 evidence form: ```text @@ -141,13 +201,22 @@ Sample URL(s): **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: +Detect the bifurcation pattern, then split intentional from accidental: - 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. +**Intentional bifurcation** — two tiers, two named audiences, the split reads as deliberate → +`REFUSE-source-of-truth-bifurcation`. Document both canonicals + their respective audiences in the +output `notes:` field. This is the case anti-pattern #11 protects and it refuses at any bucket. + +**Accidental bifurcation** — two files assert the same contract, serve the SAME audience, and +NEITHER is declared the owner. This is not the protected case; it is the N=2 bucket's defect. +Emit `PROCEED` with `bucket: N=2` and `permitted-remedies: [name-an-owner, edit-existing-rule, +normalize-wording]` — this branch has no canonical home yet, so `trim-to-citation` (the other N=2 +shape's remedy) has no target until an owner is named. Creating a third file to own the contract is +NOT among them. Tier 0 evidence form: @@ -157,6 +226,8 @@ Aggregator rule: .md: "" Audiences: Top-tier: Aggregator: +Distinct audiences? +Declared owner? Forcing single citation would create: cycle (instruction file → rule → instruction file) | over-aggregation ``` @@ -205,7 +276,8 @@ ROI verdict: LOW (size + drift indicate inline is cheaper) ```text 1. Read your working notes (resume if mid-phase) 2. Gate 0 — cluster resolution -3. Gate 1 — discriminating-phrase grep, count full reproductions +3. Gate 1 — count full reproductions by evidence shape (phrase grep for literal clusters, reading + for semantic ones), ASSIGN the bucket, gate artifact-creating remedies against it 4. Gate 2 — pre-existing citation check 5. Gate 3 — primary-source citation gate 6. Gate 4 — source-of-truth bifurcation check @@ -217,7 +289,8 @@ ROI verdict: LOW (size + drift indicate inline is cheaper) 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 `/docs-hygiene:extract-ssot plan ` next. +If ALL gates pass, emit `PROCEED` with summary evidence, the assigned `bucket:`, and the bucket's +`permitted-remedies`. User runs `/docs-hygiene:extract-ssot plan ` next. ## Side observations @@ -230,6 +303,7 @@ When a gate REFUSES with high confidence, the cluster may still warrant action | `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 /docs-hygiene:extract-ssot identify with a narrower discriminating phrase per concern.` | +| `REFUSE-rule-of-three-fails` | `Side note: bucket — no new artifact, but still apply; the candidate stays on the roster.` | Hard limit ≤2 side notes per response. If multiple gates fire, batch the rest into the working-notes entry. @@ -245,6 +319,7 @@ type: verify-evidence date: cluster: verdict: +bucket: =3> --- ## Cluster @@ -253,7 +328,7 @@ verdict: | Gate | Result | Evidence | |------|--------|----------| | 0 — Cluster resolution | PASS | matches | -| 1 — Discriminating-phrase grep | | | +| 1 — Bucket assignment (phrase grep / semantic reading) | | | | 2 — Pre-existing citations | | | | ... | | | diff --git a/plugins/docs-hygiene/skills/extract-ssot/context/anti-patterns.md b/plugins/docs-hygiene/skills/extract-ssot/context/anti-patterns.md index 3d7b1f887..b8267aa47 100644 --- a/plugins/docs-hygiene/skills/extract-ssot/context/anti-patterns.md +++ b/plugins/docs-hygiene/skills/extract-ssot/context/anti-patterns.md @@ -119,9 +119,10 @@ Patterns are framed for markdown extraction (the dominant case) but apply to cod **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 +1. The `identify` action requires evidence of 3+ instances before suggesting an artifact-creating output (`rule-file` / `new-skill` / `new-action`) — Tier 0 grep output captured this turn, not recall +2. Refuse CREATION when only 1-2 instances exist (`verify` Gate 1, `REFUSE-rule-of-three-fails`); cite Rule of Three with author attribution (Don Roberts / Fowler) +3. Do NOT refuse to report it. The candidate stays on the roster in its N=1 or N=2 bucket with that bucket's non-abstracting remedies — `trim-to-citation`, `normalize-wording`, `name-an-owner`, `edit-existing-rule`. Every one of them edits files that already exist, so none can produce the premature abstraction this pattern is about. Suppressing the finding would not prevent the abstraction; it would only prevent the fix +4. 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 @@ -167,14 +168,27 @@ Patterns are framed for markdown extraction (the dominant case) but apply to cod **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. +**Intentional vs accidental — the distinction that decides the verdict.** The REFUSE trigger +protects the INTENTIONAL case: two tiers, two named audiences, a split someone chose. The +accidental case looks similar and is the opposite problem — two files assert the same contract for +the SAME audience and NEITHER is the declared owner, so nothing keeps them in sync and they drift. +That is a defect, not a design. + +The **N=2 bucket is how accidental bifurcation reaches the user.** Two instances never cleared the +old reporting threshold, so the most common form of this defect was silently discarded by the very +skill meant to catch it. `verify` Gate 4 now splits the two: intentional refuses as below; +accidental PROCEEDs at `bucket: N=2` with the remedy **`name-an-owner`** — declare one of the two +existing files canonical and make the other cite it. No third file is created; minting one would be +the premature abstraction Rule of Three guards against. + **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 +1. The `verify` action Gate 4 detects bifurcation and classifies it. Intentional (distinct named audiences) → refuses extraction with `REFUSE-source-of-truth-bifurcation`. Accidental (same audience, no declared owner) → PROCEED at `bucket: N=2` with `name-an-owner` / `edit-existing-rule` / `normalize-wording` +2. Document both canonicals + their respective audiences in the rule file if not already explicit — that documentation is what makes the split legible as intentional on the next pass 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. diff --git a/plugins/docs-hygiene/skills/extract-ssot/context/decision-framework.md b/plugins/docs-hygiene/skills/extract-ssot/context/decision-framework.md index 1753447bc..da2154b7a 100644 --- a/plugins/docs-hygiene/skills/extract-ssot/context/decision-framework.md +++ b/plugins/docs-hygiene/skills/extract-ssot/context/decision-framework.md @@ -2,6 +2,7 @@ ## Contents +- [Reporting gate vs abstraction gate](#reporting-gate-vs-abstraction-gate) - [EXTRACT into shared SSOT only when ALL six tests pass](#extract-into-shared-ssot-only-when-all-six-tests-pass) - [KEEP INLINE when ANY of these is true](#keep-inline-when-any-of-these-is-true) - [Pre-extraction Tier 0 checklist (lessons-derived discipline)](#pre-extraction-tier-0-checklist-lessons-derived-discipline) @@ -12,32 +13,73 @@ 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. +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: mint a new shared artifact when 3+ instances exist; below that, fix the duplication in place. + +## Reporting gate vs abstraction gate + +Two different questions were long collapsed into one threshold. They are separate: + +- **Reporting gate** — *should the user be told this duplication exists?* Always yes. Withholding a + finding does not prevent an abstraction; it prevents a fix. +- **Abstraction gate** — *may a NEW SSOT artifact be minted for it?* Rule of Three, unchanged, plus + the full 6-test gate below. + +Rule of Three gates the second question only. The evidence it rests on — ~19% failure on curated +skills, ~50% on practitioner-authored ones — is evidence about the cost of *creating* a shared +artifact too early. It is not evidence that a maintainer should be kept from seeing a drifting pair +of files. + +| Bucket | Rostered? | Permitted remedies | Creates a new artifact? | +|---|---|---|---| +| **N=1** — one consumer inlines a recap of an SSOT that already exists | always | `trim-to-citation`, `normalize-wording` | never | +| **N=2** — two consumers recap a canonical home that already exists (trim both to citations), or two files assert the same contract and neither is the declared owner (name one) | always | `trim-to-citation`, `edit-existing-rule`, `name-an-owner`, `normalize-wording` | never | +| **N≥3** — Rule of Three met | always | all of the above, plus `rule-file` / `new-skill` / `new-action` | only when ALL six tests below pass | + +**Lowering the reporting threshold does not lower the abstraction threshold.** The sub-three buckets +offer only non-abstracting remedies — every one of them edits files that already exist and adds no +new indirection hop, so none of them can produce the premature abstraction the Rule of Three exists +to prevent. The guardrail is intact; only the silence is gone. + +Two constraints keep the rule-of-one default honest: + +1. **N=1 requires an existing canonical home.** A lone paragraph nothing else duplicates is not a + duplication finding and is not rostered. The bucket is specifically "inline recap of an existing + SSOT" — the defect is the consumer restating what it should be citing. +2. **Sub-three candidates can never be routed to `rule-file` / `new-skill` / `new-action`.** + `verify` Gate 1 refuses that pairing with `REFUSE-rule-of-three-fails`. ## 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" | +| 1 | **Rule of Three** — duplication appears in 3+ places | Premature abstraction creates the wrong-abstraction trap; 1-2 instances are usually coincidence, not pattern — and when they are not, the N=1 / N=2 buckets remedy them in place instead of minting an artifact. 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 | High churn drives heading/identifier rename frequency, which compounds the citation-rot risk captured in test #6 below; and every edit to an always-loaded file reaches sessions already running only at the next `/clear`, `/compact`, or restart, so a volatile SSOT ships corrections its live consumers do not see (anti-pattern #9). Not a caching cost — a mid-session edit to an always-loaded file keeps the cached prefix. Code-side equivalent: high churn means callers chase signature changes constantly | Claude Code prompt caching, [editing CLAUDE.md mid-session](https://code.claude.com/docs/en/prompt-caching#editing-claude-md-mid-session) (verified 2026-08-04); 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). +ALL six must pass — and they gate ONE thing: creating a new SSOT artifact (`rule-file` / +`new-skill` / `new-action`). They do not gate reporting, and they do not gate the non-abstracting +remedies (`trim-to-citation`, `normalize-wording`, `name-an-owner`, `edit-existing-rule`), which +edit existing files and introduce no new indirection. + +Failing one means: keep the content where it is, OR refactor the candidate before extracting (e.g. split categorical bits from reasoning bits, then extract only the categorical bits). Either way the candidate stays on the roster in its bucket with the remedies that bucket allows. ## KEEP INLINE when ANY of these is true +"Keep inline" here means "do not lift this into a new artifact" — not "say nothing". A candidate +kept inline is still rostered in its bucket and still gets that bucket's non-abstracting remedies. + | # | Test | Why | |---|------|-----| -| A | Single use site OR < 3 instances of duplication | Premature abstraction; wait for the third instance | +| A | Single use site OR < 3 instances of duplication | No new artifact — premature abstraction; wait for the third instance. The content stays where it is, and the candidate is rostered as N=1 or N=2 with `trim-to-citation` / `normalize-wording` / `name-an-owner` / `edit-existing-rule` on the table | | 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); and corrections ship that live consumers do not see until their next `/clear`, `/compact`, or restart (anti-pattern #9) | | 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. +Any ONE failure here = no new artifact. Don't extract just because you can — and don't go quiet just because you didn't. ## Pre-extraction Tier 0 checklist (lessons-derived discipline) @@ -47,14 +89,14 @@ Run via `/docs-hygiene:extract-ssot verify ` for a one-shot result, or | # | 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 | +| A | **Discriminating-phrase grep, not keyword density** | Lesson 1 | Gate 1 | Keyword count > 3 but full-reproduction count < 3 → bucket is N=1 or N=2; an artifact-creating output there → REFUSE-rule-of-three-fails (the non-abstracting remedies still stand) | | 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 | +| 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, serving two named audiences → intentional, REFUSE-source-of-truth-bifurcation. Same contract, same audience, no declared owner → accidental, the N=2 bucket's own defect: `name-an-owner` | | 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 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. Check A is the exception in shape rather than in force: it refuses the artifact, not the finding — the candidate keeps its bucket and its non-abstracting remedies. If ALL checks pass → proceed to the `architect-plan` phase. @@ -70,17 +112,21 @@ The `unwind` action implements this. Trigger signal: the SSOT has 5+ callers pas ## 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 `/docs-hygiene:rename-references` sweep). +Choose the SSOT shape from the candidate's bucket first, then its content shape. **Check whether an existing file already owns the concept** (top rows) — if so, consolidate into it rather than creating anything. The first six 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 `/docs-hygiene:rename-references` sweep). + +The **Artifact?** column is the abstraction gate made visible: only the rows marked YES are reachable at N≥3, and only after all six tests pass. -| 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 `/docs-hygiene: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 | +| Shape | Artifact? | Target | Trigger signals | +|-------|-----------|--------|-----------------| +| **Consolidate into existing SSOT home** (markdown) | no new artifact | 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 `/docs-hygiene:rename-references` sweep is a no-op unless a heading changes. Available in every bucket | +| **`normalize-wording`** (markdown) | no new artifact | Every instance, edited in place onto the canonical or agreed wording | The instances say the same thing in drifted phrasings and the drift itself is the defect — a reader cannot tell whether the difference is meaningful. Available in every bucket, including N=1 | +| **`name-an-owner`** (markdown) | no new artifact | One of the two existing files, declared canonical; the other rewritten to cite it | Accidental source-of-truth bifurcation: two files assert the same contract for the same audience and neither is declared the owner (anti-pattern #11, accidental branch). The N=2 bucket's default remedy. Minting a third file to own the contract is NOT this remedy | +| **Rule file** (markdown) | **YES — N≥3 only** | 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) | **YES — N≥3 only** | `.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** | **YES — N≥3 only** | 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** | advisory — out of scope | 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** | advisory — out of scope | 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** | advisory — out of scope | 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. @@ -150,11 +196,11 @@ Each example is generic — pattern-shaped, not tied to one specific extraction. 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) +### Example 5: two instances → N=2 bucket, no new artifact (FAILS the abstraction gate, PASSES reporting) -**Cluster:** Two files have similar (not identical) snippets. A third instance might appear later, or might not. +**Cluster:** Two files assert the same contract in similar (not identical) wording. Neither is declared the owner. A third instance might appear later, or might not. -**6-test gate:** +**6-test gate (gates artifact creation only):** 1. ❌ Rule of Three — only 2 instances 2. ✅ Namable @@ -163,11 +209,26 @@ Skill-vs-rule heuristic restated: if the SSOT body is mostly nouns (named units 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. +**Output:** the candidate is **rostered in the N=2 bucket** with its instance count — it is not discarded. Creating a `rule-file` / `new-skill` / `new-action` for it is REFUSED (`REFUSE-rule-of-three-fails`); cite Rule of Three. What IS offered: + +- `name-an-owner` — declare the better-placed of the two files canonical, rewrite the other to cite it. This resolves the drift without adding a third file. +- `edit-existing-rule` — if one of the two is already the natural home, extend it where the other carries nuance it lacks. +- `normalize-wording` — if the two have already drifted, align them before (or instead of) naming an owner. + +Also record a tracking note in the working notes so future-self knows to revisit if a third instance lands. Do NOT silently proceed to an artifact — premature abstraction is a wrong-abstraction trap that's expensive to reverse — and do NOT silently drop the finding: two undeclared canonicals asserting one contract is exactly the bifurcation that drifts (anti-pattern #11, accidental branch). + +### Example 6: one instance → N=1 bucket, trim to a citation (no gate to run) + +**Cluster:** A canonical rule file already owns a constraint. One skill body restates it in a paragraph of its own instead of citing it, and the restatement has drifted a word or two from the home's wording. + +**Abstraction gate:** not run. Nothing would be created — the SSOT already exists. + +**Output:** rostered in the **N=1 bucket** (the SSOT-existence check is what admits it; a lone paragraph with no canonical home would not be). Remedies: `trim-to-citation` — replace the recap with `per .md ""` plus a ≤80-char inline summary — and `normalize-wording` if the drifted phrasing must survive anywhere. If the site ALREADY cites the home correctly, there is no work: `verify` Gate 2 returns `REFUSE-already-cites-canonical`. ## Cross-references - SKILL.md "Output type" — the canonical markdown summary of the output-type table +- SKILL.md "Decision framework" — the headline reporting-gate / abstraction-gate split and the bucket 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/evals/evals.json b/plugins/docs-hygiene/skills/extract-ssot/evals/evals.json index ce68e7ec1..2af65c2a7 100644 --- a/plugins/docs-hygiene/skills/extract-ssot/evals/evals.json +++ b/plugins/docs-hygiene/skills/extract-ssot/evals/evals.json @@ -5,24 +5,26 @@ "id": 1, "name": "identify-on-duplication-smell", "prompt": "The same 'always run the build before opening a PR' guardrail is reworded across five rule and skill files. I want a single source of truth for it.", - "expected_output": "Routes to the identify action (or a targeted single-cluster identify), grounding the cluster in Tier 0 grep evidence, and — because 5 instances clear the Rule of Three — proceeds toward planning an extraction into a single named home cited by exact heading.", + "expected_output": "Routes to the identify action (or a targeted single-cluster identify), grounding the cluster in Tier 0 grep evidence, and — because 5 instances land the cluster in the N>=3 bucket where the Rule of Three is met — proceeds toward planning an extraction into a single named home cited by exact heading.", "files": [], "expectations": [ "Output routes to the extract-ssot workflow (identify / verify / plan) rather than editing files ad hoc", "Output confirms the instance count via direct (Tier 0) grep evidence, not recall", - "Output treats 5 instances as clearing the Rule of Three" + "Output places the cluster in the N>=3 bucket and treats 5 instances as clearing the Rule of Three", + "Output only puts an artifact-creating output (rule-file / new-skill / new-action) on the table because the bucket is N>=3, and still runs it through the 6-test gate" ] }, { "id": 2, - "name": "refuse-below-rule-of-three", + "name": "two-instances-bucketed-no-new-artifact", "prompt": "This constraint appears in exactly two files. Extract it into a shared SSOT rule so it's DRY.", - "expected_output": "Refuses extraction: with only 2 instances the Rule of Three is not met, and premature abstraction is the dominant failure mode. It recommends leaving the content inline until a third instance appears.", + "expected_output": "Rosters the cluster in the N=2 bucket (source-of-truth bifurcation risk) rather than discarding it, and refuses to create any new artifact for it: with only 2 instances the Rule of Three is not met and premature abstraction is the dominant failure mode. It offers the non-abstracting remedies instead — name-an-owner (declare one of the two existing files canonical and have the other cite it), edit-existing-rule, normalize-wording — and notes the candidate can be revisited if a third instance lands.", "files": [], "expectations": [ - "Output REFUSES the extraction at 2 instances (below the Rule of Three)", - "Output recommends keeping the content inline for now", - "Output frames premature extraction as the risk, not a benefit" + "Output rosters the candidate in the N=2 bucket with its instance count rather than dropping it as below the Rule of Three", + "Output REFUSES to create a new SSOT artifact (no new rule file, skill, or action) at 2 instances, citing the Rule of Three", + "Output offers only non-abstracting remedies — name-an-owner / edit-existing-rule / normalize-wording — that edit the two existing files", + "Output frames premature extraction as the risk, not a benefit, while still surfacing the duplication to the user" ] }, { @@ -84,6 +86,32 @@ "Output presents prescribed defaults with a recommended option first, including a path/glob-scoped exhaustive survey choice distinct from targeted cluster grep", "Output references whole-repo / orchestrated-mode defaults when offering the repo-wide option" ] + }, + { + "id": 8, + "name": "single-inline-recap-of-existing-ssot", + "prompt": "Our .claude/rules/testing.md already owns the 'run the full suite before merge' constraint, but one skill body restates it in its own paragraph instead of citing it. Only that one file does this.", + "expected_output": "Rosters the single site in the N=1 bucket — it qualifies because a canonical home already exists that the site recaps instead of cites — and recommends trim-to-citation: replace the recap with a citation by exact heading plus a short inline summary, with normalize-wording if the drifted phrasing must survive. No new artifact is created; the SSOT already exists.", + "files": [], + "expectations": [ + "Output reports the single-instance recap rather than dismissing it as below the Rule of Three", + "Output places it in the N=1 bucket and justifies admission by the existing canonical home it should be citing", + "Output recommends trim-to-citation (and/or normalize-wording) and creates no new rule file, skill, or action", + "Output does not propose a rule-file / new-skill / new-action output for a single instance" + ] + }, + { + "id": 9, + "name": "min-instances-3-regression-guard", + "prompt": "Run /docs-hygiene:extract-ssot identify --min-instances=3 over this repo. Some clusters have one or two instances.", + "expected_output": "Reproduces the pre-bucket behavior exactly: --min-instances=3 sets the lowest bucket to roster at three, so the N=1 and N=2 buckets are not rostered at all and only N>=3 clusters reach the roster. The suppressed buckets are named as suppressed by the flag rather than silently omitted, so an empty section does not read as 'nothing found'. Read-only — no flags beyond --min-instances means no edits are applied.", + "files": [], + "expectations": [ + "Output rosters only clusters with 3 or more instances under --min-instances=3", + "Output does NOT roster the one- and two-instance candidates, matching the legacy pre-bucket behavior", + "Output distinguishes 'suppressed by --min-instances' from 'no candidates found' for the omitted buckets", + "Output applies no edits (bare identify with only --min-instances stays read-only)" + ] } ] } diff --git a/plugins/docs-hygiene/skills/write-for-agents/SKILL.md b/plugins/docs-hygiene/skills/write-for-agents/SKILL.md index 09034384b..388e4c1fb 100644 --- a/plugins/docs-hygiene/skills/write-for-agents/SKILL.md +++ b/plugins/docs-hygiene/skills/write-for-agents/SKILL.md @@ -96,7 +96,9 @@ the positive alternative in the same sentence. ## After writing -- Repeated the same prose a third time? Invoke `/docs-hygiene:extract-ssot` via the Skill tool. +- Repeated the same prose in another file — even a second occurrence, or a recap of an SSOT that + already exists? Invoke `/docs-hygiene:extract-ssot` via the Skill tool. Creating a new shared home + still waits for the third occurrence; below that it remedies the repetition in place. - Resolved or coined a domain term? Invoke `/domain-driven-design:curate-language` via the Skill tool (if that plugin is installed) — never hand-write a glossary entry. - Editing exposed pre-existing problems in the surrounding doc? Invoke the fitting audit diff --git a/plugins/docs-hygiene/skills/write-for-humans/SKILL.md b/plugins/docs-hygiene/skills/write-for-humans/SKILL.md index 0c6962130..d5de71257 100644 --- a/plugins/docs-hygiene/skills/write-for-humans/SKILL.md +++ b/plugins/docs-hygiene/skills/write-for-humans/SKILL.md @@ -145,7 +145,9 @@ ambiguity). "If exceeded" gets a subject: the request (ambiguity). - **Check for AI-writing tells.** Invoke `/ai-slop:audit` via the Skill tool when it is available in the session; when it is not, re-read for the obvious tells yourself — filler, stacked hedging, negative parallelism, promotional tone — and say that you did the lighter pass. -- **Repeated the same prose a third time?** Invoke `/docs-hygiene:extract-ssot` via the Skill tool. +- **Repeated the same prose in another file — even a second occurrence, or a recap of an SSOT that + already exists?** Invoke `/docs-hygiene:extract-ssot` via the Skill tool. Creating a new shared + home still waits for the third occurrence; below that it remedies the repetition in place. - **The draft is over-long rather than misshapen?** Invoke `/docs-hygiene:compress` via the Skill tool; it trims flavor behind a semantic-diff guard rather than rewriting. - **Writing markdown an agent will load instead?** That is `/docs-hygiene:write-for-agents`.