diff --git a/docs/SKILL-CHEAT-SHEET.md b/docs/SKILL-CHEAT-SHEET.md index a2faefab84..87cec976c2 100644 --- a/docs/SKILL-CHEAT-SHEET.md +++ b/docs/SKILL-CHEAT-SHEET.md @@ -180,7 +180,7 @@ owned by [docs/CATALOG-TAXONOMY.md](CATALOG-TAXONOMY.md). | [`/disk-hygiene:clean`](../plugins/disk-hygiene/skills/clean/SKILL.md) | `disk-hygiene` | Audit a directory tree for stale leftovers and remove validated paths | | [`/docs-hygiene:audit-derivability`](../plugins/docs-hygiene/skills/audit-derivability/SKILL.md) | `docs-hygiene` | Judge whether a doc earns its existence or should become a pointer | | [`/docs-hygiene:audit-encapsulation`](../plugins/docs-hygiene/skills/audit-encapsulation/SKILL.md) | `docs-hygiene` | Find external citations reaching into a skill's private surfaces | -| [`/docs-hygiene:audit-noise`](../plugins/docs-hygiene/skills/audit-noise/SKILL.md) | `docs-hygiene` | Classify markdown for stale citations, ghost refs, and meta-commentary | +| [`/docs-hygiene:audit-noise`](../plugins/docs-hygiene/skills/audit-noise/SKILL.md) | `docs-hygiene` | Classify markdown for citations, ghost refs, meta-commentary, plan/conversational/tracker residue | | [`/docs-hygiene:audit-progressive-disclosure`](../plugins/docs-hygiene/skills/audit-progressive-disclosure/SKILL.md) | `docs-hygiene` | Grade instruction files for split opportunities and hub/spoke disclosure defects | | [`/docs-hygiene:compress`](../plugins/docs-hygiene/skills/compress/SKILL.md) | `docs-hygiene` | Tighten markdown by dropping flavor while preserving every directive | | [`/docs-hygiene:extract-ssot`](../plugins/docs-hygiene/skills/extract-ssot/SKILL.md) | `docs-hygiene` | Deduplicate repeated prose into one named source of truth | diff --git a/plugins/docs-hygiene/.claude-plugin/plugin.json b/plugins/docs-hygiene/.claude-plugin/plugin.json index 87c23b3eb1..314f0fb6f3 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.19.2", + "version": "0.20.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 — 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 1eb270b25b..6201984834 100644 --- a/plugins/docs-hygiene/CHANGELOG.md +++ b/plugins/docs-hygiene/CHANGELOG.md @@ -1,5 +1,72 @@ # Changelog — docs-hygiene plugin +## [0.20.0] + +### Added + +- **`audit-noise` gains the three residue shapes markdown had no owner for (#3125).** The code-side + sibling `/code-tidying:audit-comment-residue` detects four residue shapes; this skill detected + five noise shapes; the two sets did not tile the space. Only `history-narration` had a markdown + counterpart (this skill's `citation`). `plan-reference`, `conversational-antecedent`, and + `ticket-pr-residue` had **no detector on either side of the boundary**, so a README, rule body, or + `CLAUDE.md` saying "as you asked, retry three times" or "see PR #45 for the rationale" was + invisible to the whole fleet — not because a file type was skipped, but because of a gap behind an + otherwise correct boundary. `audit-noise` is now an eight-shape classifier: `plan-reference` and + `conversational-antecedent` at Tier 1, `ticket-pr-residue` at Tier 2. + + **The shapes went to `audit-noise` rather than widening the code skill to `.md`**, and the reason + is a treatment conflict, not a preference. On a markdown line the code skill's `history-narration` + and this skill's `citation` fire together with opposite rulings — `citation` says relocate to a + `## Sources` footer, `history-narration` says delete. Two owners for one line is a precedence + problem; one owner per file type is not. So the boundary is now explicitly by FILE TYPE, with the + three shape *names* deliberately shared so one authoring failure keeps one name wherever it lands. + The shapes also inherit this skill's more mature target router (`--paths-file`, offset/limit + pagination, space-safe porcelain parsing) for free. + + **The patterns are adapted, not copied, and the adaptation is the substance of the change.** The + code lib classifies only the extracted *comment* portion of a line; this one classifies whole + prose, where the same words are load-bearing far more often. Measured against this repository's + own 1136-file tracked-markdown corpus, four of the sibling's cues had to go: `per the plan` + prefix-matches "per the planning chapter" (and a doc citing a plan artifact that still exists is a + live cross-reference, not residue); `as planned` is a substring of "was planned", so "what was + planned, what was done instead" self-matched; `in this change` and `in this session` are ordinary + domain vocabulary in an agent-tooling corpus. `in this PR` survives only with a first-person actor + behind it, which is what separates narration ("in this PR we switch the default") from a live + referent ("the files changed in this PR"). `as we discussed` stands down in front of an anaphoric + follower (`above`, `below`, `in §3`), which makes it an intra-document cross-reference. Those + tightenings cut the corpus delta from 32 findings to 12. + + **`conversational-antecedent`'s follower test asks what the reference points AT**, rather than + which preposition introduces it. A bare `in` exemption would have spared "as we decided in the + ADR" (right) and "as we decided in favor of X" or "as we discussed in yesterday's meeting" + (wrong — the referent there is the conversation, not a document), so `in` stands the shape down + only ahead of a document locator: a `§` or `#anchor`, a section/chapter/step/table, a link or + path, an inline-code reference the strip removed, or a named durable document. Tracker nouns are + deliberately absent from that set — a decision parked in an issue is provenance, which + `ticket-pr-residue` owns and this shape must not launder — as are nouns for the conversation + itself. Followers are compared case-insensitively, so a capitalised `Above` no longer falls + through. Both first-person actor tests — this one and `plan-reference`'s `in this PR` — admit a + contracted pronoun in either the straight or the typographic (U+2019) apostrophe, so + "in this PR we've already switched the default" and "as we've discussed" no longer escape the + shape they are; `conversational-antecedent` admits only `'ve` and `'d`, the two auxiliaries its + past-participle follower can take, which keeps the present-tense passive "as you're asked" out. + + **The actor-less passive `As requested, …` is the same shape without the pronoun**, and it is + matched only as a clause-final adverbial. That bound is what keeps the live attribution "as + requested by the client" and the ordinary verb phrase "was requested" out, without a second + pattern to maintain; a closing quote does not count as the clause break, because behind one the + words are a quoted voice rather than the page's own address to its reader. + + **`ticket-pr-residue`'s carve-out is restated in markdown terms** rather than inherited: a task-list + checklist item (`- [ ] … #123`) and a `TODO(#123)`-family marker are never flagged, because both + denote OUTSTANDING tracked work — the reference is the actionable part of the line — which is what + the code skill's sanctioned-`TODO` exception is actually about. Nothing further is carved out: the + sanctioned home for a *provenance* citation is a `## Sources` / `## History` footer, and the + existing section exemptions already skip those (as they skip `CHANGELOG.md`, fenced blocks, and + frontmatter) before any shape runs, so re-implementing that as a pattern would duplicate a rule + that already holds. An inline parenthetical (`… (tracked in #482)`) stays Tier 2 on purpose, so a + reviewer rules on it rather than the scanner. + ## [0.19.2] ### Changed diff --git a/plugins/docs-hygiene/README.md b/plugins/docs-hygiene/README.md index 8b48d2a149..bf6dd7ccd2 100644 --- a/plugins/docs-hygiene/README.md +++ b/plugins/docs-hygiene/README.md @@ -11,7 +11,7 @@ and authoring axes of doc upkeep. | Skill | What it does | |---|---| | `/docs-hygiene:compress` | Tightens markdown by dropping flavor (filler, hedging, articles) while preserving all content, behind a mandatory fresh-context semantic-diff audit that reverts any semantic loss. Supports an optional `caveman` plugin backend (`/caveman:compress`) with a built-in in-session fallback. | -| `/docs-hygiene: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:audit-noise` | Read-only classifier for eight markdown noise shapes (historical citations, ghost refs to ephemeral working directories, "why this file exists" preambles, hard-coupled consumer lists, scope/loading meta-commentary, plan/changeset references, conversational antecedents, tracker/PR back-references) with tiered findings and per-shape treatment guidance. | | `/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. | diff --git a/plugins/docs-hygiene/skills/audit-noise/SKILL.md b/plugins/docs-hygiene/skills/audit-noise/SKILL.md index fa773d7456..9204aac786 100644 --- a/plugins/docs-hygiene/skills/audit-noise/SKILL.md +++ b/plugins/docs-hygiene/skills/audit-noise/SKILL.md @@ -1,5 +1,5 @@ --- -description: "Classify tracked markdown for five noise shapes — historical citations, ghost refs to ephemeral working-directory paths, \"Why this file exists\" preambles, hard-coupled enumerated consumer lists, and scope/loading meta-commentary — emitting Tier 1 (remove/relocate), Tier 2 (review needed), and Tier 3 (likely legitimate) findings with per-shape treatment guidance; read-only, no edits applied. Use when: 'audit markdown noise', 'declutter', 'check for stale citations', 'find ghost refs', 'classify preamble', 'sweep a rule/skill/convention doc for noise', or before editing any tracked .md — not for prose flavor/compression (use /compress) or structural markdown lint (your repo's markdown linter)." +description: "Classify tracked markdown for eight noise shapes — historical citations, ghost refs to ephemeral working-directory paths, \"Why this file exists\" preambles, hard-coupled enumerated consumer lists, scope/loading meta-commentary, plan/changeset references, conversational antecedents (\"as you asked\", \"per our discussion\"), and tracker/PR/branch back-references — emitting Tier 1 (remove/relocate), Tier 2 (review needed), and Tier 3 (likely legitimate) findings with per-shape treatment guidance; read-only, no edits applied. Use when: 'audit markdown noise', 'declutter', 'check for stale citations', 'find ghost refs', 'classify preamble', 'strip conversational residue from a doc', 'sweep a rule/skill/convention doc for noise', or before editing any tracked .md — not for prose flavor/compression (use /compress), structural markdown lint (your repo's markdown linter), or the same residue shapes inside code comments (use /code-tidying:audit-comment-residue, which owns non-markdown files)." argument-hint: "[audit] [target]" user-invocable: true disable-model-invocation: false @@ -7,7 +7,7 @@ allowed-tools: ["Bash(${CLAUDE_SKILL_DIR}/scripts/detect.sh:*)", "Bash(grep:*)", shell: bash metadata: workflow-stage: anytime - summary: Classify markdown for stale citations, ghost refs, and meta-commentary + summary: Classify markdown for citations, ghost refs, meta-commentary, plan/conversational/tracker residue --- ## Pre-computed context @@ -18,7 +18,9 @@ Noise findings (sample): !`${CLAUDE_SKILL_DIR}/scripts/detect.sh 2>/dev/null | g ## Purpose -Tracked markdown — rules, skill bodies, instruction files (`CLAUDE.md`, `AGENTS.md`), `docs/`, READMEs — accumulates five NOISE shapes distinct from FLAVOR (owned by the sibling `/docs-hygiene:compress`). Each shape carries a maintenance tax plus a reader-facing tax that compounds across the corpus. This skill is a read-only classifier: it surfaces candidates with treatment guidance; the author hand-applies every edit. +Tracked markdown — rules, skill bodies, instruction files (`CLAUDE.md`, `AGENTS.md`), `docs/`, READMEs — accumulates eight NOISE shapes distinct from FLAVOR (owned by the sibling `/docs-hygiene:compress`). Each shape carries a maintenance tax plus a reader-facing tax that compounds across the corpus. This skill is a read-only classifier: it surfaces candidates with treatment guidance; the author hand-applies every edit. + +Three of the eight — `plan-reference`, `conversational-antecedent`, `ticket-pr-residue` — carry the same names the code-side sibling `/code-tidying:audit-comment-residue` uses, because they are the same authoring failure landing in a different file type. Ownership splits by file type, not by shape: markdown is this skill's, everything else is the sibling's, and neither scans the other's files. The patterns are **not** shared code. The sibling classifies only the extracted comment portion of a line; this skill classifies whole prose, where the same words are load-bearing far more often, so its patterns are tightened accordingly and several of the sibling's cues are deliberately not carried over. ## Existence pre-check (before in-page noise) @@ -42,7 +44,7 @@ resolve and defer to it via `/discipline:follow-our-standards`'s resolution ladder (repo-declared source → repo's own conventions → this portable baseline) instead of the default above. -Only a page that passes admission proceeds to the five in-page NOISE shapes below. +Only a page that passes admission proceeds to the eight in-page NOISE shapes below. ## Noise shapes and treatments @@ -53,6 +55,9 @@ Only a page that passes admission proceeds to the five in-page NOISE shapes belo | `preamble` — "Why this file exists" openers | Opening section explaining motivation/history/rationale | 2 | Diataxis classify: KEEP on Explanation-quadrant files (rule bodies, ADRs, convention rationale); STRIP on Reference-quadrant files (data tables, registries, cheat-sheets), replacing with a 1-sentence orientation | | `enum-list` — hard-coupled consumer lists | Tables/lists hardcoding N specific consumers that drift on every add/remove ("the following five skills…", bulleted `/skill — role` rosters) | 1 | Replace with a runtime derivation (a grep/list command cited inline) or a category citation; hardcode only when both fail | | `scope-meta` — scope/loading meta-commentary | Body prose restating loading mechanics that config/frontmatter already owns ("Path-scoped to X", "Loads on Read of Y", "Auto-loads when…") | 1 | Strip the clause — the frontmatter/config is the single source of truth; keep a genuine cross-ref riding the same sentence. Files with no scoping frontmatter MAY state scope in one sentence | +| `plan-reference` — plan/changeset narration | Prose pointing at the work that produced the page instead of the page's subject: `replaces the old …`, `in this PR we …`, `Task 2 of the plan` | 1 | Delete the plan/changeset frame and keep whatever the sentence asserts about the present subject, rewritten without it. A doc citing a plan artifact that still exists is a live cross-reference, not this shape — matching requires a first-person actor behind `in this PR`, so `the files changed in this PR` is not flagged | +| `conversational-antecedent` — asides to the requester | Prose addressed to the person who asked for the page or to the conversation that produced it: `As you asked, …`, `As requested, …`, `Per our discussion, …`, `per your request`, `like you said` | 1 | Delete the address — the conversation is invisible to every future reader, and the assertion behind it survives verbatim once the clause is cut. Two followers stand the shape down, because both name something a future reader can still open: an anaphoric adverb (`as we discussed above`), and `in` ahead of a **document locator** — a `§` or `#anchor`, a section/chapter/step/table, a link or path, or a named durable document (`as we decided in §3`, `in the ADR`). `in` ahead of anything else is matched, so `as we discussed in yesterday's meeting` and `as we decided in favor of X` are residue; tracker nouns are deliberately not locators, since `decided in issue 88` is provenance that `ticket-pr-residue` owns. The actor-less `as requested` matches only as a clause-final adverbial, so the attribution `as requested by the client` is not matched | +| `ticket-pr-residue` — tracker/PR back-references | Bare provenance offered as the reason the prose says what it says: `See PR #45 for the rationale`, `Tracked in JIRA-123`, `decided in issue 88`, `from the feature branch` | 2 | Review — delete a bare provenance reference, or relocate it to the `## Sources` / `## History` footer (already an exempt section, so a relocated reference stops flagging). **Carve-out:** a markdown task-list item (`- [ ] … #123`, `- [x] … #123`) and a `TODO(#123)`-family marker are never flagged — both denote OUTSTANDING tracked work, where the reference is the actionable part of the line, which is the markdown restatement of the sibling's sanctioned-`TODO` exception. Nothing else is carved out: an inline parenthetical (`… (tracked in #482)`) stays Tier 2 so a reviewer rules on it rather than the scanner | Consumers with their own ephemeral-path or noise conventions can refine these defaults in their repo's `CLAUDE.md` / rules; the classifier's shapes and tiers above are the skill's built-in baseline. @@ -91,7 +96,7 @@ Shared clean-tree / no-scope shape: [`../../context/clean-tree-fallback.md`](../ - **Read-only.** No `Edit`, no `Write`, no mutating `Bash` ops. The author owns every treatment edit. - **Tier semantics.** Tier 1 = definite noise; Tier 2 = review needed; Tier 3 = likely legitimate (surfaced for awareness). Tier 3 carries NO treatment — a finding whose ruling includes an edit ("strip", "relocate", "replace") is Tier 2 or 1 by definition. -- **Section EXEMPTIONS never flagged:** `## Recheck triggers`, `## Cross-references`, `## Sources` / `## History` / `## External authority` footers (any ATX heading level — `### Sources` counts; a later non-exempt heading of any level ends the exemption), ADR amendment blocks, `CHANGELOG.md` entries and release notes (detect.sh skips `CHANGELOG.md` by basename), YAML frontmatter (`---` … `---`), and fenced code blocks. Inline `` `code` `` spans are stripped before citation/enum/scope matching (ghost-ref still sees unwrapped path text). +- **Section EXEMPTIONS never flagged:** `## Recheck triggers`, `## Cross-references`, `## Sources` / `## History` / `## External authority` footers (any ATX heading level — `### Sources` counts; a later non-exempt heading of any level ends the exemption), ADR amendment blocks, `CHANGELOG.md` entries and release notes (detect.sh skips `CHANGELOG.md` by basename), YAML frontmatter (`---` … `---`), and fenced code blocks. Inline `` `code` `` spans are stripped before every shape match EXCEPT ghost-ref, which still sees unwrapped path text — so a shape-definition or worked example written in backticks does not self-match, and an example written in plain quotes does. - **Dismissal grounds the judgment pass may use** (recurring, sanctioned; the scanner cannot see them): a fictional slug instantiated by a worked example (nothing can dangle), a vendored-verbatim upstream baseline that is never hand-edited by policy, a delete/prune instruction whose target is the path being removed (a record, not a followable reference), and a shape-definition or output-schema example matching its own pattern. - **Opt-out markers respected.** A well-formed HTML comment line `` (covers the next paragraph, through the next blank line or heading) and `` (exactly the next physical line — a blank line consumes it, so place the marker directly above the content line) skip the wrapped content. A prose mention of the marker name is not a live marker. - **Convention-path exemptions apply per matched path, never per line.** An angle-bracket slot variable (`.work//…`, `docs/topics//…`) is a schema placeholder, not a literal path; the reserved concern-scoped roots (`.work/handoffs/`, `.work/reviews/`, `.work/running-retros/`, `.work/overengineering/` — roster SSOT: topic-docs Memory, concern-scoped tier) are citable only bare or with a placeholder child — a concrete child under them flags. A convention token on a line never exempts a concrete ghost ref sharing that line; the tracked concern file (`.claude/topic-docs.yaml`) matches no ghost-ref pattern and needs no exemption. Exception: the retired `.claude/notes/` location flags even in placeholder form. @@ -118,6 +123,8 @@ A FAIL skips the in-page tier table below; a PASS proceeds to it: | 2 | ghost-ref | 87 | ".work/foo-slice/PLAN.md cites..." | 3-way classify (promote / SHA-permalink / strip) | | 2 | preamble | 7 | "## Why this file exists" | Diataxis classify (KEEP if Explanation; STRIP if Reference) | | 3 | preamble | 1 | (top-of-file orientation paragraph) | Likely legitimate; surfaced for awareness | +| 1 | conversational-antecedent | 9 | "As you asked, this section..." | Delete the address to the requester | +| 2 | ticket-pr-residue | 55 | "See PR #45 for the rationale" | Review (delete bare provenance / relocate to ## Sources) | ``` Batch aggregate at end: @@ -126,11 +133,12 @@ Batch aggregate at end: Total: file(s) audited, Tier 1, Tier 2, Tier 3 findings. ``` -`shape` values: `citation`, `ghost-ref`, `preamble`, `enum-list`, `scope-meta`. +`shape` values: `citation`, `ghost-ref`, `preamble`, `enum-list`, `scope-meta`, `plan-reference`, `conversational-antecedent`, `ticket-pr-residue`. ## What this skill is NOT -- **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 `/docs-hygiene:compress`.** The sibling `/docs-hygiene:compress` owns FLAVOR (filler, hedging, articles, redundant restatement); `/docs-hygiene:audit-noise` owns NOISE (the eight shapes above). Different concerns; both may apply to the same target iteratively. +- **Not `/code-tidying:audit-comment-residue`.** The boundary is the FILE TYPE, not the shape vocabulary: three shape names (`plan-reference`, `conversational-antecedent`, `ticket-pr-residue`) are deliberately shared, so the same authoring failure gets the same name whichever file it lands in, and each file type keeps exactly one owner — markdown here, everything else there, no dedup or precedence rule needed. That split is also why the code skill is not simply widened to `.md`: on markdown its `history-narration` would fire on the same lines as this skill's `citation` with the opposite treatment (delete vs. relocate to a `## Sources` footer), and a conflict between two treatments is resolved by ownership, not by scope. The two detectors do not share pattern code, and this skill's are tighter — see Purpose. - **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 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+. diff --git a/plugins/docs-hygiene/skills/audit-noise/evals/evals.json b/plugins/docs-hygiene/skills/audit-noise/evals/evals.json index e2ca86ae9a..fba4390a5a 100644 --- a/plugins/docs-hygiene/skills/audit-noise/evals/evals.json +++ b/plugins/docs-hygiene/skills/audit-noise/evals/evals.json @@ -5,12 +5,15 @@ "id": 1, "name": "classify-noise-shapes-with-tiers", "prompt": "Declutter this file: read evals/fixtures/noisy-rule-snippet.md relative to the skill directory and audit it for noise.", - "expected_output": "Emits a per-file tier table classifying the noise: the dated 'Empirically observed 2026-03-14' line as a citation, the `.work/net-hardening-slice/PLAN.md` reference as a ghost-ref (Tier 2), the 'Path-scoped to ... loads on Read' clause as scope-meta, and the hardcoded 'following three skills' roster as an enum-list. Each finding carries treatment guidance. No edits are applied.", + "expected_output": "Emits a per-file tier table classifying the noise: the dated 'Empirically observed 2026-03-14' line as a citation, the `.work/net-hardening-slice/PLAN.md` reference as a ghost-ref (Tier 2), the 'Path-scoped to ... loads on Read' clause as scope-meta, the hardcoded 'following three skills' roster as an enum-list, the 'As you asked' opener as a conversational-antecedent (Tier 1), the 'Task 2 replaces the old ... in this PR we' sentence as a plan-reference (Tier 1), and the 'See PR #45 for the ordering rationale' line as a ticket-pr-residue (Tier 2). Each finding carries treatment guidance. No edits are applied.", "files": ["evals/fixtures/noisy-rule-snippet.md"], "expectations": [ - "Output classifies findings by the skill's shape vocabulary (citation, ghost-ref, preamble, enum-list, scope-meta)", + "Output classifies findings by the skill's shape vocabulary (citation, ghost-ref, preamble, enum-list, scope-meta, plan-reference, conversational-antecedent, ticket-pr-residue)", "The `.work/net-hardening-slice/PLAN.md` reference is flagged as a ghost-ref", "The hardcoded 'following three skills' list is flagged as an enum-list with a runtime-derivation / category-citation treatment", + "The 'As you asked' opener is flagged as a conversational-antecedent with a delete treatment", + "The 'Task 2 replaces the old ... in this PR we' sentence is flagged as a plan-reference with a delete-the-frame treatment", + "The 'See PR #45' line is flagged as a ticket-pr-residue at Tier 2 (review — delete the bare provenance or relocate it to a `## Sources` footer)", "The output is a classification report with tiers and treatments; no file is edited" ] }, @@ -54,10 +57,10 @@ "id": 5, "name": "flavor-is-compress-not-audit-noise", "prompt": "Declutter docs/wordy.md — it's full of filler like 'basically', 'really', and 'just', plus a lot of hedging. Flag all of that.", - "expected_output": "Does NOT classify filler / hedging / articles as any of audit-noise's five noise shapes — that FLAVOR is the sibling /compress's territory. It reports no noise-shape findings for the filler and routes flavor removal to /compress.", + "expected_output": "Does NOT classify filler / hedging / articles as any of audit-noise's eight noise shapes — that FLAVOR is the sibling /compress's territory. It reports no noise-shape findings for the filler and routes flavor removal to /compress.", "files": [], "expectations": [ - "Filler / hedging words are NOT classified under any of audit-noise's five noise shapes", + "Filler / hedging words are NOT classified under any of audit-noise's eight noise shapes", "Output routes flavor removal to /compress rather than classifying it as a noise shape" ] }, @@ -94,9 +97,37 @@ "Respects the `` line-scope opt-out — does not flag the citation-shaped content on the wrapped line", "Does not flag the `## ADR amendment block` heading or its dated `### … amendment` subsections (amendment-block exemption)", "Does not flag `## Recheck triggers`, `## Cross-references`, `## Sources`, or `## History` sections (Section EXEMPTIONS)", + "Does not flag the tracker references in the ordinary `## Outstanding work` section — the task-list items and the `TODO(#123)` marker are the `ticket-pr-residue` tracked-work carve-out, which holds outside an exempt section", "Reports zero Tier 1 findings overall", "Reports zero Tier 2 findings overall" ] + }, + { + "id": 9, + "name": "markdown-owns-the-shared-residue-shapes", + "prompt": "Declutter a README whose body says 'As you asked, this section documents the retry policy.' and 'See PR #45 for the rationale behind the ordering constraint.' Those look like comment-residue shapes — should I run /code-tidying:audit-comment-residue on the README instead?", + "expected_output": "Classifies both here: the opener as a conversational-antecedent (Tier 1, delete the address to the requester) and the reference as a ticket-pr-residue (Tier 2, delete the bare provenance or relocate it to a `## Sources` footer). Explains that the boundary between the two skills is the FILE TYPE, not the shape vocabulary — the three shape names are shared deliberately so one authoring failure keeps one name, and markdown has exactly one owner, this skill. Does NOT route the `.md` file to /code-tidying:audit-comment-residue.", + "files": [], + "expectations": [ + "The 'As you asked' opener is classified as a conversational-antecedent, not routed elsewhere", + "The 'See PR #45' reference is classified as a ticket-pr-residue at Tier 2", + "Output does NOT send the markdown file to /code-tidying:audit-comment-residue", + "Output states the boundary is by file type (markdown here, code there), with the shape names deliberately shared" + ] + }, + { + "id": 10, + "name": "ticket-reference-carve-out-is-outstanding-work", + "prompt": "Declutter a rule file. Its body says 'See PR #45 for the rationale behind the ordering constraint.' Its `## Outstanding work` section (an ordinary section, not `## Sources`) has the items `- [ ] Cap the retry budget, see issue 42 for the acceptance test` and `TODO(#123): see issue 44 before the next release.` Classify every tracker reference.", + "expected_output": "Flags only the body line as a ticket-pr-residue (Tier 2): it is bare provenance offered as the reason the prose says what it says. The task-list item and the `TODO(#123)` marker are NOT flagged — both denote outstanding tracked work, where the reference is the actionable part of the line, which is the markdown restatement of the code sibling's sanctioned-`TODO` exception. The treatment for the flagged line is delete-or-relocate to a `## Sources` / `## History` footer, which is itself an exempt section.", + "files": [], + "expectations": [ + "The body's 'See PR #45' line IS flagged as a ticket-pr-residue at Tier 2", + "The `- [ ]` task-list item is NOT flagged, despite sitting in a non-exempt section", + "The `TODO(#123)` marker line is NOT flagged", + "Output distinguishes bare provenance from outstanding tracked work as the carve-out's boundary", + "The treatment names relocation to a `## Sources` / `## History` footer as the alternative to deletion" + ] } ] } diff --git a/plugins/docs-hygiene/skills/audit-noise/evals/fixtures/legit-optouts.md b/plugins/docs-hygiene/skills/audit-noise/evals/fixtures/legit-optouts.md index 7ba7e089d2..65834fcaae 100644 --- a/plugins/docs-hygiene/skills/audit-noise/evals/fixtures/legit-optouts.md +++ b/plugins/docs-hygiene/skills/audit-noise/evals/fixtures/legit-optouts.md @@ -47,3 +47,13 @@ Status amended from `accepted` to `superseded by ADR-NNNN`. Bar replaced by Baz - 2026-04-30 — Bar contract finalized after stakeholder review - 2026-05-15 — Bar superseded by Baz per ADR-NNNN + +## Outstanding work + +Tracker references here are outstanding work, not provenance, so the `ticket-pr-residue` +carve-out covers them in an ordinary (non-exempt) section. + +- [ ] Reconcile the BarContract split with the Baz successor, see issue 42 for the acceptance test +- [x] Publish the Bar deprecation note, see issue 43 + +TODO(#123): see issue 44 before the next release cuts. diff --git a/plugins/docs-hygiene/skills/audit-noise/evals/fixtures/noisy-rule-snippet.md b/plugins/docs-hygiene/skills/audit-noise/evals/fixtures/noisy-rule-snippet.md index ba795e8d08..f593ff7eab 100644 --- a/plugins/docs-hygiene/skills/audit-noise/evals/fixtures/noisy-rule-snippet.md +++ b/plugins/docs-hygiene/skills/audit-noise/evals/fixtures/noisy-rule-snippet.md @@ -17,6 +17,12 @@ example the author drafted. The following three skills consume this rule: `/net-audit`, `/net-lint`, `/net-verify`. +As you asked, the cap sits at 30s rather than the 45s default. + +Task 2 replaces the old fixed-delay retry, and in this PR we make backoff the default. + +See PR #45 for the ordering rationale. + ## Cross-references - `src/net/backoff.md` — backoff curve derivation diff --git a/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh b/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh index bf4339eaf6..edb798bbdd 100755 --- a/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh +++ b/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh @@ -442,6 +442,188 @@ assert_contains "H1 closes Sources exemption" "$leak_out" "after an H1 closed So assert_not_contains "H3 Sources is exempt" "$leak_out" "under H3 Sources" assert_not_contains "H2 Sources body stays exempt" "$leak_out" "in Sources" +# --- 11. Prose residue shapes: plan / conversational / tracker ------------------------ + +RESIDUE="$TEST_TMPDIR/prose-residue.md" +cat >"$RESIDUE" <<'EOF' +# Prose residue fixture + +As you asked, this section documents the retry policy. +Per our discussion, the timeout is 30s. +Task 2 replaces the old buffering approach described below. +In this PR we switch the default to streaming. +See PR #45 for the rationale behind the ordering constraint. +Tracked in JIRA-123. +EOF +res_out="$(bash "$DETECT" "$RESIDUE")" +assert_contains "plan-reference detected" "$res_out" "Finding shape: plan-reference" +assert_contains "conversational-antecedent detected" "$res_out" "Finding shape: conversational-antecedent" +assert_contains "ticket-pr-residue detected" "$res_out" "Finding shape: ticket-pr-residue" +assert_contains "plan-reference is tier 1" "$res_out" $'Finding tier: 1\nFinding shape: plan-reference' +assert_contains "conversational-antecedent is tier 1" "$res_out" $'Finding tier: 1\nFinding shape: conversational-antecedent' +assert_contains "ticket-pr-residue is tier 2" "$res_out" $'Finding tier: 2\nFinding shape: ticket-pr-residue' +assert_contains "project-key tracker reference detected" "$res_out" "JIRA-123" +assert_contains "prose residue file summary" "$res_out" "| T1=4 T2=2 T3=0" + +# --- 11b. ticket-pr-residue carve-outs: outstanding tracked work ---------------------- + +CARVEOUT="$TEST_TMPDIR/tracked-work.md" +cat >"$CARVEOUT" <<'EOF' +# Tracked-work fixture + +- [ ] Cap the retry budget, see issue 42 for the acceptance test +- [x] Land the streaming default, see issue 43 for the acceptance test +TODO(#123): see issue 44 for the acceptance test. +Bare provenance instead: see issue 45 for the rationale. +EOF +carve_out="$(bash "$DETECT" "$CARVEOUT")" +assert_not_contains "unchecked task-list item is not ticket residue" "$carve_out" "issue 42" +assert_not_contains "checked task-list item is not ticket residue" "$carve_out" "issue 43" +assert_not_contains "tracked-work marker is not ticket residue" "$carve_out" "issue 44" +assert_contains "the same phrasing without a carve-out still flags" "$carve_out" "issue 45" + +# --- 11c. Section exemptions cover the prose residue shapes too ----------------------- + +RES_EXEMPT="$TEST_TMPDIR/residue-exempt.md" +cat >"$RES_EXEMPT" <<'EOF' +# Residue exemption fixture + +## Sources + +See PR #45 for the rationale behind the ordering constraint. +As you asked, this footer records the provenance. + +# Body resumes + +As you asked, this line sits outside the exempt section. +EOF +res_ex_out="$(bash "$DETECT" "$RES_EXEMPT")" +assert_not_contains "Sources footer suppresses ticket residue" "$res_ex_out" "ordering constraint" +assert_not_contains "Sources footer suppresses conversational antecedent" "$res_ex_out" "footer records" +assert_contains "residue after the exempt section still flags" "$res_ex_out" "outside the exempt section" + +# --- 11d. Prose negatives: the tightenings that keep live prose out ------------------ + +RES_NEG="$TEST_TMPDIR/prose-negatives.md" +cat >"$RES_NEG" <<'EOF' +# Prose negatives fixture + +As we discussed above, the resolver reads the team-tracked file only. +Scope the review to the files changed in this PR before reading further. +Budget deliberation by reversibility tier, per the planning chapter. +The template records what was planned and what was done instead. +The shape definition quotes `As you asked` as its own example. +EOF +res_neg_out="$(bash "$DETECT" "$RES_NEG")" +assert_not_contains "anaphoric cross-reference is not a conversational antecedent" \ + "$res_neg_out" "Finding shape: conversational-antecedent" +assert_not_contains "live 'in this PR' referent is not a plan reference" \ + "$res_neg_out" "Finding shape: plan-reference" +assert_contains "prose negatives file is clean" "$res_neg_out" "| T1=0 T2=0 T3=0" + +# --- 11e. conversational-antecedent: passive form and the `in` follower --------------- + +ANTE_POS="$TEST_TMPDIR/antecedent-positive.md" +cat >"$ANTE_POS" <<'EOF' +# Antecedent positives fixture + +As requested, retry the shard sweep three times. +As we discussed in yesterday's meeting, cap the retry budget. +As we decided in favor of streaming, the buffer is gone. +The change was requested by the operator, so the default flipped. +Fields come back as requested by the client. +EOF +ante_pos_out="$(bash "$DETECT" "$ANTE_POS")" +assert_contains "actor-less 'As requested,' flags" "$ante_pos_out" "retry the shard sweep three times" +assert_contains "'in' ahead of a conversation flags" "$ante_pos_out" "cap the retry budget" +assert_contains "'in favor of' is not a document locator" "$ante_pos_out" "the buffer is gone" +assert_not_contains "'was requested' is not the passive antecedent" "$ante_pos_out" "the default flipped" +assert_not_contains "'as requested by' attribution is not the passive antecedent" \ + "$ante_pos_out" "come back as requested" +assert_contains "antecedent positives count" "$ante_pos_out" "| T1=3 T2=0 T3=0" + +ANTE_NEG="$TEST_TMPDIR/antecedent-negative.md" +cat >"$ANTE_NEG" <<'EOF' +# Antecedent exemptions fixture + +As we discussed above, the resolver reads the team-tracked file only. +As we agreed Above, follower case does not decide the match. +As we decided in §3, the resolver reads the team-tracked file only. +As we decided in the ADR, the resolver reads the team-tracked file only. +As we decided in the previous section, the resolver reads it. +EOF +ante_neg_out="$(bash "$DETECT" "$ANTE_NEG")" +assert_not_contains "document locators keep the antecedent unflagged" \ + "$ante_neg_out" "Finding shape: conversational-antecedent" +assert_contains "antecedent exemptions file is clean" "$ante_neg_out" "| T1=0 T2=0 T3=0" + +# --- 11f. Contracted first-person actors, straight and curly apostrophes ------------- +# A contraction is the same actor and the same shape; requiring a literal space +# after the pronoun let it escape silently. Both apostrophe forms must work: `’` +# (U+2019) is what most editors produce, and matching only `'` would close half +# the gap while looking fixed. + +CONTRACT_POS="$TEST_TMPDIR/contracted-positive.md" +cat >"$CONTRACT_POS" <<'EOF' +# Contracted actors fixture + +In this PR we've already switched the default to streaming. +In this PR we’ve already switched the default to streaming. +In this commit I'm switching the default to streaming. +In this commit I’d already switched the default to streaming. +In this PR we’ll switch the default to streaming. +As we've discussed, the timeout is 30s. +As we’ve discussed, the timeout is 30s. +As we'd agreed, the timeout is 30s. +As you’d requested, the timeout is 30s. +EOF +contract_pos_out="$(bash "$DETECT" "$CONTRACT_POS")" +assert_contains "straight-apostrophe plan-reference flags" "$contract_pos_out" "Finding shape: plan-reference" +assert_contains "curly-apostrophe plan-reference flags" "$contract_pos_out" "Finding line: 4" +assert_contains "contracted 'I'm' plan-reference flags" "$contract_pos_out" "Finding line: 5" +assert_contains "curly 'I’d' plan-reference flags" "$contract_pos_out" "Finding line: 6" +assert_contains "curly 'we’ll' plan-reference flags" "$contract_pos_out" "Finding line: 7" +assert_contains "contracted antecedent flags" "$contract_pos_out" "Finding shape: conversational-antecedent" +assert_contains "curly-apostrophe antecedent flags" "$contract_pos_out" "Finding line: 9" +assert_contains "'we'd agreed' antecedent flags" "$contract_pos_out" "Finding line: 10" +assert_contains "curly 'you’d requested' antecedent flags" "$contract_pos_out" "Finding line: 11" +assert_contains "every contracted actor line flags" "$contract_pos_out" "| T1=9 T2=0 T3=0" + +# The follower test still runs behind a contraction: widening the actor must not +# smuggle past the document-locator and anaphoric-adverb stand-downs. +CONTRACT_FOLLOWER="$TEST_TMPDIR/contracted-follower.md" +cat >"$CONTRACT_FOLLOWER" <<'EOF' +# Contracted follower fixture + +As we've discussed above, the resolver reads the team-tracked file only. +As we’ve decided in §3, the resolver reads the team-tracked file only. +As we'd agreed in the ADR, the resolver reads the team-tracked file only. +EOF +contract_fol_out="$(bash "$DETECT" "$CONTRACT_FOLLOWER")" +assert_not_contains "anaphoric follower still stands a contracted antecedent down" \ + "$contract_fol_out" "Finding shape: conversational-antecedent" +assert_contains "contracted follower file is clean" "$contract_fol_out" "| T1=0 T2=0 T3=0" + +# Negatives: widening the actor must add no new false-positive surface. `'re` is +# deliberately absent from the antecedent set — its follower is a past +# participle, and "as you're asked" is a present-tense passive addressing the +# reader generically, not a pointer at a prior exchange. +CONTRACT_NEG="$TEST_TMPDIR/contracted-negative.md" +cat >"$CONTRACT_NEG" <<'EOF' +# Contracted negatives fixture + +Do it as you're asked to, then record the result. +Do it as you’re asked to, then record the result. +Scope the review to the files we've changed in this PR before reading further. +The weave is the same as we'd expect from the sibling detector. +EOF +contract_neg_out="$(bash "$DETECT" "$CONTRACT_NEG")" +assert_not_contains "\"as you're asked\" passive is not an antecedent" \ + "$contract_neg_out" "Finding shape: conversational-antecedent" +assert_not_contains "contracted actor after a live 'in this PR' referent is not a plan reference" \ + "$contract_neg_out" "Finding shape: plan-reference" +assert_contains "contracted negatives file is clean" "$contract_neg_out" "| T1=0 T2=0 T3=0" + # --- Chunk affordance: --offset / --limit over the sorted target list ---------------- CHUNK_A="$TEST_TMPDIR/chunk-a.md" diff --git a/plugins/docs-hygiene/skills/audit-noise/scripts/lib/noise-shapes.sh b/plugins/docs-hygiene/skills/audit-noise/scripts/lib/noise-shapes.sh index 2caf623f27..bf0d7a276d 100644 --- a/plugins/docs-hygiene/skills/audit-noise/scripts/lib/noise-shapes.sh +++ b/plugins/docs-hygiene/skills/audit-noise/scripts/lib/noise-shapes.sh @@ -95,6 +95,155 @@ audit_noise_line_has_ghost_ref() { return 1 } +# --- Prose-adapted residue shapes ------------------------------------------- +# +# plan-reference, conversational-antecedent, and ticket-pr-residue carry the +# same shape names the code-side sibling (/code-tidying:audit-comment-residue) +# owns, but the patterns are deliberately TIGHTER, not copies. That scanner +# classifies only the extracted comment portion of a line; this one classifies +# whole markdown prose, where the same words are load-bearing far more often. +# All three scan the inline-code strip, so a shape-definition example written +# in backticks does not self-match. + +# True when the text following an antecedent's `in` names a written locus a +# future reader can still open — a position inside this page, or a named +# durable document — rather than the conversation or circumstance the sentence +# came out of. The input is the raw follower text: it is lowercased here (the +# follower used to be compared case-sensitively, so a capitalised one fell +# through) and cut at the first clause break, so a locator noun in a LATER +# clause cannot exempt the antecedent. Two deliberate absences: tracker nouns +# (`issue`, `ticket`, `PR`) — a decision parked in a tracker is provenance, +# which ticket-pr-residue owns and this shape must not launder — and nouns for +# the conversation itself (`meeting`, `call`, `thread`, `review`), which are +# the residue. `part` and `phase` are absent too: "in part" is an idiom, and a +# phase is a stage of work, not a place in a document. +audit_noise_follower_is_document_locator() { + local head="${1,,}" + # Empty: an inline-code reference (`docs/x.md`) the strip removed, or a line + # that ends on the preposition. Both are references, not conversation. + [[ -z "${head//[[:space:]]/}" ]] && return 0 + head="${head#"${head%%[![:space:]]*}"}" + head="${head%%,*}" + head="${head%%;*}" + head="${head%%. *}" + # A markdown link, a section sign, or a path is a locus outright. `#` counts + # only ahead of a letter: `#anchor` is an anchor, `#482` is a tracker ref. + [[ "$head" == '['* || "$head" == *'§'* || "$head" == '#'[a-z]* ]] && return 0 + [[ "${head%% *}" == */* ]] && return 0 + [[ "$head" =~ (^|[^a-z])(section|sections|chapter|chapters|appendix|step|steps|table|tables|figure|paragraph|adr|adrs|rfc|rfcs|spec|specs|specification|readme|changelog|convention|conventions|guide|schema|doc|docs|document|documentation)([^a-z]|$) ]] && return 0 + return 1 +} + +# The sentence addresses the requester or the conversation that produced the +# text. Exactly two followers stand the shape down: an anaphoric adverb ("as we +# discussed above / earlier"), and `in` in front of a document locator ("as we +# decided in §3 / in the ADR"). A bare `in` used to exempt the whole sentence, +# which correctly spared "as we decided in the ADR" but also spared "as we +# decided in favor of X" and "as we discussed in yesterday's meeting" — both +# residue, because the referent is the conversation, not a document. +# The actor-less passive ("As requested, retry three times") is the same shape +# without the pronoun, but it is matched only as a clause-final adverbial: +# bounded that way, the live attribution "as requested by the client" and the +# ordinary verb phrase "was requested" stay out without a second pattern. +# Closing quotes are not clause breaks here on purpose — behind one the words +# are a quoted voice, not the page's own address to its reader. +# The pronoun admits a CONTRACTED auxiliary ("as we've discussed", "as you'd +# requested"): the same actor and the same shape, so requiring a literal space +# after the pronoun let the contraction escape silently. Only `'ve` and `'d` +# are admitted, because the follower is a past participle and those are the +# only auxiliaries that can precede one — `'re`/`'ll`/`'m` would add nothing +# but ungrammatical alternatives, and admitting `'re` would newly match the +# present-tense passive "do it as you're asked", which addresses the reader +# generically rather than pointing at a prior exchange. Both apostrophe forms +# are spelled as literal ALTERNATIVES rather than a bracket class: `’` (U+2019) +# is multibyte, and a bracket class over it breaks under a C locale, where the +# regex is byte-based. Same reasoning, and same spelling, as the I6_ERE in +# plugins/claude-config/skills/audit-instructions/scripts/instruction-scan.sh. +audit_noise_line_has_conversational_antecedent() { + local line="$1" rest follower + [[ "$line" =~ [Pp]er[[:space:]]+your[[:space:]]+request ]] && return 0 + [[ "$line" =~ [Pp]er[[:space:]]+our[[:space:]]+(conversation|discussion|chat) ]] && return 0 + [[ "$line" =~ [Ll]ike[[:space:]]+you[[:space:]]+said ]] && return 0 + [[ "$line" =~ (^|[^A-Za-z])[Aa]s[[:space:]]+requested[[:space:]]*([,;:.!?]|\)|$) ]] && return 0 + if [[ "$line" =~ [Aa]s[[:space:]]+(you|we)(\'ve|\'d|’ve|’d)?[[:space:]]+(asked|requested|discussed|agreed|decided)(.*)$ ]]; then + rest="${BASH_REMATCH[4]}" + # A follower counts only as a whole word behind whitespace, so punctuation + # ("as you asked, …") leaves the antecedent flagged. + if [[ "$rest" =~ ^[[:space:]]+([A-Za-z]+)(.*)$ ]]; then + follower="${BASH_REMATCH[1],,}" + rest="${BASH_REMATCH[2]}" + case "$follower" in + above | below | earlier | later | previously | elsewhere | under | at | on) return 1 ;; + in) audit_noise_follower_is_document_locator "$rest" && return 1 ;; + *) ;; + esac + fi + return 0 + fi + return 1 +} + +# The prose points at the work plan / changeset that produced the page rather +# than at the page's subject. Four of the code lib's cues are deliberately NOT +# carried over, each because a corpus sweep showed it matching live prose: +# "per the plan" — prefix-matches "per the planning chapter", and a doc +# citing a plan artifact that still exists is a live +# cross-reference, not residue; +# "as planned" — substring of "was planned", so "what was planned, what +# was done instead" self-matched; +# "in this change" / "in this session" — ordinary domain vocabulary in an +# agent-tooling corpus. +# "in this PR" survives only with a first-person actor behind it, which is what +# separates narration ("in this PR we switch the default") from a live referent +# ("the files changed in this PR"). A CONTRACTED actor is still that actor, so +# the pronoun admits one ("in this PR we've already switched the default"): +# requiring a literal space after it let the same shape escape on a +# contraction, and widening to the contraction adds no false-positive surface +# because the discriminator is the pronoun, not the verb behind it. Unlike the +# antecedent above — whose past-participle follower admits only `'ve`/`'d` — +# any auxiliary can lead the present/future narration here, so all five are +# admitted; the non-words the shared alternation also spells (`I're`, `we'm`) +# cost nothing and keep this one group instead of two per-pronoun ones. Both +# apostrophe forms are literal alternatives, never a bracket class, for the +# C-locale reason recorded above. +audit_noise_line_has_plan_reference() { + local line="$1" + [[ "$line" =~ [Rr]eplaces[[:space:]]+the[[:space:]]+old ]] && return 0 + [[ "$line" =~ [Ii]n[[:space:]]+this[[:space:]]+(PR|MR|pull[[:space:]]+request|commit|changeset|refactor),?[[:space:]]+(we|I)(\'ve|\'re|\'ll|\'d|\'m|’ve|’re|’ll|’d|’m)?[[:space:]] ]] && return 0 + [[ "$line" =~ ([Tt]ask|[Pp]hase|[Ss]tep)[[:space:]]+#?[0-9]+[[:space:]]+(of|in)[[:space:]]+(the|this)[[:space:]]+plan ]] && return 0 + return 1 +} + +# Markdown restatement of the code skill's sanctioned-marker carve-out. Both +# forms denote OUTSTANDING TRACKED WORK, where the reference is the actionable +# part of the sentence; everything else is bare provenance, which is the shape. +# Nothing further earns a carve-out here: the sanctioned home for a provenance +# citation is a `## Sources` / `## History` footer, and the section exemptions +# in detect.sh already skip those (as they skip CHANGELOG.md, fences, and +# frontmatter) before any shape runs. +audit_noise_line_is_tracked_work() { + local line="$1" + # Task-list checklist item: `- [ ] … #123` / `- [x] … #123`. + [[ "$line" =~ ^[[:space:]]*[-*+][[:space:]]+\[[[:space:]xX]\][[:space:]] ]] && return 0 + # Tracked-work marker with a parenthesised reference. + [[ "$line" =~ (TODO|FIXME|HACK|XXX)[[:space:]]*\(#?[A-Za-z0-9_-]+\) ]] && return 0 + return 1 +} + +# Bare provenance: a tracker, PR, or branch back-reference offered as the +# reason the surrounding prose says what it says. +audit_noise_line_has_ticket_pr_residue() { + local line="$1" + audit_noise_line_is_tracked_work "$line" && return 1 + [[ "$line" =~ [Ss]ee[[:space:]]+(PR|MR|pull[[:space:]]+request|issue|ticket)[[:space:]]*#?[0-9] ]] && return 0 + [[ "$line" =~ [Ss]ee[[:space:]]+the[[:space:]]+(PR|MR|pull[[:space:]]+request|issue|ticket)[[:space:]]*#?[0-9] ]] && return 0 + [[ "$line" =~ ([Ii]ntroduced|[Aa]dded|[Ll]anded|[Ss]hipped|[Ff]ixed|[Rr]everted|[Dd]ecided|[Aa]greed)[[:space:]]+in[[:space:]]+(PR|MR|issue|ticket)[[:space:]]*#?[0-9] ]] && return 0 + [[ "$line" =~ [Tt]racked[[:space:]]+in[[:space:]]+#[0-9] ]] && return 0 + [[ "$line" =~ ([Tt]racked|[Ff]iled|[Ll]ogged|[Rr]eported)[[:space:]]+(in|as|under)[[:space:]]+[A-Z][A-Z0-9]+-[0-9] ]] && return 0 + [[ "$line" =~ [Ff]rom[[:space:]]+the[[:space:]]+feature[[:space:]]+branch ]] && return 0 + return 1 +} + # Append matching shape names into the nameref array (avoids a per-line # command-substitution subshell in the detect hot loop). # Ghost-ref scans an unwrap (ticks removed, content kept); other shapes scan a @@ -132,6 +281,15 @@ audit_noise_detect_shapes_into() { [[ "$stripped" =~ [Aa]uto-loads[[:space:]]+when ]]; then _audit_noise_shapes_out+=('scope-meta') fi + if audit_noise_line_has_plan_reference "$stripped"; then + _audit_noise_shapes_out+=('plan-reference') + fi + if audit_noise_line_has_conversational_antecedent "$stripped"; then + _audit_noise_shapes_out+=('conversational-antecedent') + fi + if audit_noise_line_has_ticket_pr_residue "$stripped"; then + _audit_noise_shapes_out+=('ticket-pr-residue') + fi ((${#_audit_noise_shapes_out[@]} > 0)) } @@ -152,8 +310,8 @@ audit_noise_detect_shapes() { audit_noise_shape_tier() { local shape="$1" case "$shape" in - ghost-ref | preamble) printf '2' ;; - citation | enum-list | scope-meta) printf '1' ;; + ghost-ref | preamble | ticket-pr-residue) printf '2' ;; + citation | enum-list | scope-meta | plan-reference | conversational-antecedent) printf '1' ;; *) printf '3' ;; esac } @@ -163,8 +321,8 @@ audit_noise_shape_tier_into() { local shape="$1" local -n _audit_noise_tier_out="$2" case "$shape" in - ghost-ref | preamble) _audit_noise_tier_out=2 ;; - citation | enum-list | scope-meta) _audit_noise_tier_out=1 ;; + ghost-ref | preamble | ticket-pr-residue) _audit_noise_tier_out=2 ;; + citation | enum-list | scope-meta | plan-reference | conversational-antecedent) _audit_noise_tier_out=1 ;; *) _audit_noise_tier_out=3 ;; esac }