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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/docs-hygiene/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "docs-hygiene",
"version": "0.21.4",
"version": "0.21.5",
"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",
Expand Down
19 changes: 19 additions & 0 deletions plugins/docs-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog — docs-hygiene plugin

## [0.21.5]

### Fixed

- **`audit-noise`'s `conversational-antecedent` shape now runs `under` / `at` /
`on` through the same document-locator predicate `in` already uses (#3192).**
Those three followers still carried the blanket exemption #3162 narrowed only
for `in`, so conversational residue phrased with them escaped: "as we agreed on
Tuesday", "as we decided at the standup", "as we agreed under time pressure".

The anaphoric-adverb followers (`above` / `below` / `earlier` / `later` /
`previously` / `elsewhere`) stay unconditional — they name a position a future
reader can still open. A real document locus after the narrowed prepositions
stays exempt (`as we decided on the ADR's recommendation`), including when
the locus is entirely inline code and the clause cut would otherwise leave
only the punctuation that followed it. High-traffic `on` idioms that are not
this shape (`based on`, `depends on`, `on disk`, `on the other hand`) never
matched the antecedent pattern and stay unflagged.

## [0.21.4]

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion plugins/docs-hygiene/skills/audit-noise/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Only a page that passes admission proceeds to the nine in-page NOISE shapes belo
| `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 |
| `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 follower classes stand the shape down, because both name something a future reader can still open: an anaphoric adverb (`as we discussed above`), and `in` / `under` / `at` / `on` 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`, `on the ADR's recommendation`). Those prepositions ahead of anything else are matched, so `as we discussed in yesterday's meeting`, `as we decided in favor of X`, `as we agreed on Tuesday`, and `as we decided at the standup` 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 |
| `negation` — imperative prohibition with no positive alternative | An **imperative** prohibition (`never`, `do not`, `don't`, `avoid`, `must not`, `should not`) opening the line, on a line that **closes its own sentence**, with no positive alternative stated in that sentence ("Do not use markdown."). Descriptive prose ("Older versions do not support this flag"), a mid-sentence cue (already-paired "Prefer X; never Y"), a hard-wrapped continuation, and a table row are all out of scope | 2 | Rewrite to the positive target the prohibition implies (*"Do not use markdown"* → *"Compose your response as smoothly flowing prose paragraphs"*). Keep a negation only where the positive form genuinely loses the constraint, and then pair it with the positive in the same sentence. **Never a deletion** — the constraint survives; only its framing changes. The write-side rule this completes is [`/docs-hygiene:write-for-agents`](../write-for-agents/SKILL.md) "Prompt the positive". **A hard guardrail that cannot be phrased positively is not a finding** and is never flagged (carve-outs in Hard rules) |

Expand Down
49 changes: 49 additions & 0 deletions plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,55 @@ 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"

# under / at / on used to exempt unconditionally, the same weakness `in` had
# before the locator predicate. Conversational residue phrased with those
# prepositions must flag; a real document locus after them must stay exempt.
# High-traffic `on` idioms that are NOT this shape (based on, depends on, on
# disk, on the other hand) are pinned so narrowing `on` cannot start matching
# ordinary prose that never had an antecedent.
ANTE_PREP="$TEST_TMPDIR/antecedent-prepositions.md"
cat >"$ANTE_PREP" <<'EOF'
# Antecedent preposition fixture

As we agreed on Tuesday, the cap is 30s.
As we decided at the standup, retry three times.
As we agreed under time pressure, this is temporary.
As we decided on the ADR's recommendation, the resolver reads the team file.
The default is based on the measured timeout.
Retry count depends on the shard width.
Persist the snapshot on disk after the write.
On the other hand, keep the current default.
As we discussed above, the resolver reads the team-tracked file only.
EOF
ante_prep_out="$(bash "$DETECT" "$ANTE_PREP")"
assert_contains "'on Tuesday' is conversational residue" "$ante_prep_out" "the cap is 30s"
assert_contains "'at the standup' is conversational residue" "$ante_prep_out" "retry three times"
assert_contains "'under time pressure' is conversational residue" "$ante_prep_out" "this is temporary"
assert_not_contains "'on the ADR' is a document locator" "$ante_prep_out" "the resolver reads the team file"
assert_not_contains "anaphoric 'above' is still a blanket exemption" \
"$ante_prep_out" "the resolver reads the team-tracked file only"
assert_not_contains "'based on' is not an antecedent" "$ante_prep_out" "measured timeout"
assert_not_contains "'depends on' is not an antecedent" "$ante_prep_out" "shard width"
assert_not_contains "'on disk' is not an antecedent" "$ante_prep_out" "after the write"
assert_not_contains "'on the other hand' is not an antecedent" "$ante_prep_out" "current default"
assert_contains "three new preposition residues and nothing else" "$ante_prep_out" "| T1=3 T2=0 T3=0"

# An inline-code locator the strip removes, followed by punctuation, must
# still count as a document locus after the clause cut — otherwise `on` /
# `at` / `under` newly flag a real path cite.
ANTE_TICK="$TEST_TMPDIR/antecedent-inline-locator.md"
cat >"$ANTE_TICK" <<'EOF'
# Inline-code locator fixture

As we discussed on `docs/design.md`, retain the fallback.
As we decided at `§3`, keep the resolver.
As we agreed under `the ADR`, ship the narrower form.
EOF
ante_tick_out="$(bash "$DETECT" "$ANTE_TICK")"
assert_not_contains "an inline-code locator after on/at/under stays exempt" \
"$ante_tick_out" "Finding shape: conversational-antecedent"
assert_contains "inline-code locator file is clean" "$ante_tick_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: `’`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ audit_noise_follower_is_document_locator() {
head="${head%%,*}"
head="${head%%;*}"
head="${head%%. *}"
# Recheck after the clause cut: an inline-code locator the strip removed
# leaves only the punctuation that followed it (`on `path`, …` → `, …` →
# empty). Empty-before-cut already counts as a locator; empty-after-cut is
# the same signal and must not fall through to a "not a locator" miss.
[[ -z "${head//[[:space:]]/}" ]] && return 0
# 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
Expand All @@ -135,12 +140,15 @@ audit_noise_follower_is_document_locator() {
}

# 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,
# text. Exactly two follower classes stand the shape down: an anaphoric adverb
# ("as we discussed above / earlier"), and a preposition (`in` / `under` / `at`
# / `on`) in front of a document locator ("as we decided in §3 / in the ADR /
# on the ADR's recommendation"). 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.
# residue, because the referent is the conversation, not a document. `under`,
# `at`, and `on` carried the same blanket exemption until the locator predicate
# was applied to them too: "as we agreed on Tuesday" is residue, identically.
# 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
Expand Down Expand Up @@ -173,8 +181,8 @@ audit_noise_line_has_conversational_antecedent() {
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 ;;
above | below | earlier | later | previously | elsewhere) return 1 ;;
in | under | at | on) audit_noise_follower_is_document_locator "$rest" && return 1 ;;
Comment thread
kyle-sexton marked this conversation as resolved.
*) ;;
esac
fi
Expand Down
Loading