cc-os/plugins/os-doc-hygiene/lifecycle-spec.md

301 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Lifecycle-aware doc hygiene — spec
_Status: Locked design (wayfinder map [#31](https://forgejo.swansoncloud.com/jared/cc-os/issues/31)); assembled 2026-07-14 from decision tickets #32#48._
_Last updated: 2026-07-14_
Extends os-doc-hygiene from stale/bloat monitoring to lifecycle management: a
rulebook assigns every managed file a **lifetime**, `:check`/`:clean` gain
delete and extract-then-delete behavior, and a new `:calibrate` skill learns
rules per project. Validated against calibration project #1 (cc-os) per the
criteria in [Calibration](#calibration-pass-1-cc-os).
ADR set: [ADR-0038](../../docs/adr/) (rulebook location), ADR-0039 (deletion
autonomy tiers), ADR-0040 (no ignore-surface propagation), ADR-0041
(determinism-promotion principle).
## 1. Lifetime taxonomy (#33)
Three lifetimes, plus one modifier:
- **keep** — indefinite; scanned and reported, never deleted.
- **temporary** — age-triggered deletion (see [Temporary tier](#4-temporary-tier-43-48)).
- **delete-once-served** — purpose-triggered deletion (see [Served signals](#5-delete-once-served-served-signal-split-43)).
- **extract** (modifier on deletion, not a fourth lifetime) — distill durable
content before deleting. Extraction reuses existing knowledge routing (#36):
repo-durable residue → ADR/CLAUDE.md/docs; cross-repo lessons → SecondBrain
via `/os-vault:write`. No new destinations, no "retired specs" pile.
Distinct from all lifetimes: an **IGNORE surface** — paths the scanner never
walks at all (vs `keep` = walked and reported, never deleted). Seed members:
`graphify-out/**`, `.dochygiene/**`. The ignore surface is an explicit list,
never inferred from `.gitignore` (gitignored ≠ deletable AND gitignored ≠
keepable — #43).
## 2. Rulebook (#34, #38, #40, #44)
### Locations
- **Global:** `plugins/os-doc-hygiene/rulebook.json`, resolved relative to
plugin scripts. Ships everywhere.
- **Per-project override:** repo-root `.dochygiene-rules.json`, committed
(matches the `.dochygiene-ignore` precedent; deliberately NOT in gitignored
`.cc-os/` — ADR-0038).
Both use envelope `{"schema_version": 1, "rules": [...]}`.
### Glob dialect
gitignore-style via stdlib `glob.translate(recursive=True,
include_hidden=True)` (Python ≥ 3.13); patterns repo-root-relative, real `**`;
compiled once at load. Directory rules = patterns covering a subtree.
### Precedence (two-axis, source first)
project file-rule > project directory-rule > global file-rule > global
directory-rule; ties broken by longest pattern, then last-defined. File-level
rules override directory rules (#38). The merge is **add-only**: a project
neutralizes a global rule by shadowing it with `lifetime: "keep"` — there is
no rule-removal mechanism.
### Per-rule fields
```json
{
"glob": "autoresearch/*/**",
"lifetime": "keep|temporary|delete-once-served",
"extract": true,
"served_when": "free text — hint consumed by the LLM classifier, no trigger DSL",
"served_when_path": "openspec/changes/archive/{id}/",
"retain_recent": 3,
"max_age_days": 3,
"confirm": true,
"confirmed_by": "human | <strong-model-id>",
"confirmed_on": "2026-07-14",
"source": "clutter-inventory #41",
"note": "free-text rationale"
}
```
- `retain_recent` default 3; `max_age_days` default **3** (not
null-means-forever) — #43 amendments to the #40 schema.
- `served_when_path` is the deterministic sibling of `served_when` (#43).
- `confirm: true` is an optional always-confirm escape hatch — **human-settable
only**; a model-proposed rule may never set it, only ask a human to (#42/#43).
- **There is no `propagate_ignore` field.** It was reserved in #40 and dropped
by #44: a lifecycle rule never writes into another tool's ignore/config
surface (ADR-0040).
- Unmatched files get NO lifetime — they flow through existing signals
unchanged and become `:calibrate` candidates (**unmatched = unmanaged**).
### Scanner consumption
New stdlib `rulebook.py` loader. A directory-rule match **prunes the walk**
(files beneath are never opened; one aggregate entry) — this also implements
the IGNORE surface for free. A file-rule match attaches a **lifecycle signal**
(rule ref + lifetime + served_when) consumed by the classifier as a new signal
class (#37).
### Validation
Skip-and-warn per invalid/unconfirmed rule — a rule missing `confirmed_by`
never acts (#39) but doesn't take scanning down. Hard-fail on unparseable JSON
or unknown `schema_version`.
`.dochygiene-ignore` remains a hand-authored, human-only escape hatch,
unmanaged by the rulebook (#44).
## 3. Deletion semantics and autonomy tiers (#35, #43 — ADR-0039)
Delete = **true git deletion in a dedicated hygiene commit**; git history is
the archive. No `archive/` dirs or graveyard branches (relocated clutter still
distracts AI/search).
Rule-backed deletes are **auto** — a rule confirmed per #39 *is* the standing
consent; no per-run prompt. The auto/confirm line is drawn on **evidence
quality + recoverability**, not file type:
| Case | Behavior |
|---|---|
| IGNORE surface (`graphify-out/**`, `.dochygiene/**`) | never walked |
| lifetime `keep` | scanned + reported, never deleted |
| tracked + delete rule + clean worktree | **auto** |
| tracked + delete rule + DIRTY | **confirm** (an uncommitted diff dies with the file) |
| untracked + delete rule | **confirm** (no history to recover from) |
| no rule match | unmanaged; existing signals only, never deleted |
`clean` verifies tracked+clean **at runtime** (`git ls-files` + dirty check);
it never trusts the rule's word for it. No `recoverable_via` field, no
"regenerate" class.
## 4. Temporary tier (#43, #48)
**retain-recent-N + age, not age alone.** Defaults `retain_recent: 3`,
`max_age_days: 3` (both per-rule overridable). The newest 3 entries matching a
rule are always kept regardless of age (they show current trajectory); an
entry ranked 4th or older is deleted once it exceeds `max_age_days`. 90-day
windows were rejected as far too slow.
**Retention unit = the rule's match entry**: a file for file rules, a run
*directory* for directory rules ("3 most recent autoresearch runs", not "3
most recent files inside one run").
**Age = git commit time, falling back to filesystem mtime for untracked
files.** mtime-alone was rejected (clone/branch-switch resets every mtime,
putting the rulebook to sleep on fresh checkouts); the objection doesn't apply
to the fallback, since untracked files don't exist in a fresh clone. No
per-rule `age_source` field (#48).
**Untracked directory entries** use the directory inode's own mtime (one
stat), NOT a recursive max-mtime walk — the tier's failure mode is
self-healing (a spuriously bumped mtime merely delays deletion one round), so
the cheap signal suffices (#48).
## 5. delete-once-served: served-signal split (#43)
Two slots, split by evidence quality:
```json
{ "glob": "openspec/changes/*/", "lifetime": "delete-once-served",
"served_when_path": "openspec/changes/archive/{id}/" }
```
Scanner **proves** the served condition → may delete silently under the tier
matrix.
```json
{ "glob": "docs/plans/*.md", "lifetime": "delete-once-served",
"served_when": "the effort this plan describes has shipped" }
```
Classifier **judges** the condition → **always forced to confirm**, regardless
of tracked status. The LLM may propose; it may never silently destroy on a
hunch.
Example of the boundary: `autoresearch/*/` can be auto (a concluded run is
provable from the filesystem); `PRD.md` cannot (purpose-triggered — "did this
ship?" is a judgment; and it is NOT temporary, since age-keying would delete
the PRD of a feature not yet built — #45 Addendum 2).
## 6. Determinism promotion (#43 item 7, #47 — ADR-0041)
**Design principle: hygiene drives projects toward structurally-obvious
maintenance.** When a rule's served signal is subjective (classifier-judged),
the tool does not merely downgrade it to confirm — it names the subjectivity
and recommends a concrete structural convention that would graduate the rule
to `served_when_path` and make it silent. Confirm-fatigue is the incentive to
fix the convention.
### Completion-conventions catalog
`plugins/os-doc-hygiene/conventions.json` — global-only, machine-readable so
the deterministic pipeline can emit nudges without an LLM. No per-project
override: the catalog only recommends; adoption lands in the project's own
rulebook. Each entry: name, what it proves, the `served_when_path` /
frontmatter template a rule graduates to, and a one-line human pitch.
v1 contents — exactly two conventions:
- **archive-bucket** — "done" = the file moved into a sibling `archive/` dir
(`docs/plans/x.md` → `docs/plans/archive/x.md`); graduates a rule to
`served_when_path: <dir>/archive/{name}`. Precedent: openspec changes.
- **status-frontmatter** — "done" = a `status: shipped|done` frontmatter key;
file stays put; scanner reads frontmatter.
Successor-artifact checks stay in the fog until a calibration pass demands one.
### Nudge surfacing (split by capability)
- **`:check`** names promotion candidates in every report (deterministic,
recurring — the report gains a promotion-candidates section).
- **`:calibrate`** may go further and DRAFT the adoption — the graduated rule
plus the file moves — for human approval. It proposes, never applies unasked.
## 7. Pipeline integration (#37)
Lifecycle categorization is a **new signal class** in the existing
scanner/classifier pipeline; `delete` and `extract-then-delete` are **new op
types** in the clean report schema. The only new skill is
**`os-doc-hygiene:calibrate`**.
## 8. The :calibrate protocol (#42, #39, #45)
The learn-new-rules loop, run per project:
1. **Cluster-and-sample** over unmatched files (unmatched = unmanaged = the
candidate pool). Clustering exists precisely so rules are written over the
cluster, not one member.
2. **Nomination (cheap model):** haiku nominates a bare glob + lifetime per
cluster — constrained to produce *patterns*, never exact-instance globs.
3. **Judgment (strong model):** one batched Opus/Fable judge gathers its own
evidence and authors final rule entries (#39: weak-model discoveries need
strong-model confirmation). Verdicts: **confirm / reject / amend /
consult** — consult is mandatory when an artifact's purpose is unclear
(regenerable ≠ removable).
4. **Rule report to the human — before any rule is persisted.** Per proposed
rule, the report shows:
1. the **glob verbatim**, exactly as it would be persisted;
2. every path it currently matches (or a capped sample + total count);
3. **the boundary — near-miss paths it does NOT match** (this caught a
real bug during #45: `autoresearch/classic-*/` silently missing
`autoresearch/improve-260710-1057/`);
4. lifetime + behavior tier (auto vs confirm);
5. a plain-language why — what the artifact is and why it's clutter.
The human reviews patterns and examples, not JSON schema.
5. **Persistence:** project rules land on judge confirmation;
**global-rulebook writes are human-gated** (a cross-repo write into cc-os).
Rule removals are HITL-only, with recorded reasoning.
6. **Retest loop:** stop at <2 new rules OR <10% unmatched shrink; hard cap 3
rounds.
Seed intake: the #41 clutter-inventory seed candidates enter at judge intake
**full intake for every run after calibration pass #1** (see the one-off
carve-out below).
### Rule-quality tests the report enforces
- **The rule is the CLASS, never the PATH.** A glob may hardcode a name that
*recurs by convention* (`PRD.md`, `HANDOFF-*.md`, `migration-report.md`); it
may NOT hardcode an identifier *unique to one instance* (a run-id, hash, or
bare timestamp). A rule matching one file today is fine; a rule that can
only EVER match one file is a failed generalization flag loudly, never
silently persist.
- **Glob-breadth tie-breaker: prefer the NARROWER glob.** Too-narrow fails
safe (leaves clutter; the recurring pass catches it next round self-
healing). Too-broad fails dangerous (deletes a keeper not self-healing).
Readability beats cleverness when the cost is one missed round.
## 9. Calibration pass #1: cc-os (#45)
Project: **cc-os** proves the protocol *works* before testing whether it
*generalizes*; the self-referential risk is accepted and paid for by the
validation criteria carrying the weight.
- **Precision hard gate:** the pass FAILS if any rule persisted to the
rulebook has a glob matching a protected path regardless of behavior tier
(a confirm gate is a safety property of the human sitting there, not of the
protocol). Exploration-time consult verdicts on protected paths are FREE.
Protected set (fixed before the pass, human-edited, never revised after):
eval `scenarios/`/`scenarios-reserve/`/`fixture/`/judge-rubric.md;
`openspec/specs/`; `docs/adr/**`; mirrored `.claude/`/`.codex/`/`.pi/` skill
dirs; `CLAUDE.md`; plugin source.
- **Recall floor: 8 of the 10 cc-os rows of the #41 inventory**, with 4
mandatory (missing any fails the pass): `autoresearch/<run-id>/`,
`HANDOFF-*.md`, `docs/adr/migration-report.md`,
`.dochygiene/report.{json,md}`. `graphify-out/` is **void, not a miss** (it
is IGNORE surface per #43). The recall floor is a grading bar, not runtime
behavior err-toward-keeping comes from the hard gate + consult.
- **Seed hold-out (one-off for pass #1 ONLY):** the cc-os rows of #41 are the
sealed answer key and are withheld from judge intake otherwise the pass is
an open-book exam. This deliberately deviates from #42; **every later run
uses full seed intake.** Do not mistake the carve-out for a permanent
property.
- Novel matches beyond the answer key are expected and human-spot-checked
a wrong novel match triggers rule adjustment + a retest round, not failure.
- A do-nothing pass cannot pass: the recall floor makes the pass falsifiable
in the finding direction.
## Out of scope for this design
Shipping the recurring cross-project categorize-and-learn skill (charted as
fog on map #31). Ignore-surface propagation into other tools' config
(rejected ADR-0040); if disposable files polluting the knowledge graph later
proves painful, the fix belongs to graphify or os-vault:onboard-project.