From 4d45c36f2781b3dc1c3f455f6c55d258c75d3734 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 03:11:38 +0000 Subject: [PATCH 1/4] feat(docs-hygiene): audit-noise gains three residue shapes (0.19.0) The code-side sibling /code-tidying:audit-comment-residue detects four residue shapes; audit-noise detected five noise shapes; the two sets did not tile the space. Only history-narration had a markdown counterpart (audit-noise's citation). plan-reference, conversational-antecedent, and ticket-pr-residue had no detector on either side, so a README or rule body saying "as you asked, retry three times" was invisible to the whole fleet -- a gap behind an otherwise correct boundary, not a skipped file type. The shapes go to audit-noise rather than widening the code skill to .md, because on a markdown line history-narration and citation would fire together with opposite rulings (delete vs. relocate to a Sources footer). Two owners for one line is a precedence problem; one owner per file type is not. The boundary is now explicitly by file type, with the three shape names deliberately shared. Patterns are adapted, not copied. The code lib classifies only the extracted comment portion of a line; this one classifies whole prose. Measured against this repo's 1136-file tracked-markdown corpus, four of the sibling's cues had to go: "per the plan" prefix-matches "per the planning chapter"; "as planned" is a substring of "was planned"; "in this change" and "in this session" are ordinary domain vocabulary here. "in this PR" now requires a first-person actor behind it, and "as we discussed" stands down before an anaphoric follower. Those tightenings cut the corpus delta from 32 findings to 12. ticket-pr-residue's carve-out is restated in markdown terms: a task-list checklist item and a TODO-family marker with a parenthesised reference are never flagged, because both denote outstanding tracked work. Nothing further is carved out -- the sanctioned home for a provenance citation is a Sources or History footer, which the existing section exemptions already skip. Refs #3125 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HigdNorttH9UCTeh5QdycM --- .../docs-hygiene/.claude-plugin/plugin.json | 2 +- plugins/docs-hygiene/CHANGELOG.md | 46 +++++++++ plugins/docs-hygiene/README.md | 2 +- .../docs-hygiene/skills/audit-noise/SKILL.md | 22 +++-- .../skills/audit-noise/evals/evals.json | 39 +++++++- .../evals/fixtures/legit-optouts.md | 10 ++ .../evals/fixtures/noisy-rule-snippet.md | 6 ++ .../skills/audit-noise/scripts/detect.test.sh | 79 +++++++++++++++ .../audit-noise/scripts/lib/noise-shapes.sh | 95 ++++++++++++++++++- 9 files changed, 284 insertions(+), 17 deletions(-) diff --git a/plugins/docs-hygiene/.claude-plugin/plugin.json b/plugins/docs-hygiene/.claude-plugin/plugin.json index 9d971f25b2..45e6a1357b 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 b554bf3386..399fff74d3 100644 --- a/plugins/docs-hygiene/CHANGELOG.md +++ b/plugins/docs-hygiene/CHANGELOG.md @@ -1,5 +1,51 @@ # Changelog — docs-hygiene plugin +## [0.19.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. + + **`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.18.3] ### Changed diff --git a/plugins/docs-hygiene/README.md b/plugins/docs-hygiene/README.md index 26aa5c842c..bdff180f11 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 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: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 3cea92c5bc..bc4386ccc1 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 noise: 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, …`, `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. An anaphoric follower (`as we discussed above`, `as we decided in §3`) is an intra-document cross-reference and 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 3+ files, that is the sibling `/docs-hygiene:extract-ssot`'s territory. 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 a55683908a..6ef74b008a 100755 --- a/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh +++ b/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh @@ -438,6 +438,85 @@ 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" + # --- 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..0e90753b8a 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,84 @@ 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. + +# The sentence addresses the requester or the conversation that produced the +# text. An anaphoric follower ("as we discussed above / below / in section 3") +# makes it an intra-document cross-reference instead — legitimate, not residue. +audit_noise_line_has_conversational_antecedent() { + local line="$1" + [[ "$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 + if [[ "$line" =~ [Aa]s[[:space:]]+(you|we)[[:space:]]+(asked|requested|discussed|agreed|decided)([[:space:]]+([A-Za-z]+))? ]]; then + case "${BASH_REMATCH[4]:-}" in + above | below | earlier | later | previously | elsewhere | in | under | at | on) return 1 ;; + *) return 0 ;; + esac + 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"). +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)[[: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 +210,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 +239,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 +250,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 } From 376d98ad4a776a54276667f6f26bef5cafa312a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 04:36:35 +0000 Subject: [PATCH 2/4] fix(docs-hygiene): quote the audit-noise summary so its frontmatter parses The 0.19.0 summary rewrite introduced a colon ("Classify markdown noise: citations, ...") in an unquoted YAML scalar, which makes the frontmatter invalid: a block-mapping value cannot contain ": ". check-hook-exec-form.sh fails closed on frontmatter it cannot read and rejected the file. skill-quality's check-skill.sh passed the same file, so the defect reached CI: its frontmatter reader is lenient enough to return the value that a strict YAML parse refuses. Quoting the scalar fixes the file; the reader divergence is a separate finding. Value unchanged at exactly 100 codepoints, still inside the summary cap. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HigdNorttH9UCTeh5QdycM --- plugins/docs-hygiene/skills/audit-noise/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/docs-hygiene/skills/audit-noise/SKILL.md b/plugins/docs-hygiene/skills/audit-noise/SKILL.md index bc4386ccc1..ffd004aea7 100644 --- a/plugins/docs-hygiene/skills/audit-noise/SKILL.md +++ b/plugins/docs-hygiene/skills/audit-noise/SKILL.md @@ -7,7 +7,7 @@ allowed-tools: ["Bash(${CLAUDE_SKILL_DIR}/scripts/detect.sh:*)", "Bash(grep:*)", shell: bash metadata: workflow-stage: anytime - summary: Classify markdown noise: citations, ghost refs, meta-commentary, plan/conversational/tracker residue + summary: "Classify markdown noise: citations, ghost refs, meta-commentary, plan/conversational/tracker residue" --- ## Pre-computed context From 3345108b08d701a5da32f0d73e0ade038ec25327 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 04:51:04 +0000 Subject: [PATCH 3/4] fix(docs-hygiene): tighten audit-noise's conversational-antecedent followers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings on the 0.19.0 shape, plus the summary line the cheatsheet gate rejected on the previous push. The `in` exemption was a blanket: any antecedent whose next word was `in` stood down, so "as we decided in the ADR" was spared (right) alongside "as we decided in favor of X" and "as we discussed in yesterday's meeting" (wrong — the referent there is the conversation, not a document). The follower test now asks what the reference points AT: `in` stands the shape down only ahead of a document locator — a section sign 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, because a decision parked in an issue is provenance that ticket-pr-residue owns. Followers are compared case-insensitively; the other stop-words are unchanged. The actor-less passive "As requested, ..." matched nothing, because the pattern required `you` or `we`. It is now matched as a clause-final adverbial only, which keeps the live attribution "as requested by the client" and the ordinary verb phrase "was requested" out without a second pattern. `metadata.summary` is now a plain unquoted scalar with no colon. Quoting it made the cheatsheet reader count 102 codepoints and fail the plugin gate; that reader also rejects ": " outright, since an invalid frontmatter value makes Claude Code drop ALL frontmatter silently. Colon-free and unquoted is the only form the three readers agree on. Cheat sheet regenerated to match. Corpus sweep over the 1136 tracked markdown files is byte-identical before and after (T1=13 T2=42 T3=0), confirmed line-by-line against the old detector: zero disagreements. detect.test.sh is 87 checks, all passing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HigdNorttH9UCTeh5QdycM --- docs/SKILL-CHEAT-SHEET.md | 2 +- plugins/docs-hygiene/CHANGELOG.md | 17 +++++ .../docs-hygiene/skills/audit-noise/SKILL.md | 4 +- .../skills/audit-noise/scripts/detect.test.sh | 36 ++++++++++ .../audit-noise/scripts/lib/noise-shapes.sh | 65 ++++++++++++++++--- 5 files changed, 113 insertions(+), 11 deletions(-) diff --git a/docs/SKILL-CHEAT-SHEET.md b/docs/SKILL-CHEAT-SHEET.md index e5636f58f5..6c29afa5e9 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/CHANGELOG.md b/plugins/docs-hygiene/CHANGELOG.md index 399fff74d3..199147abd1 100644 --- a/plugins/docs-hygiene/CHANGELOG.md +++ b/plugins/docs-hygiene/CHANGELOG.md @@ -36,6 +36,23 @@ 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. + + **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 diff --git a/plugins/docs-hygiene/skills/audit-noise/SKILL.md b/plugins/docs-hygiene/skills/audit-noise/SKILL.md index ffd004aea7..4850746318 100644 --- a/plugins/docs-hygiene/skills/audit-noise/SKILL.md +++ b/plugins/docs-hygiene/skills/audit-noise/SKILL.md @@ -7,7 +7,7 @@ allowed-tools: ["Bash(${CLAUDE_SKILL_DIR}/scripts/detect.sh:*)", "Bash(grep:*)", shell: bash metadata: workflow-stage: anytime - summary: "Classify markdown noise: citations, ghost refs, meta-commentary, plan/conversational/tracker residue" + summary: Classify markdown for citations, ghost refs, meta-commentary, plan/conversational/tracker residue --- ## Pre-computed context @@ -56,7 +56,7 @@ Only a page that passes admission proceeds to the eight in-page NOISE shapes bel | `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, …`, `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. An anaphoric follower (`as we discussed above`, `as we decided in §3`) is an intra-document cross-reference and is not matched | +| `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. 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 6ef74b008a..a88cca96ea 100755 --- a/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh +++ b/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh @@ -517,6 +517,42 @@ 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" + # --- 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 0e90753b8a..7d401b2b13 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 @@ -105,19 +105,68 @@ audit_noise_line_has_ghost_ref() { # 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. An anaphoric follower ("as we discussed above / below / in section 3") -# makes it an intra-document cross-reference instead — legitimate, not residue. +# 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. audit_noise_line_has_conversational_antecedent() { - local line="$1" + 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 - if [[ "$line" =~ [Aa]s[[:space:]]+(you|we)[[:space:]]+(asked|requested|discussed|agreed|decided)([[:space:]]+([A-Za-z]+))? ]]; then - case "${BASH_REMATCH[4]:-}" in - above | below | earlier | later | previously | elsewhere | in | under | at | on) return 1 ;; - *) return 0 ;; - esac + [[ "$line" =~ (^|[^A-Za-z])[Aa]s[[:space:]]+requested[[:space:]]*([,;:.!?]|\)|$) ]] && return 0 + if [[ "$line" =~ [Aa]s[[:space:]]+(you|we)[[:space:]]+(asked|requested|discussed|agreed|decided)(.*)$ ]]; then + rest="${BASH_REMATCH[3]}" + # 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 } From 825ebb1b109edbb72f5e38cd7cddbf050d82b19c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 05:20:54 +0000 Subject: [PATCH 4/4] fix(docs-hygiene): admit contracted actors in audit-noise residue shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `plan-reference` and `conversational-antecedent` both required a literal space directly after the first-person pronoun, so a contracted actor fell through silently — "in this PR we've already switched the default" and "as we've discussed, the timeout is 30s" are the exact narration and antecedent shapes those rules target, and neither was flagged. Unlike the follower tightenings that preceded it, this is not a precision/recall trade. The discriminator in both patterns is the PRONOUN, not the verb behind it, so admitting a contraction of that same pronoun adds no new false-positive surface; it only stops the shape escaping on an apostrophe. Both apostrophe forms are handled. Prose in this corpus frequently uses the typographic right single quote `’` (U+2019), which most editors produce, so a fix matching only `'` would have closed half the gap while looking complete. They are spelled as literal ALTERNATIVES — `(\'ve|\'d|’ve|’d)` — never a bracket class: `’` is multibyte, and a bracket class over it breaks under a C locale, where bash's regex is byte-based. That is the same spelling, for the same reason, as the I6_ERE in claude-config's instruction-scan.sh. Verified to behave identically under LC_ALL=C and LC_ALL=C.UTF-8. The two contraction sets differ, deliberately: - `conversational-antecedent` admits only `'ve` and `'d`. Its follower is a past participle (asked/requested/discussed/agreed/decided), and those are the only two auxiliaries that can precede one. 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 — a real false positive avoided, not a hypothetical one. - `plan-reference` admits all five (`'ve 're 'll 'd 'm`), because any auxiliary can lead its present/future narration ("in this PR we'll switch the default", "in this commit I'm switching"). The non-words the shared alternation also spells (`I're`, `we'm`) cost nothing and keep it one group rather than two per-pronoun ones. Corpus sweep over the 1136-file tracked-markdown corpus is unchanged at T1=13 T2=42 T3=0, with a byte-identical finding list. That zero delta is genuine absence rather than an inert pattern: the corpus contains no instance of either contracted shape (verified by direct grep), and detect.test.sh gains 15 checks covering both functions, both apostrophe forms, the follower stand-downs behind a contraction, and the negatives. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HigdNorttH9UCTeh5QdycM --- plugins/docs-hygiene/CHANGELOG.md | 6 +- .../skills/audit-noise/scripts/detect.test.sh | 67 +++++++++++++++++++ .../audit-noise/scripts/lib/noise-shapes.sh | 30 +++++++-- 3 files changed, 98 insertions(+), 5 deletions(-) diff --git a/plugins/docs-hygiene/CHANGELOG.md b/plugins/docs-hygiene/CHANGELOG.md index 199147abd1..b11209d2ce 100644 --- a/plugins/docs-hygiene/CHANGELOG.md +++ b/plugins/docs-hygiene/CHANGELOG.md @@ -45,7 +45,11 @@ 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. + 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 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 a88cca96ea..82ad57fe4a 100755 --- a/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh +++ b/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh @@ -553,6 +553,73 @@ 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 7d401b2b13..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 @@ -147,14 +147,26 @@ audit_noise_follower_is_document_locator() { # 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)[[:space:]]+(asked|requested|discussed|agreed|decided)(.*)$ ]]; then - rest="${BASH_REMATCH[3]}" + 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 @@ -183,11 +195,21 @@ audit_noise_line_has_conversational_antecedent() { # 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"). +# ("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)[[:space:]] ]] && 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 }