# Delta: lifecycle-rulebook (calibrate-assessment-inventory) ## MODIFIED Requirements ### Requirement: Rulebook Locations and Envelope 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 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 - **WHEN** the rulebook loader runs in any project - **THEN** it loads `plugins/os-doc-hygiene/rulebook.json` resolved relative to the plugin scripts directory #### Scenario: Per-project override is optional and committed - **WHEN** a project has no `.dochygiene-rules.json` at its repo root - **THEN** the loader proceeds using only the global rulebook, and when the file is present it is read as a committed, reviewable file, never from `.cc-os/` #### Scenario: Both files share one envelope shape - **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 ## ADDED Requirements ### 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