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:
parent
0d303caf43
commit
3466159f7c
|
|
@ -106,7 +106,13 @@ reviewed the rule report. Global-rulebook writes (writing into
|
|||
`plugins/os-doc-hygiene/rulebook.json`) SHALL additionally require explicit
|
||||
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,
|
||||
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
|
||||
|
||||
|
|
@ -123,6 +129,16 @@ with recorded reasoning, regardless of scope.
|
|||
- **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
|
||||
|
||||
#### 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
|
||||
|
||||
`: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
|
||||
currently matches only one file is acceptable; a rule that can, by
|
||||
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
|
||||
|
||||
|
|
@ -183,6 +207,16 @@ be flagged loudly rather than silently persisted.
|
|||
- **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
|
||||
|
||||
#### 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
|
||||
|
||||
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
|
||||
- **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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
extraction failure, neither SHALL be applied for that entry (skip, not a
|
||||
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
|
||||
|
||||
|
|
@ -233,6 +242,21 @@ trigger).
|
|||
- **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
|
||||
|
||||
#### 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
|
||||
|
||||
The `clean` skill SHALL load the current report via `StateStore.read_report`
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ The plugin SHALL ship a global rulebook at
|
|||
`plugins/os-doc-hygiene/rulebook.json`, resolved relative to plugin scripts,
|
||||
present in every project. A project MAY additionally provide a committed
|
||||
repo-root `.dochygiene-rules.json` override. Both files SHALL use the
|
||||
envelope `{"schema_version": 1, "rules": [...]}`. The per-project override
|
||||
SHALL NOT live under gitignored `.cc-os/` — it SHALL be a committed,
|
||||
reviewable dotfile.
|
||||
envelope `{"schema_version": 1, "rules": [...]}`. The project file MAY
|
||||
additionally carry a top-level `nominations` key (see the Nominations Memory
|
||||
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
|
||||
|
||||
|
|
@ -36,6 +38,11 @@ reviewable dotfile.
|
|||
- **WHEN** either the global rulebook or a project override is loaded
|
||||
- **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
|
||||
|
||||
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
|
||||
- **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
|
||||
|
|
|
|||
Loading…
Reference in New Issue