cc-os/plugins/os-doc-hygiene/PRD.md

311 lines
18 KiB
Markdown
Raw Permalink Normal View History

# PRD: `doc-hygiene` plugin
> Status: shipped · Built at `plugins/os-doc-hygiene/` in cc-os, not the
> `cc-plugins` target/marketplace named below. Historical design doc — kept
> as-is except this note and the location/marketplace corrections below.
## Problem Statement
When I work intensively in a project, documentation accumulates two distinct
defects that both degrade how usefully an AI agent (and I) can read the repo:
- **Stale docs** — content that is now *wrong*: it contradicts the running
system, references files/symbols/paths that no longer exist, is pinned to an
old tool version, claims "in progress" for shipped work, or duplicates a fact
that has since drifted. Trusting it causes wrong actions. This is the
high-severity class because agents read confidently and don't sanity-check.
- **Bloated docs** — content that is still *true* but mostly irrelevant: long
debugging narratives, resolved-problem detail at full fidelity, decisions
recorded without distillation, append-only growth. It doesn't lie; it dilutes
attention and burns context-window budget, and buries the one live caveat in
archaeology.
Severity scales with **injection frequency**: a stale line in a doc nobody opens
is annoying; the same line in an auto-injected file (`CLAUDE.md`, memory index)
misleads *every* session unprompted.
Today I notice this only incidentally, have no systematic way to find it across
projects, and cleaning it up by hand wastes both my time and AI tokens. I also
don't want a tool that interrupts my flow or silently rewrites my repos.
## Solution
A Claude Code plugin, installed globally but operating per-project, that:
1. **Reminds, doesn't nag.** A deterministic `SessionStart` hook checks an
in-project timestamp and, only if the last hygiene check is older than a
threshold, injects a noticeable reminder telling me how many days it's been
and which command to run. Zero AI tokens unless I act. Snoozes so it reminds
at most once per day while stale.
2. **Checks on demand.** A `check` skill runs a **deterministic scan** that
gathers objective signals and a candidate shortlist, then an **AI
classification pass** that reads candidates, categorizes them (stale vs
bloat, by sub-type), assigns each a concrete operation tagged by
*op-type* (`deterministic` | `generative`) and *safety tier* (`auto` |
`confirm`), and estimates context-weight savings. It produces a **human
report** (what to clean and why) and a **machine report** (structured ops
for the cleaner to consume).
3. **Cleans, git-safely and scoped.** A `clean` skill consumes the machine
report. For each op it checks an **mtime guard** (file unchanged since the
check); `deterministic` ops are applied mechanically with no model;
`generative` ops are delegated to Sonnet subagents. `auto`-tier ops run
without fuss; `confirm`-tier ops escalate into an approval list. Cleanup runs
only on a clean/committed tree (or after an auto-checkpoint) and lands as a
single reviewable commit. The user can scope a run to specific categories or
files.
4. **Composes.** A `sweep` entry runs check-then-clean in one go, still
surfacing the report and gating `confirm`-tier ops.
The design leads with deterministic code (scan, signals, state, patch-apply,
token estimate) and reserves AI for the genuine judgment (classification and
prose distillation). This keeps it fast, cheap, and trustworthy.
## User Stories
1. As a developer, I want a deterministic `SessionStart` reminder when a
project's docs haven't been checked in over a threshold, so that I'm nudged
without spending any AI tokens.
2. As a developer, I want the reminder to be visually noticeable and to name the
exact command to run, so that acting on it is one step.
3. As a developer, I want the reminder to snooze (at most once/day while stale),
so that it doesn't nag me every session for a week.
4. As a developer, I want hygiene state stored per-project in a gitignored dot
directory, so that there is no global index to corrupt, race, or itself go
stale.
5. As a developer, I want to run a check manually with a slash command, so that
after a heavy context-building session I can clear what accumulated.
6. As a developer, I want the check to deterministically scan scoped files and
compute objective signals (broken references, version skew, edit-recency vs
git churn, location, append-only growth, archive-to-live ratio, frontmatter
markers), so that the AI pass starts from facts, not a blank read.
7. As a developer, I want the AI pass to classify each candidate as stale
(contradicted, orphaned, superseded, provisional, completed-in-place, duplicated) or
bloat (distill, split, freeze), so that the right remedy is chosen per file.
8. As a developer, I want each recommended operation tagged with an op-type
(`deterministic` | `generative`) and a safety tier (`auto` | `confirm`), so
that the cleaner knows what it can apply mechanically and what needs a model
or my approval.
9. As a developer, I want a human-readable report describing what should be
cleaned and *why*, grouped by category, so that I can decide at a glance.
10. As a developer, I want a machine-readable report the cleaner consumes
directly, so that the cleanup step doesn't re-derive the analysis.
11. As a developer, I want only the single most-recent report kept (rollover
deletes prior reports), so that the tool's own artifacts don't become bloat.
12. As a developer, I want the check to estimate context-weight savings per
file, summed into categories and a total (bottom-up), so that I can
prioritize.
13. As a developer, I want savings honestly framed and weighted by injection
frequency (auto-injected files counted as real per-session savings,
on-demand docs as theoretical-max), so that the numbers aren't misleading.
14. As a developer, I want the cleaner to apply `deterministic`+`auto` ops
(move-to-archive, freeze-stamp, known-target link fix, exact-dup dedupe)
with no model and no prompt, so that no-brainer cleanup just happens.
15. As a developer, I want every destructive, subjective, or generative op to be
`confirm`-tier and escalated for my approval, so that nothing surprising
happens to my repos.
16. As a developer, I want a per-op mtime guard so that if a file changed since
the check, its cached edit is skipped (and re-analysis recommended) rather
than blindly applied.
17. As a developer, I want generative cleanup (distilling narrative into live
constraints) delegated to a Sonnet subagent, so that distillation quality is
high and doesn't drop the constraint that mattered.
18. As a developer, I want cleanup to run only on a clean/committed tree, or to
auto-commit a WIP checkpoint first, so that my uncommitted work is never
lost.
19. As a developer, I want each cleanup run to land as a single reviewable
commit, so that I can inspect or revert the whole sweep trivially.
20. As a developer, I want to scope a cleanup to specific categories or files
(e.g. "only the orphaned-reference fixes"), so that I stay in control of how
much changes at once.
21. As a developer, I want a `sweep` command that runs check-then-clean, so that
when I already trust the project I can do both in one step.
22. As a developer, I want `sweep` to still surface the report and gate
`confirm`-tier ops, so that the convenience path isn't a foot-gun.
23. As a developer, I want a status command that reports last-check and
last-clean timestamps for the current project, so that I can see where it
stands without running anything.
24. As a developer, I want to protect files from being flagged via
`hygiene: frozen` frontmatter or a `.dochygiene-ignore` file, so that
deliberately-frozen records and append-only logs aren't repeatedly flagged.
25. As a developer, I want sensible scope defaults (markdown + known doc
locations; excluding build/vendor/archive/graphify-out dirs) with
per-project overrides, so that the scan targets docs and nothing irrelevant.
26. As a developer, I want the check and clean steps to update their own
timestamps (`last_check`, `last_clean`) and the reminder to track
`last_reminded`, so that the lifecycle is observable and the snooze works.
27. As a developer, I want the plugin to work in any project where it's enabled
without per-project setup beyond enablement, so that adoption is frictionless.
28. As a developer, I want classifier behavior protected by golden examples and
invariants, so that future edits to the plugin don't silently regress what
counts as stale vs bloat.
29. As a developer, I want decisions (especially `confirm`-tier approvals)
recorded, so that there's an audit trail of what was changed and why.
30. As a developer, I want the deterministic scripts to emit structured JSON and
correct exit codes, so that they're independently testable and composable.
## Implementation Decisions
**Distribution & activation**
- New plugin `os-doc-hygiene` in the cc-os marketplace (built at
`plugins/os-doc-hygiene/`; see migration note above). Installed globally,
operates per-project. No global state index — the question of "how to key
projects globally" is dissolved by per-project storage.
- Marketplace registration deferred until the plugin is functional (documented
as a build-completion step, not done now).
**State & artifacts (in-project)**
- A gitignored `.dochygiene/` directory at the project root (resolved via git
root, fallback cwd) holds:
- `state.json``last_check`, `last_clean`, `last_reminded` timestamps.
- the single most-recent report (human `.md` + machine `.json`).
- Rollover: each new check deletes the prior report before writing the new one.
- `.dochygiene/` must not be tracked. The tool does **not** silently edit the
user's `.gitignore` (that would itself be an outward mutation, against the
non-intrusive premise). Instead: on first check it detects whether the dir is
ignored and, if not, surfaces a one-line offer to add the entry — applied only
on confirmation (or noted in the report). The scanner always self-excludes
`.dochygiene/` regardless.
**Components / modules** (described abstractly; see CLAUDE.md for the build map)
- **State store** (deterministic): reads/writes `state.json`; atomic writes;
resolves project root.
- **Scanner** (deterministic): walks scoped files, computes signals, emits a
candidate shortlist with attached signals. Respects scope config + ignore
markers + frozen frontmatter + append-only detection.
- **Reminder hook** (deterministic, `SessionStart`): reads state, compares to
staleness threshold, applies once/day snooze via `last_reminded`, emits a
noticeable notice. No model.
- *Mechanism (confirmed):* plugins declare hooks in `hooks/hooks.json` at the
plugin root (not under `.claude-plugin/`). The visible, zero-token notice is
emitted via the hook's JSON `systemMessage` field (a user-facing banner) —
NOT `additionalContext` (which is silent to the user). The command names the
slash command to run. `matcher` is `startup|resume`.
- *Snooze is load-bearing, not polish:* `SessionStart` also fires on `resume`,
`clear`, and `compact`. The once/day `last_reminded` snooze is what prevents
the banner re-firing after every compaction within a single working session.
- *Constraints to respect:* keep `timeout` low (≤5s); exit 0 (never block the
session); the script must be fast and side-effect-free beyond reading state.
- **Classifier / report builder** (AI — the `check` skill): consumes the
shortlist, reads candidates, assigns category + op-type + safety tier, writes
exact edits for deterministic ops, and calls the token estimator. Emits human
+ machine reports. Updates `last_check`.
- **Token estimator** (deterministic helper): counts tokens of removed/reduced
spans with a local tokenizer approximation (no API call); weights by injection
frequency; rolls up file → category → total. Framed as "context weight
reduced," splitting injected vs on-demand.
- **Cleanup executor** (the `clean` skill + deterministic patch-applier):
consumes the machine report; mtime-guards each op; applies `deterministic` ops
mechanically; delegates `generative` ops to Sonnet subagents; runs `auto`
freely and escalates `confirm`; enforces git checkpoint + single-commit;
scopable by category/file. Updates `last_clean`.
- **Sweep** (composition): invokes check then clean, preserving the report
surface and `confirm` gate.
**Operation taxonomy**
- Op-type `deterministic`: exact, reversible edits the check pre-computes
(delete/move/stamp/known-link-fix/exact-dedupe). Applied with no model.
- Op-type `generative`: prose transformations (distill, split) requiring a model
at clean time (Sonnet). Deferred — not pre-written at check time — so a check
is cheap even when no clean follows.
- Safety tier `auto`: deterministic + reversible + objective → runs without
prompt.
- Safety tier `confirm`: destructive (any delete), subjective, or generative →
escalated for approval.
**Git safety (reversion-protection vocabulary)**
- Cleanup requires a clean/committed working tree, or auto-commits a WIP
checkpoint first.
- Each cleanup run lands as one reviewable commit (the post-state).
- `confirm`-tier approvals are logged to a decisions record.
**Model routing** (per the marketplace convention)
- Deterministic scan/state/patch-apply/token-estimate: **no model** (scripts).
- Writing those scripts: **Haiku**.
- Classification / report building: **Sonnet** (reading comprehension +
judgment). The hardest stale-vs-bloat distinctions may escalate to **Opus**.
- Generative distillation at clean time: **Sonnet** (explicitly *not* Haiku —
this is the highest-judgment task and Haiku produces lossy summaries).
- Orchestration: **Opus** (the calling Claude).
**Language**
- **Python**, OOP, small composable single-responsibility classes with
dependency injection (per the deterministic-scripting reference), chosen for
broad reach in a general-purpose plugin and for testability.
**Report schema is the linchpin**
- The machine report schema (per-file: category, signals, recommended op,
op-type, safety tier, optional exact-edit, token estimate) is the contract
every other component consumes. It is designed and frozen first.
## Testing Decisions
**What makes a good test here:** assert external behavior at the highest
deterministic seam — given an input doc tree / state file / report, the script
produces the correct structured output and exit code. Do not assert internal
class structure. The AI classification layer is pinned by golden examples, not
unit assertions.
**Seams (highest first):**
- **Scanner** — input: a fixture doc tree; output: candidate shortlist JSON.
Unit-tested against fixtures exercising each signal (broken ref, version skew,
churn-vs-edit, location, append-only, frozen frontmatter, ignore file).
- **State store** — timestamp read/write, rollover (only latest report kept),
snooze logic. Unit-tested with an injected clock and temp dirs.
- **Reminder hook** — given a `state.json` + injected clock + threshold, asserts
notice / no-notice / snoozed. Unit-tested via the injected clock (no real
time, no real session).
- **Token estimator** — counts on known spans; injection-frequency weighting;
bottom-up rollup. Unit-tested on fixed inputs.
- **Patch-applier + mtime guard** — applies deterministic ops on fixtures;
skips when fixture mtime is newer than the check timestamp. Unit-tested.
- **Classifier (AI)** — golden examples (`examples/golden/`): input doc tree →
expected report categorization, per the reversion-protection pattern. Run the
check against each; flag mismatches for human review.
**Prior art:** the `commit` and `cc-architect` plugins pair deterministic
`scripts/` with skills and structured output; this plugin follows that split.
The reversion-protection reference (`cc-architect/.../reversion-protection.md`)
defines the golden-example + invariants approach used for the classifier.
## Out of Scope
- A global cross-project dashboard / index (dissolved by per-project storage).
- Exact Claude token counting via API at check time (local approximation only;
exactness isn't worth the latency/cost for a motivational estimate).
- Non-markdown content analysis beyond known doc locations (e.g. linting source
code comments). Scope defaults to docs.
- Auto-running cleanup from the `SessionEnd`/`SessionStart` hook — the hook only
ever reminds; all mutation is user-invoked.
- Pushing commits or any outbound/network action — cleanup is local commits
only; the user pushes.
- Editing files outside the resolved project root.
- v2 polish: richer per-category savings visualizations, configurable op
taxonomies beyond the built-in set.
## Further Notes
- **Build order** (the report schema gates everything): (1) machine report
schema; (2) deterministic scanner + state store + reminder hook; (3) `check`
skill (scanner → AI classify → report); (4) `clean` skill (consume report,
git-safe, scoped) + `sweep`; (5) bonus: deterministic patch-apply + token
estimator. Patch-apply and token-estimate are explicitly v2 — they optimize a
report format that must exist and be proven first.
- **Why deterministic-first matters here specifically:** the check does the
expensive cognition once (reading + deciding); cleanup executes. Pushing
attribute-detection into the scanner and exact edits into the report means
most cleanup needs no model at all, and a check is cheap even when no cleanup
follows.
- **False-positive trust risk:** without the frozen/ignore mechanism and
append-only detection, the tool would re-flag deliberately-frozen records
every week and lose the user's trust. This is treated as a correctness
requirement, not a nicety.
- **The tool must not become bloat:** report rollover (keep latest only) and the
gitignored dot dir are deliberate guards against the plugin polluting the very
repos it cleans.
- **Linchpin mechanism verified:** the SessionStart `systemMessage` banner path
was confirmed against the Claude Code hook docs before this PRD was finalized
(no other plugin in the collection wires a CC hook, so it had no precedent).
Build-spike #1 is still to stand up a trivial `hooks/hooks.json` emitting a
`systemMessage` and confirm it renders visibly in a real session before
building the deterministic core on top of it.