os-doc-hygiene: sync calibrate-assessment-inventory delta specs to main + archive change

Main specs updated: lifecycle-rulebook (+nominations memory, +canonical
writer-enforced ordering, envelope modified), calibrate (+intake filter,
+consult persistence, persistence-by-scope and class-never-path modified),
doc-clean (extract-then-delete extracted.md pointer). Change archived to
openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjFRroqLsvkL2WrMJtRBrK
This commit is contained in:
jared 2026-07-15 15:06:38 -04:00
parent 0d303caf43
commit 3466159f7c
10 changed files with 195 additions and 6 deletions

View File

@ -106,7 +106,13 @@ reviewed the rule report. Global-rulebook writes (writing into
`plugins/os-doc-hygiene/rulebook.json`) SHALL additionally require explicit `plugins/os-doc-hygiene/rulebook.json`) SHALL additionally require explicit
human gating, distinct from project-rule confirmation, since it is a human gating, distinct from project-rule confirmation, since it is a
cross-repo write into cc-os. Rule removals SHALL be HITL-only in all cases, cross-repo write into cc-os. Rule removals SHALL be HITL-only in all cases,
with recorded reasoning, regardless of scope. with recorded reasoning, regardless of scope. Every settled verdict SHALL
persist: judge `keep` verdicts become ordinary `lifetime: keep` rules in
`rules` (including exact-path singletons, per the keep-tier relaxation);
human declines at the rule report persist as `rejected` entries with
`rejected_by: "human"`; open `consult` verdicts persist to
`nominations.consults`, deduped by glob at write time. All persistence SHALL
go through the canonical rules-file writer.
#### Scenario: Project rule persists on judge confirmation plus report review #### Scenario: Project rule persists on judge confirmation plus report review
@ -123,6 +129,16 @@ with recorded reasoning, regardless of scope.
- **WHEN** any rule (project or global) is proposed for removal - **WHEN** any rule (project or global) is proposed for removal
- **THEN** the removal happens only via explicit human instruction, with the reasoning recorded, never as an automatic side effect of a calibration pass - **THEN** the removal happens only via explicit human instruction, with the reasoning recorded, never as an automatic side effect of a calibration pass
#### Scenario: A keep verdict persists as a plain keep rule
- **WHEN** the judge's settled verdict for a cluster is that the artifacts must be retained
- **THEN** an ordinary `lifetime: keep` rule is persisted to `rules` — removing the cluster from future calibrate pools and protecting future matches — even when the glob is an exact-path singleton
#### Scenario: A human decline persists as a rejection
- **WHEN** the human declines a judge-confirmed rule at the rule report
- **THEN** a `rejected` entry with `rejected_by: "human"` is written, so a later haiku round cannot re-nominate the identical glob+lifetime without the judge knowing
### Requirement: Retest Loop with Stop Conditions and Hard Cap ### Requirement: Retest Loop with Stop Conditions and Hard Cap
`:calibrate` SHALL re-run its clustering pass against the shrunk unmatched `:calibrate` SHALL re-run its clustering pass against the shrunk unmatched
@ -166,7 +182,15 @@ recurring by convention (e.g. `PRD.md`, `HANDOFF-*.md`,
or bare timestamp unique to one instance is not acceptable. A rule that or bare timestamp unique to one instance is not acceptable. A rule that
currently matches only one file is acceptable; a rule that can, by currently matches only one file is acceptable; a rule that can, by
construction, only ever match one file is a failed generalization and SHALL construction, only ever match one file is a failed generalization and SHALL
be flagged loudly rather than silently persisted. be flagged loudly rather than silently persisted. **Keep-tier relaxation:**
exact-path/instance globs ARE acceptable for `lifetime: keep` entries only —
this test exists to prevent bad deletion rules, and a singleton keep merely
protects; instance globs remain forbidden for `temporary` and
`delete-once-served`. When the near-miss boundary check reveals sibling
artifacts a glob misses, the fix SHALL be to enumerate the conventional
prefixes as separate rule entries, never to widen the glob to the containing
directory; container-claiming globs are justified ONLY when the directory is
wholly machine-owned (e.g. `plugins/*/.pytest_cache/`).
#### Scenario: A convention-recurring name is acceptable #### Scenario: A convention-recurring name is acceptable
@ -183,6 +207,16 @@ be flagged loudly rather than silently persisted.
- **WHEN** a proposed rule currently matches exactly one file - **WHEN** a proposed rule currently matches exactly one file
- **THEN** it is acceptable if the glob's structure could match future similarly-named files; it is flagged as a failed generalization if the glob's structure can never match any file but the one it names today - **THEN** it is acceptable if the glob's structure could match future similarly-named files; it is flagged as a failed generalization if the glob's structure can never match any file but the one it names today
#### Scenario: A singleton keep passes under the keep-tier relaxation
- **WHEN** a proposed rule is `docs/research/clutter-pattern-inventory.md -> keep`
- **THEN** it passes despite being an exact-path instance glob, because the keep tier only protects; the same glob with lifetime `temporary` or `delete-once-served` fails
#### Scenario: Missed siblings are enumerated, never widened to the container
- **WHEN** the boundary check shows `autoresearch/classic-*/` misses sibling runs under `autoresearch/improve-*/`
- **THEN** the fix is a second rule entry `autoresearch/improve-*/` (same cluster), never a widening to `autoresearch/*/` or `autoresearch/**`, so keep-worthy content can still live in the container without a counter-rule
### Requirement: Rule-Quality Tie-Breaker — Prefer the Narrower Glob ### Requirement: Rule-Quality Tie-Breaker — Prefer the Narrower Glob
When choosing between candidate globs of differing breadth for the same When choosing between candidate globs of differing breadth for the same
@ -244,3 +278,65 @@ the finding direction).
- **WHEN** a calibration pass persists zero rules - **WHEN** a calibration pass persists zero rules
- **THEN** it fails the recall floor and therefore cannot pass - **THEN** it fails the recall floor and therefore cannot pass
### Requirement: Deterministic Nomination Intake Filter
Between cheap-model nomination and strong-model judgment, `:calibrate` SHALL
run a deterministic `NominationIntakeFilter` (in `calibrate_helpers.py`, no
model — invariant #6). A nomination whose glob+lifetime exactly equals a
`rejected` entry SHALL be dropped before the judge and logged in the run
summary. Surviving nominations SHALL be annotated with every related
rejection, where related means the two globs' match sets intersect on the
current shortlist (deterministic, computed from the scan). The annotations
plus all open consults SHALL enter the judge prompt as its "Nominations
memory" input section; related rejections are context for the judge, never a
veto.
#### Scenario: Exact glob+lifetime repeat is dropped before the judge
- **WHEN** haiku nominates `docs/research/** -> temporary` and `nominations.rejected` contains an entry with glob `docs/research/**` and lifetime `temporary`
- **THEN** the nomination is dropped at intake, never reaches the judge, and the drop is logged in the run summary
#### Scenario: A variant flows through annotated, not blocked
- **WHEN** haiku nominates `docs/research/drafts/** -> temporary` and `nominations.rejected` contains `docs/research/** -> temporary`, and the two globs' match sets intersect on the current shortlist
- **THEN** the nomination proceeds to the judge carrying the related rejection (its why and consider_instead) as context, and the judge may still confirm it
#### Scenario: Open consults always reach the judge prompt
- **WHEN** `nominations.consults` is non-empty at intake time
- **THEN** every open consult is included in the judge prompt's "Nominations memory" section, regardless of what haiku nominated this round
### Requirement: Consult Persistence and Resurfacing
Open `consult` verdicts SHALL persist to `nominations.consults` (deduped by
glob at write time) rather than dying with the run. Consults SHALL resurface
in `:calibrate` only — `:check` and `:clean` are unchanged — appearing in the
judge prompt and as an "Open consults" section of the rule report. A consult
SHALL exit in exactly one of three ways: (a) a human answer settles the
purpose — a normal rule is persisted and the consult entry deleted; (b) the
human deems it not rule-worthy — the entry is rewritten into `rejected` with
`rejected_by: "human"` and the human's why; (c) the human defers — the entry
stays and resurfaces next run. New rejections and consults SHALL appear in
the rule report but are not individually gated — they are memory, not
deletion authority.
#### Scenario: A consult survives the run and resurfaces
- **WHEN** a judge verdict is `consult` and the run ends without a human answer
- **THEN** the consult is written to `nominations.consults`, and the next `:calibrate` run surfaces it in both the judge prompt and the report's "Open consults" section
#### Scenario: An answered consult becomes a rule and disappears
- **WHEN** the human answers an open consult in a way that settles the artifact's purpose
- **THEN** a normal rule is persisted through the standard report flow and the consult entry is deleted in the same write
#### Scenario: A declined consult becomes a human rejection
- **WHEN** the human answers that an open consult's artifact class is not rule-worthy
- **THEN** the consult entry is rewritten into `nominations.rejected` with `rejected_by: "human"` and the stated reason
#### Scenario: Consults never surface outside calibrate
- **WHEN** `:check` or `:clean` runs against a project with open consults
- **THEN** their behavior is unchanged — consults neither appear in output nor affect any classification

View File

@ -211,7 +211,16 @@ writing into an ADR/CLAUDE.md/docs target, or cross-repo via
succeeded; both steps SHALL land in the same single hygiene commit, or, on succeeded; both steps SHALL land in the same single hygiene commit, or, on
extraction failure, neither SHALL be applied for that entry (skip, not a extraction failure, neither SHALL be applied for that entry (skip, not a
run-level hard failure, unless the failure matches an existing hard-failure run-level hard failure, unless the failure matches an existing hard-failure
trigger). trigger). When the extraction destination is the vault (the content
physically leaves the repo), the op SHALL additionally append a pointer
entry to the deleted file's per-directory `extracted.md` index (creating the
file if absent), in the same atomic sequence — distill → `/os-vault:write`
append the pointer line → `git rm` — all staged into the same single hygiene
commit, so there is no window where the doc is gone but undiscoverable. The
pointer entry SHALL name the vault note, state why a future reader would
follow it, and record the source filename and date. Repo-durable extraction
targets (ADR, CLAUDE.md, docs) SHALL NOT produce an index entry — they are
already discoverable in-repo.
#### Scenario: delete performs a true git rm at apply time #### Scenario: delete performs a true git rm at apply time
@ -233,6 +242,21 @@ trigger).
- **WHEN** the applier applies a `delete` entry whose path is a directory-rule aggregate entry - **WHEN** the applier applies a `delete` entry whose path is a directory-rule aggregate entry
- **THEN** it performs a recursive `git rm` removing the entire matched directory in one operation staged into the single hygiene commit - **THEN** it performs a recursive `git rm` removing the entire matched directory in one operation staged into the single hygiene commit
#### Scenario: A vault extraction leaves an extracted.md pointer in the same commit
- **WHEN** an `extract-then-delete` entry extracts to the vault via `/os-vault:write`
- **THEN** a pointer entry naming the vault note, the reason to follow it, the source filename, and the date is appended to the deleted file's directory `extracted.md` (created if absent), and the append, the deletion, and the index file are all staged into the same single hygiene commit
#### Scenario: A repo-durable extraction leaves no index entry
- **WHEN** an `extract-then-delete` entry extracts into an ADR, CLAUDE.md, or docs target inside the repo
- **THEN** no `extracted.md` entry is written — the residue is already discoverable in-repo
#### Scenario: A failed pointer append skips the delete
- **WHEN** the vault write succeeds but appending the `extracted.md` pointer fails
- **THEN** the `git rm` is not applied for that entry and it is reported as skipped, preserving the invariant that a doc is never gone but undiscoverable
### Requirement: Clean Skill Orchestration ### Requirement: Clean Skill Orchestration
The `clean` skill SHALL load the current report via `StateStore.read_report` The `clean` skill SHALL load the current report via `StateStore.read_report`

View File

@ -17,9 +17,11 @@ The plugin SHALL ship a global rulebook at
`plugins/os-doc-hygiene/rulebook.json`, resolved relative to plugin scripts, `plugins/os-doc-hygiene/rulebook.json`, resolved relative to plugin scripts,
present in every project. A project MAY additionally provide a committed present in every project. A project MAY additionally provide a committed
repo-root `.dochygiene-rules.json` override. Both files SHALL use the repo-root `.dochygiene-rules.json` override. Both files SHALL use the
envelope `{"schema_version": 1, "rules": [...]}`. The per-project override envelope `{"schema_version": 1, "rules": [...]}`. The project file MAY
SHALL NOT live under gitignored `.cc-os/` — it SHALL be a committed, additionally carry a top-level `nominations` key (see the Nominations Memory
reviewable dotfile. requirement); the loader SHALL ignore unknown top-level keys, so the key is
additive to `schema_version` 1. The per-project override SHALL NOT live under
gitignored `.cc-os/` — it SHALL be a committed, reviewable dotfile.
#### Scenario: Global rulebook is always present #### Scenario: Global rulebook is always present
@ -36,6 +38,11 @@ reviewable dotfile.
- **WHEN** either the global rulebook or a project override is loaded - **WHEN** either the global rulebook or a project override is loaded
- **THEN** it is validated against the envelope `{"schema_version": 1, "rules": [...]}` - **THEN** it is validated against the envelope `{"schema_version": 1, "rules": [...]}`
#### Scenario: A nominations key does not disturb the v1 loader
- **WHEN** a project `.dochygiene-rules.json` carries a top-level `nominations` key alongside `rules`
- **THEN** `rulebook.py` loads the `rules` array exactly as before, ignoring the unknown top-level key without warning or error
### Requirement: Glob Dialect Is glob.translate ### Requirement: Glob Dialect Is glob.translate
Rule `glob` patterns SHALL be compiled using stdlib Rule `glob` patterns SHALL be compiled using stdlib
@ -222,3 +229,65 @@ other signal.
- **WHEN** `HANDOFF-2026-07-01.md` matches a file-rule with `lifetime: delete-once-served` and also has an existing broken-reference signal - **WHEN** `HANDOFF-2026-07-01.md` matches a file-rule with `lifetime: delete-once-served` and also has an existing broken-reference signal
- **THEN** its shortlist entry carries both the lifecycle signal and the pre-existing broken-reference signal - **THEN** its shortlist entry carries both the lifecycle signal and the pre-existing broken-reference signal
### Requirement: Nominations Memory Lives in the Project Rules File
The project `.dochygiene-rules.json` MAY carry a top-level `nominations` key
holding exactly two lists: `consults` (open questions — entries with `glob`,
`question`, `evidence`, `cluster_key`, `asked_on`, and deliberately NO
lifetime) and `rejected` (settled "no" answers — entries with `glob`,
`lifetime`, `why`, optional `consider_instead`, `rejected_by` (`"judge"` or
`"human"`), `judged_on`). The `nominations` key SHALL never affect which
files the rulebook governs — only entries in `rules` decide that.
`rulebook.py` SHALL remain nomination-unaware; only the calibrate helpers
read the key, and the calibrate reader SHALL warn on unrecognized nomination
fields, mirroring the rules array's unknown-field discipline. Rejected
entries and exact-path singleton keep rules SHALL exit only by hand-deletion
(removals stay HITL with recorded reasoning); no automated revisit path
SHALL exist.
#### Scenario: Nominations never filter files
- **WHEN** the scanner or rulebook resolves the governing rule for a path that only a `nominations` entry's glob matches
- **THEN** the path is treated as unmatched/unmanaged — nominations carry no lifecycle authority
#### Scenario: Consult entries carry no lifetime
- **WHEN** a consult entry is written to `nominations.consults`
- **THEN** it records `glob`, `question`, `evidence`, `cluster_key`, `asked_on` and no lifetime field — presence in the list means open, with no status field
#### Scenario: Unrecognized nomination fields warn in the calibrate reader
- **WHEN** the calibrate helpers read a nominations entry containing an unknown field
- **THEN** a warning is emitted and the field is preserved, never silently dropped
#### Scenario: A rejection leaves only by hand-deletion
- **WHEN** a calibration pass runs against a rules file containing a stale rejection
- **THEN** no automated path removes or expires the entry; it is removed only by explicit human edit
### Requirement: Canonical Writer-Enforced Ordering
Every code path that serializes `.dochygiene-rules.json` SHALL write through
one canonical writer that emits: `rules` grouped by lifetime tier in the
order delete-once-served, temporary, keep, glob-sorted within each group;
`nominations` after `rules`; `consults` before `rejected`, each glob-sorted.
The writer SHALL be idempotent (canonicalizing an already-canonical file is a
no-op) and SHALL round-trip unknown fields with a warning rather than
dropping them. Ordering SHALL NOT be enforced by any hook; hand edits
re-canonicalize on the next write.
#### Scenario: Writes are grouped and sorted canonically
- **WHEN** the writer serializes a rules file containing rules of all three tiers plus nominations
- **THEN** the output orders rules delete-once-served → temporary → keep with globs sorted within each group, and nominations follows rules with consults before rejected, each list glob-sorted
#### Scenario: Canonicalization is idempotent
- **WHEN** the writer serializes a file it previously wrote, unchanged
- **THEN** the output is byte-identical
#### Scenario: A hand-edited file re-canonicalizes on the next write
- **WHEN** a human appends a rule out of tier order and a later calibrate run persists a new entry
- **THEN** the whole file is rewritten in canonical order in that write, with no hook involved in the interim