os-doc-hygiene: sync lifecycle delta specs to main + archive change
Main specs gain calibrate, determinism-promotion, lifecycle-deletion, lifecycle-rulebook (new) and rulebook-aware updates to doc-check, doc-clean, report-schema. Change archived to openspec/changes/archive/2026-07-15-lifecycle-aware-doc-hygiene. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAw7YxP71Fk6K9AHNaYPj2
This commit is contained in:
parent
7b9afb1f6a
commit
fe04297604
|
|
@ -0,0 +1,246 @@
|
|||
# Spec: calibrate
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the `:calibrate` skill, which proposes new lifecycle rulebook rules by
|
||||
clustering the pool of files the rulebook currently leaves unmatched, having a
|
||||
cheap model nominate generalizable glob patterns, and having a strong model
|
||||
judge and report those nominations to a human before any persistence.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Calibrate Clusters and Samples Over Unmatched Files
|
||||
|
||||
The `:calibrate` skill SHALL run over the unmatched-files pool (unmatched =
|
||||
unmanaged, per `lifecycle-rulebook`) as its candidate pool. It SHALL cluster
|
||||
unmatched paths by shape before nominating any rule, so that a proposed rule
|
||||
is authored against a cluster of similar paths rather than a single file.
|
||||
|
||||
#### Scenario: Calibrate operates only on the unmatched pool
|
||||
|
||||
- **WHEN** `:calibrate` runs
|
||||
- **THEN** its candidate pool is exactly the set of files the rulebook currently leaves unmatched
|
||||
|
||||
#### Scenario: Rules are proposed against clusters, not single files
|
||||
|
||||
- **WHEN** `:calibrate` nominates a candidate rule
|
||||
- **THEN** the nomination is derived from a cluster of similar unmatched paths, not from one instance in isolation
|
||||
|
||||
### Requirement: Cheap-Model Nomination Produces Patterns, Never Exact-Instance Globs
|
||||
|
||||
For each sampled cluster, `:calibrate` SHALL dispatch a haiku subagent
|
||||
constrained to nominate a bare glob pattern plus a candidate lifetime. The
|
||||
haiku nomination SHALL be constrained to produce generalizable patterns; it
|
||||
SHALL NOT be accepted as final if it hardcodes an identifier unique to a
|
||||
single instance (a run-id, hash, or bare timestamp).
|
||||
|
||||
#### Scenario: Haiku nominates a glob and lifetime per cluster
|
||||
|
||||
- **WHEN** a cluster of unmatched paths is sampled
|
||||
- **THEN** the haiku subagent returns a bare glob pattern and a candidate lifetime for that cluster
|
||||
|
||||
#### Scenario: Instance-unique nominations are not accepted as final
|
||||
|
||||
- **WHEN** a haiku nomination's glob hardcodes a run-id, hash, or bare timestamp unique to one file
|
||||
- **THEN** it is not persisted as-is; it must be caught and generalized before the strong-model judgment step, per the rule-quality tests below
|
||||
|
||||
### Requirement: Strong-Model Batched Judgment with Confirm/Reject/Amend/Consult
|
||||
|
||||
`:calibrate` SHALL dispatch one batched strong-model (Opus or Fable) judge
|
||||
subagent that gathers its own evidence (re-reading matched paths and
|
||||
checking near-miss boundaries) and authors final rule entries. The judge
|
||||
SHALL return one of four verdicts per nominated rule: `confirm`, `reject`,
|
||||
`amend`, or `consult`. `consult` SHALL be mandatory whenever an artifact's
|
||||
purpose is unclear (i.e., the judge cannot determine whether the artifact is
|
||||
regenerable or must be retained).
|
||||
|
||||
#### Scenario: Judge gathers its own evidence rather than trusting the nomination
|
||||
|
||||
- **WHEN** the strong-model judge evaluates a haiku nomination
|
||||
- **THEN** it independently re-reads matched paths and checks near-miss boundaries rather than accepting the nomination's claims at face value
|
||||
|
||||
#### Scenario: Four verdicts are the only possible outcomes
|
||||
|
||||
- **WHEN** the judge evaluates a nominated rule
|
||||
- **THEN** its verdict is exactly one of `confirm`, `reject`, `amend`, or `consult`
|
||||
|
||||
#### Scenario: Consult is mandatory when purpose is unclear
|
||||
|
||||
- **WHEN** the judge cannot determine whether a clustered artifact type is regenerable or must be retained
|
||||
- **THEN** the verdict is `consult`, never `confirm` or `reject`
|
||||
|
||||
### Requirement: Rule Report to the Human Before Persistence
|
||||
|
||||
Before any proposed rule is persisted, `:calibrate` SHALL present a rule
|
||||
report to the human containing, per proposed rule: the glob verbatim exactly
|
||||
as it would be persisted; every path it currently matches (or a capped
|
||||
sample plus a total count); the near-miss boundary — paths that do NOT match
|
||||
despite looking similar; the lifetime and behavior tier (auto vs confirm);
|
||||
and a plain-language explanation of what the artifact is and why it is
|
||||
clutter. No rule SHALL be persisted before this report has been shown.
|
||||
|
||||
#### Scenario: The report shows the exact persisted glob
|
||||
|
||||
- **WHEN** a rule report is generated for a proposed rule
|
||||
- **THEN** the glob shown is character-for-character identical to what would be written to the rulebook file
|
||||
|
||||
#### Scenario: The report shows current matches with a capped sample
|
||||
|
||||
- **WHEN** a proposed rule matches more paths than the display cap
|
||||
- **THEN** the report shows a capped sample of matched paths plus the total count of all matches
|
||||
|
||||
#### Scenario: The report shows the near-miss boundary
|
||||
|
||||
- **WHEN** a proposed rule's glob narrowly excludes similar-looking paths
|
||||
- **THEN** the report explicitly lists those near-miss non-matching paths, so a boundary bug (e.g. a glob silently missing a sibling path) is visible before persistence
|
||||
|
||||
#### Scenario: No rule is persisted before the report is shown
|
||||
|
||||
- **WHEN** `:calibrate` has generated proposed rules
|
||||
- **THEN** it does not write any rule to a rulebook file until the human has seen the rule report for it
|
||||
|
||||
### Requirement: Persistence Rules by Scope
|
||||
|
||||
Project-rulebook writes SHALL land on judge confirmation once the human has
|
||||
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.
|
||||
|
||||
#### Scenario: Project rule persists on judge confirmation plus report review
|
||||
|
||||
- **WHEN** the judge verdict is `confirm` for a project-scoped rule and the human has reviewed its rule report
|
||||
- **THEN** the rule is written to the project's `.dochygiene-rules.json`
|
||||
|
||||
#### Scenario: Global rulebook writes require an additional explicit gate
|
||||
|
||||
- **WHEN** a proposed rule would be written to the global `rulebook.json`
|
||||
- **THEN** a distinct human confirmation for the cross-repo write is required, beyond the project-rule confirmation step
|
||||
|
||||
#### Scenario: Rule removal is always HITL-only
|
||||
|
||||
- **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
|
||||
|
||||
### Requirement: Retest Loop with Stop Conditions and Hard Cap
|
||||
|
||||
`:calibrate` SHALL re-run its clustering pass against the shrunk unmatched
|
||||
pool after each round of persisted rules. It SHALL stop when a round yields
|
||||
fewer than 2 new rules OR shrinks the unmatched pool by less than 10%. It
|
||||
SHALL hard-cap at 3 rounds regardless of shrink rate.
|
||||
|
||||
#### Scenario: Stops on fewer than 2 new rules
|
||||
|
||||
- **WHEN** a retest round yields only 1 new confirmed rule
|
||||
- **THEN** the retest loop stops after that round
|
||||
|
||||
#### Scenario: Stops on less than 10% shrink
|
||||
|
||||
- **WHEN** a retest round shrinks the unmatched pool by less than 10%
|
||||
- **THEN** the retest loop stops after that round, even if 2 or more rules were confirmed
|
||||
|
||||
#### Scenario: Hard cap of 3 rounds regardless of shrink
|
||||
|
||||
- **WHEN** three retest rounds have run and each still meets the continuation criteria (≥2 new rules and ≥10% shrink)
|
||||
- **THEN** the retest loop stops after the third round regardless
|
||||
|
||||
### Requirement: Seed Intake at Judge Intake
|
||||
|
||||
The clutter-inventory seed candidates SHALL enter the calibration protocol at
|
||||
judge intake. Full seed intake SHALL apply to every calibration run after
|
||||
calibration pass #1; pass #1 uses the one-off seed hold-out described in the
|
||||
`Calibration Pass Validation Criteria` requirement below.
|
||||
|
||||
#### Scenario: Seed candidates feed the judge step
|
||||
|
||||
- **WHEN** a calibration run (other than pass #1) begins
|
||||
- **THEN** the clutter-inventory seed candidates are included as judge-intake evidence
|
||||
|
||||
### Requirement: Rule-Quality Test — Class Never Path
|
||||
|
||||
A proposed rule's glob SHALL name a recurring class of artifact, never an
|
||||
identifier unique to a single instance. A glob that hardcodes a name
|
||||
recurring by convention (e.g. `PRD.md`, `HANDOFF-*.md`,
|
||||
`migration-report.md`) is acceptable. A glob that hardcodes a run-id, hash,
|
||||
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.
|
||||
|
||||
#### Scenario: A convention-recurring name is acceptable
|
||||
|
||||
- **WHEN** a proposed rule's glob is `HANDOFF-*.md`
|
||||
- **THEN** it passes the class-never-path test, since `HANDOFF-*` is a recurring naming convention, not a single instance
|
||||
|
||||
#### Scenario: An instance-unique identifier fails the test
|
||||
|
||||
- **WHEN** a proposed rule's glob hardcodes a specific run-id or hash string that can only ever identify one artifact
|
||||
- **THEN** the rule fails the class-never-path test and is flagged loudly, not silently persisted
|
||||
|
||||
#### Scenario: One current match is fine; one-EVER match is not
|
||||
|
||||
- **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
|
||||
|
||||
### Requirement: Rule-Quality Tie-Breaker — Prefer the Narrower Glob
|
||||
|
||||
When choosing between candidate globs of differing breadth for the same
|
||||
cluster, `:calibrate` SHALL prefer the narrower glob. Too-narrow failure is
|
||||
self-healing (clutter is merely left for a later round to catch); too-broad
|
||||
failure is dangerous (a keeper file may be deleted and is not
|
||||
self-healing).
|
||||
|
||||
#### Scenario: Narrower glob is chosen when both would satisfy the cluster
|
||||
|
||||
- **WHEN** two candidate globs both cover the sampled cluster, one narrower and one broader
|
||||
- **THEN** the narrower glob is chosen
|
||||
|
||||
#### Scenario: Rationale is evidence quality, not readability alone
|
||||
|
||||
- **WHEN** justifying the narrower-glob preference
|
||||
- **THEN** the reasoning cited is that too-narrow fails safe (self-healing) while too-broad fails dangerous (not self-healing), not merely stylistic preference
|
||||
|
||||
### Requirement: Calibration Pass Validation Criteria
|
||||
|
||||
A calibration pass SHALL be judged against: a precision hard gate (the pass
|
||||
FAILS if any persisted rule's glob matches a protected path, regardless of
|
||||
behavior tier — exploration-time `consult` verdicts on protected paths are
|
||||
free and do not fail the pass); a recall floor of 8 of the 10 rows of the
|
||||
project's clutter inventory, with 4 specific rows mandatory (missing any
|
||||
mandatory row fails the pass); a one-off seed hold-out for calibration pass
|
||||
#1 only (the sealed answer key is withheld from judge intake for pass #1;
|
||||
every later run uses full seed intake); treatment of IGNORE-surface paths as
|
||||
void, not a miss, against the recall floor; and the requirement that a
|
||||
do-nothing pass cannot pass (the recall floor makes the pass falsifiable in
|
||||
the finding direction).
|
||||
|
||||
#### Scenario: A rule matching a protected path fails the pass
|
||||
|
||||
- **WHEN** any persisted rule's glob matches a path in the fixed protected set
|
||||
- **THEN** the calibration pass fails, regardless of whether that rule's tier was auto or confirm
|
||||
|
||||
#### Scenario: Consult verdicts on protected paths do not fail the pass
|
||||
|
||||
- **WHEN** the judge issues a `consult` verdict during exploration for a candidate touching a protected path, and that candidate is not persisted
|
||||
- **THEN** the pass is not failed by that consult verdict
|
||||
|
||||
#### Scenario: Recall floor requires 8 of 10 with 4 mandatory
|
||||
|
||||
- **WHEN** a calibration pass is graded against the clutter inventory
|
||||
- **THEN** it must recall at least 8 of the 10 inventory rows, and all 4 mandatory rows must be among them, or the pass fails
|
||||
|
||||
#### Scenario: Pass #1 seed hold-out is a one-off
|
||||
|
||||
- **WHEN** calibration pass #1 runs
|
||||
- **THEN** the sealed answer key (the project's clutter-inventory rows) is withheld from judge intake; every subsequent calibration run instead uses full seed intake
|
||||
|
||||
#### Scenario: IGNORE-surface rows are void, not a miss
|
||||
|
||||
- **WHEN** grading recall against the clutter inventory and a row corresponds to an IGNORE-surface path
|
||||
- **THEN** that row is excluded from the recall calculation entirely (void), not counted as a miss
|
||||
|
||||
#### Scenario: A do-nothing pass cannot pass
|
||||
|
||||
- **WHEN** a calibration pass persists zero rules
|
||||
- **THEN** it fails the recall floor and therefore cannot pass
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
# Spec: determinism-promotion
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the global, machine-readable completion-conventions catalog and the
|
||||
deterministic pipeline steps that use it to recommend graduating a
|
||||
classifier-judged (`served_when`) lifecycle rule to a scanner-provable
|
||||
(`served_when_path`) one, without ever applying an adoption unasked.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Conventions Catalog Is Global-Only and Machine-Readable
|
||||
|
||||
The plugin SHALL ship a global-only, machine-readable completion-conventions
|
||||
catalog at `plugins/os-doc-hygiene/conventions.json`. There SHALL be no
|
||||
per-project override of this catalog — it only recommends; adoption of a
|
||||
convention lands in the project's own rulebook, not in a project-specific
|
||||
copy of the catalog.
|
||||
|
||||
#### Scenario: The catalog is read from a single global location
|
||||
|
||||
- **WHEN** the deterministic pipeline consults the conventions catalog
|
||||
- **THEN** it reads `plugins/os-doc-hygiene/conventions.json` and no project-level override file for it exists or is consulted
|
||||
|
||||
#### Scenario: Adoption is a rulebook write, not a catalog write
|
||||
|
||||
- **WHEN** a project adopts a convention
|
||||
- **THEN** the adoption is expressed as a graduated rule in the project's `.dochygiene-rules.json`, and the global catalog itself is unchanged
|
||||
|
||||
### Requirement: Convention Entry Shape
|
||||
|
||||
Each entry in the conventions catalog SHALL record: a name, a description of
|
||||
what the convention proves (i.e., what filesystem-observable condition
|
||||
counts as "done"), the `served_when_path` pattern or frontmatter template a
|
||||
rule graduates to when the convention is adopted, and a one-line human pitch
|
||||
for why adopting it is worthwhile.
|
||||
|
||||
#### Scenario: An entry carries all required fields
|
||||
|
||||
- **WHEN** a convention entry is read from the catalog
|
||||
- **THEN** it includes a name, a "what it proves" description, the graduation target (`served_when_path` pattern or frontmatter template), and a one-line pitch
|
||||
|
||||
### Requirement: v1 Catalog Contains Exactly Two Conventions
|
||||
|
||||
The v1 conventions catalog SHALL contain exactly two entries: `archive-bucket`
|
||||
(done = the file moved into a sibling `archive/` directory, graduating a rule
|
||||
to `served_when_path: <dir>/archive/{name}`) and `status-frontmatter` (done =
|
||||
a `status: shipped|done` frontmatter key is present, with the file staying in
|
||||
place and the scanner reading the frontmatter). No other conventions SHALL be
|
||||
present in v1.
|
||||
|
||||
#### Scenario: Exactly two conventions ship in v1
|
||||
|
||||
- **WHEN** the v1 conventions catalog is loaded
|
||||
- **THEN** it contains exactly the `archive-bucket` and `status-frontmatter` entries and no others
|
||||
|
||||
#### Scenario: archive-bucket graduates to a served_when_path
|
||||
|
||||
- **WHEN** a project adopts `archive-bucket` for a rule
|
||||
- **THEN** the rule graduates from a classifier-judged `served_when` to a scanner-provable `served_when_path` pointing at the sibling `archive/` directory
|
||||
|
||||
#### Scenario: status-frontmatter graduates via a frontmatter key
|
||||
|
||||
- **WHEN** a project adopts `status-frontmatter` for a rule
|
||||
- **THEN** the rule graduates to a condition the scanner can check deterministically by reading a `status: shipped|done` frontmatter key, with the file remaining at its original path
|
||||
|
||||
### Requirement: Check Names Promotion Candidates Deterministically in Every Report
|
||||
|
||||
The `:check` skill's deterministic finalize pass SHALL, for every entry whose
|
||||
lifecycle signal is classifier-judged (`served_when` present, no
|
||||
`served_when_path`), check the conventions catalog for an applicable
|
||||
not-yet-adopted convention and, when one applies, emit an entry in the
|
||||
report's `promotion_candidates` section naming the convention and its
|
||||
one-line pitch. This check SHALL run without any model call.
|
||||
|
||||
#### Scenario: A classifier-judged entry with an applicable convention is named
|
||||
|
||||
- **WHEN** an entry uses `served_when` and the `archive-bucket` convention is not yet adopted for that rule
|
||||
- **THEN** the report's `promotion_candidates` section names `archive-bucket` for that entry with its one-line pitch
|
||||
|
||||
#### Scenario: Promotion-candidate naming requires no model call
|
||||
|
||||
- **WHEN** `report_builder.py` computes `promotion_candidates`
|
||||
- **THEN** it does so by reading `conventions.json` and the project's rulebook state directly, with no subagent dispatch
|
||||
|
||||
#### Scenario: An already-adopted convention is not re-named
|
||||
|
||||
- **WHEN** a rule has already graduated to a `served_when_path` matching a catalog convention
|
||||
- **THEN** that rule does not reappear in `promotion_candidates`
|
||||
|
||||
### Requirement: Calibrate May Draft Adoption but Never Applies Unasked
|
||||
|
||||
The `:calibrate` skill MAY draft the adoption of a catalog convention — the
|
||||
graduated rule plus the file moves or frontmatter additions the convention
|
||||
implies — and present it to the human for approval. It SHALL NEVER apply an
|
||||
adoption without explicit human confirmation.
|
||||
|
||||
#### Scenario: Calibrate drafts a convention adoption for review
|
||||
|
||||
- **WHEN** `:calibrate` identifies an unmatched pattern that would benefit from `archive-bucket`
|
||||
- **THEN** it drafts the graduated rule and the implied file moves and presents them to the human before any change is made
|
||||
|
||||
#### Scenario: No adoption is applied without confirmation
|
||||
|
||||
- **WHEN** a human has not yet confirmed a drafted adoption
|
||||
- **THEN** no rulebook write and no file move occurs
|
||||
|
|
@ -35,20 +35,25 @@ arguments or unknown arguments SHALL print usage plus the current status.
|
|||
|
||||
### Requirement: Check Skill Orchestrates Scan, Classification, and Report Writing
|
||||
|
||||
The `check` skill SHALL orchestrate the check pipeline: run the deterministic
|
||||
scanner, dispatch a Sonnet subagent for judgment-only classification of the
|
||||
signal-bearing candidates, run the deterministic finalize pass, validate, write the
|
||||
report pair, and stamp `last_check`. The skill SHALL run all non-judgment steps as
|
||||
deterministic scripts with no model (invariant #6). Zero-signal shortlisted files
|
||||
SHALL be treated as presumptively cleared: they SHALL remain in the shortlist,
|
||||
produce no entries, and SHALL NOT be read by the model. A `--scope` argument SHALL
|
||||
narrow the scanner; a `--category` argument SHALL filter which entries are produced
|
||||
after classification; both SHALL be recorded in the human-report header.
|
||||
The `check` skill SHALL orchestrate the check pipeline: load the lifecycle
|
||||
rulebook (global plus any project override), run the deterministic scanner
|
||||
(consuming the rulebook so directory-rule matches prune the walk and
|
||||
lifecycle signals are attached per the `lifecycle-rulebook` spec), dispatch
|
||||
a Sonnet subagent for judgment-only classification of the signal-bearing
|
||||
candidates, run the deterministic finalize pass (which also computes
|
||||
`promotion_candidates` from `conventions.json`), validate, write the report
|
||||
pair, and stamp `last_check`. The skill SHALL run all non-judgment steps as
|
||||
deterministic scripts with no model (invariant #6). Zero-signal shortlisted
|
||||
files SHALL be treated as presumptively cleared: they SHALL remain in the
|
||||
shortlist, produce no entries, and SHALL NOT be read by the model. A
|
||||
`--scope` argument SHALL narrow the scanner; a `--category` argument SHALL
|
||||
filter which entries are produced after classification; both SHALL be
|
||||
recorded in the human-report header.
|
||||
|
||||
#### Scenario: Skill runs the full pipeline
|
||||
|
||||
- **WHEN** the `check` skill runs
|
||||
- **THEN** it scans (deterministic), classifies signal-bearing candidates (Sonnet), finalizes (deterministic), validates (deterministic), writes the report pair (deterministic), and stamps `last_check` (deterministic)
|
||||
- **THEN** it loads the rulebook, scans (deterministic, rulebook-aware), classifies signal-bearing candidates (Sonnet), finalizes (deterministic, including promotion candidates), validates (deterministic), writes the report pair (deterministic), and stamps `last_check` (deterministic)
|
||||
|
||||
#### Scenario: Zero-signal files are not read by the model
|
||||
|
||||
|
|
@ -60,6 +65,51 @@ after classification; both SHALL be recorded in the human-report header.
|
|||
- **WHEN** the user passes `--scope docs/**/*.md` and `--category bloat`
|
||||
- **THEN** the scanner is narrowed by the scope, only `bloat` entries are produced after classification, and both the scope and the category are recorded in the human-report header
|
||||
|
||||
#### Scenario: Rulebook load failure is a hard failure, not a silent skip
|
||||
|
||||
- **WHEN** the rulebook loader hard-fails (unparseable JSON or unknown `schema_version` in either rulebook file)
|
||||
- **THEN** the check skill stops and reports the rulebook error before running the scanner, rather than proceeding with lifecycle signals silently disabled
|
||||
|
||||
### Requirement: Scanner Consumes the Rulebook for Pruning and Lifecycle Signals
|
||||
|
||||
The deterministic scanner SHALL consult the loaded rulebook during its walk.
|
||||
A directory-rule match (including IGNORE-surface entries) SHALL prune the
|
||||
walk beneath that directory per the `lifecycle-rulebook` spec. A file-rule
|
||||
match SHALL attach a lifecycle signal to that file's shortlist entry. These
|
||||
lifecycle signals SHALL flow into the classification subagent as a new
|
||||
signal class alongside the pre-existing stale/bloat signals, and MAY drive
|
||||
`op`/`op_type` selection toward `delete` or `extract-then-delete` per the
|
||||
`lifecycle-deletion` spec.
|
||||
|
||||
#### Scenario: A directory-rule prune is reflected in the scan artifact
|
||||
|
||||
- **WHEN** the scanner encounters a directory matching a directory rule
|
||||
- **THEN** the scan artifact reflects the prune (no files beneath it are in `files_scanned`), and, for non-IGNORE directory rules, exactly one aggregate shortlist entry appears for that directory
|
||||
|
||||
#### Scenario: A file-rule lifecycle signal reaches the classifier
|
||||
|
||||
- **WHEN** a file matches a file-rule with `lifetime: delete-once-served`
|
||||
- **THEN** the classification subagent receives the lifecycle signal (rule reference, lifetime, served_when/served_when_path) as part of that file's signals, verbatim, per the existing "signals are passed through verbatim" contract
|
||||
|
||||
### Requirement: Report Gains a Promotion-Candidates Section
|
||||
|
||||
The machine and human reports produced by `:check` SHALL include a
|
||||
`promotion_candidates` section (top-level, sibling to `entries`), populated
|
||||
deterministically by the finalize pass from `conventions.json` for every
|
||||
classifier-judged lifecycle entry with an applicable, not-yet-adopted
|
||||
convention. This section SHALL be present (possibly empty) on every run,
|
||||
including runs with no lifecycle entries.
|
||||
|
||||
#### Scenario: A run with an applicable convention names it in both reports
|
||||
|
||||
- **WHEN** a classifier-judged entry has an applicable, unadopted convention
|
||||
- **THEN** both the machine report's `promotion_candidates` array and the human report show the candidate with its one-line pitch
|
||||
|
||||
#### Scenario: A run with no applicable conventions still has the section, empty
|
||||
|
||||
- **WHEN** no classifier-judged entry has an applicable unadopted convention
|
||||
- **THEN** `promotion_candidates` is present as an empty array/section rather than omitted
|
||||
|
||||
### Requirement: Classification Subagent Returns Judgment-Only Proposals
|
||||
|
||||
The Sonnet classification subagent SHALL return, per signal-bearing candidate, a slim
|
||||
|
|
|
|||
|
|
@ -87,14 +87,19 @@ SHALL NOT attempt to compose or sequence incompatible ops in v1.
|
|||
|
||||
### Requirement: Safety-Tier Gating Before Any Mutation
|
||||
|
||||
The clean skill SHALL partition report entries by safety tier (read from the report —
|
||||
never recomputed) into `auto` entries (applied without prompt) and `confirm` entries
|
||||
(escalated before any mutation). It SHALL present all `confirm`-tier entries as a
|
||||
single batch-confirm list showing path, category, op, token count, and rationale with
|
||||
per-entry opt-out, visually distinguishing irreversible `delete-range` entries from
|
||||
reversible entries. The approved set SHALL be all `auto` entries plus any
|
||||
user-approved `confirm` entries. The gate SHALL run identically under `sweep` — the
|
||||
`/os-doc-hygiene:sweep` convenience path SHALL NOT bypass invariant #7.
|
||||
The clean skill SHALL partition report entries by safety tier (read from the
|
||||
report — never recomputed) into `auto` entries (applied without prompt) and
|
||||
`confirm` entries (escalated before any mutation). It SHALL present all
|
||||
`confirm`-tier entries as a single batch-confirm list showing path, category,
|
||||
op, token count, and rationale with per-entry opt-out, visually
|
||||
distinguishing irreversible `delete-range`, `delete`, and
|
||||
`extract-then-delete` entries from reversible entries. The approved set
|
||||
SHALL be all `auto` entries plus any user-approved `confirm` entries. The
|
||||
gate SHALL run identically under `sweep` — the `/os-doc-hygiene:sweep`
|
||||
convenience path SHALL NOT bypass invariant #7. Regardless of the tier
|
||||
recorded in the report, any `delete` or `extract-then-delete` entry SHALL be
|
||||
re-verified at apply time per the `lifecycle-deletion` tier matrix (tracked
|
||||
+ clean required for auto) before being applied without a prompt.
|
||||
|
||||
#### Scenario: auto entries apply without prompt
|
||||
|
||||
|
|
@ -103,7 +108,7 @@ user-approved `confirm` entries. The gate SHALL run identically under `sweep`
|
|||
|
||||
#### Scenario: confirm entries escalate before any mutation
|
||||
|
||||
- **WHEN** the report contains confirm-tier entries (delete-range or any generative op)
|
||||
- **WHEN** the report contains confirm-tier entries (delete-range, delete, extract-then-delete, or any generative op)
|
||||
- **THEN** the clean skill presents a batch-confirm list before any file is modified, and applies only user-approved entries
|
||||
|
||||
#### Scenario: per-entry opt-out is respected
|
||||
|
|
@ -116,29 +121,41 @@ user-approved `confirm` entries. The gate SHALL run identically under `sweep`
|
|||
- **WHEN** the user runs /os-doc-hygiene:sweep and the report contains confirm-tier entries
|
||||
- **THEN** the confirm gate runs identically to a standalone /os-doc-hygiene:clean
|
||||
|
||||
#### Scenario: A report-tier auto delete is downgraded if runtime state has changed
|
||||
|
||||
- **WHEN** a `delete` entry was tiered `auto` in the report but the applier's runtime check finds the file is now dirty or untracked
|
||||
- **THEN** the applier does not apply it silently; it is skipped and reported for re-analysis, never trusted from the cached tier
|
||||
|
||||
### Requirement: Git-Safe Single Commit
|
||||
|
||||
The clean skill SHALL produce exactly one git commit per run. Before any mutation it
|
||||
SHALL resolve the project root via `StateStore`, run `git status --porcelain`, and
|
||||
if the tree is dirty, SHALL automatically create a WIP checkpoint commit of the
|
||||
user's work before proceeding, so the cleanup commit remains exactly one. The cleanup
|
||||
commit SHALL be created via `git-context commit-apply --message-stdin` with a
|
||||
generated message summarizing auto/confirmed/skipped counts and op breakdown.
|
||||
Staging SHALL be precise: for non-move ops the skill calls `git add <staged_paths>`
|
||||
from the applier result; for `move-to-archive` the applier calls `git mv` (staging
|
||||
both sides) and the skill SHALL NOT `git add` the destination path again. The skill
|
||||
SHALL NEVER use `git add -A` or `git add .`. If a hard failure occurs (applier
|
||||
exit 2, `git mv` fail, or write error), the skill SHALL roll back via
|
||||
`git restore`/`reset` to the pre-run baseline and abort with a structured error.
|
||||
Partial success (some file batches guard-skipped) SHALL NOT trigger rollback — the
|
||||
skill SHALL commit what applied and report skipped files. Untracked candidate docs
|
||||
SHALL be skipped and reported (tracked-files-only). `last_clean` SHALL be stamped
|
||||
to the commit instant, not the run-start instant.
|
||||
The clean skill SHALL produce exactly one git commit per run. Before any
|
||||
mutation it SHALL resolve the project root via `StateStore`, run
|
||||
`git status --porcelain`, and if the tree is dirty, SHALL automatically
|
||||
create a WIP checkpoint commit of the user's work before proceeding, so the
|
||||
cleanup commit remains exactly one. The cleanup commit SHALL be created via
|
||||
`git-context commit-apply --message-stdin` with a generated message
|
||||
summarizing auto/confirmed/skipped counts and op breakdown, including
|
||||
lifecycle delete/extract-then-delete counts when present. Staging SHALL be
|
||||
precise: for non-move, non-delete ops the skill calls `git add
|
||||
<staged_paths>` from the applier result; for `move-to-archive` the applier
|
||||
calls `git mv` (staging both sides) and the skill SHALL NOT `git add` the
|
||||
destination path again; for `delete` and the delete half of
|
||||
`extract-then-delete` the applier calls `git rm` (staging the removal
|
||||
itself) and the skill SHALL NOT separately stage the removed path. The skill
|
||||
SHALL NEVER use `git add -A` or `git add .`. If a hard failure occurs
|
||||
(applier exit 2, `git mv`/`git rm` fail, or write error), the skill SHALL
|
||||
roll back via `git restore`/`reset` to the pre-run baseline and abort with a
|
||||
structured error. Partial success (some file batches guard-skipped) SHALL
|
||||
NOT trigger rollback — the skill SHALL commit what applied and report
|
||||
skipped files. Untracked candidate docs SHALL be skipped and reported
|
||||
(tracked-files-only) except where a lifecycle rule explicitly escalates an
|
||||
untracked delete to confirm and the user approves it. `last_clean` SHALL be
|
||||
stamped to the commit instant, not the run-start instant.
|
||||
|
||||
#### Scenario: Clean tree produces exactly one commit
|
||||
|
||||
- **WHEN** the user runs /os-doc-hygiene:clean with a clean git tree
|
||||
- **THEN** the run produces exactly one git commit containing all applied edits
|
||||
- **THEN** the run produces exactly one git commit containing all applied edits, including any lifecycle deletes
|
||||
|
||||
#### Scenario: Dirty tree gets a WIP checkpoint then one cleanup commit
|
||||
|
||||
|
|
@ -152,7 +169,7 @@ to the commit instant, not the run-start instant.
|
|||
|
||||
#### Scenario: Hard failure triggers rollback
|
||||
|
||||
- **WHEN** a write error or applier exit 2 occurs mid-run
|
||||
- **WHEN** a write error, applier exit 2, or a `git rm` failure occurs mid-run
|
||||
- **THEN** the skill rolls back to the pre-run baseline and aborts with a structured error; no partial commit is created
|
||||
|
||||
#### Scenario: Partial success commits what applied
|
||||
|
|
@ -165,6 +182,50 @@ to the commit instant, not the run-start instant.
|
|||
- **WHEN** the applier stages a move-to-archive via git mv (both source and dest)
|
||||
- **THEN** the skill does not call git add on the destination path again
|
||||
|
||||
#### Scenario: A lifecycle delete is not double-staged
|
||||
|
||||
- **WHEN** the applier stages a `delete` via `git rm`
|
||||
- **THEN** the skill does not separately call `git add` or any other staging command on the removed path
|
||||
|
||||
### Requirement: Applier Applies Delete and Extract-Then-Delete Under the Tier Matrix
|
||||
|
||||
The patch applier SHALL support two new op kinds, `delete` and
|
||||
`extract-then-delete`. For both, immediately before applying, it SHALL
|
||||
re-run `git ls-files <path>` and a dirty check against that specific path —
|
||||
never trusting the cached report tier or rule claim — and SHALL apply the
|
||||
tier matrix from the `lifecycle-deletion` spec to decide whether the entry
|
||||
may proceed as `auto` or must be treated as `confirm` (already gated
|
||||
upstream by the clean skill). `delete` SHALL perform a `git rm` (recursive
|
||||
for a directory-rule aggregate entry) staged into the run's single hygiene
|
||||
commit. `extract-then-delete` SHALL first complete its generative extraction
|
||||
step (repo-durable via the existing live-read Sonnet distillation path
|
||||
writing into an ADR/CLAUDE.md/docs target, or cross-repo via
|
||||
`/os-vault:write`) and SHALL only perform the `git rm` once extraction has
|
||||
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).
|
||||
|
||||
#### Scenario: delete performs a true git rm at apply time
|
||||
|
||||
- **WHEN** the applier applies a `delete` entry
|
||||
- **THEN** it re-verifies tracked/clean status via `git ls-files` and a dirty check, then performs a `git rm` staged into the single hygiene commit
|
||||
|
||||
#### Scenario: extract-then-delete only deletes after extraction succeeds
|
||||
|
||||
- **WHEN** the applier applies an `extract-then-delete` entry
|
||||
- **THEN** it completes the extraction write (ADR/CLAUDE.md/docs or vault) first, and performs the `git rm` only after that write succeeds
|
||||
|
||||
#### Scenario: A failed extraction skips the delete for that entry
|
||||
|
||||
- **WHEN** the extraction step of an `extract-then-delete` entry fails
|
||||
- **THEN** the delete is not applied for that entry, the entry is reported as skipped, and the run is not treated as a hard failure unless the failure matches an existing hard-failure trigger (applier exit 2, write error)
|
||||
|
||||
#### Scenario: Directory-rule aggregate delete removes the whole directory
|
||||
|
||||
- **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
|
||||
|
||||
### Requirement: Clean Skill Orchestration
|
||||
|
||||
The `clean` skill SHALL load the current report via `StateStore.read_report`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
# Spec: lifecycle-deletion
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines true git deletion for lifecycle-managed artifacts: the deletion
|
||||
autonomy tier matrix (evidence quality and recoverability, not file type),
|
||||
temporary-tier retention semantics, age computation, the split between
|
||||
scanner-provable and classifier-judged served signals, and the extract
|
||||
modifier's routing through existing knowledge destinations.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Delete Is True Git Deletion in a Dedicated Hygiene Commit
|
||||
|
||||
A lifecycle `delete` op SHALL perform a true `git rm` (file or, for a
|
||||
directory-rule aggregate entry, recursive directory removal) staged into the
|
||||
same single hygiene commit the run produces. There SHALL be no archive
|
||||
directory, graveyard branch, or other relocation of the deleted content —
|
||||
git history SHALL be the sole archive.
|
||||
|
||||
#### Scenario: A deleted file is git-rm'd, not moved
|
||||
|
||||
- **WHEN** a `delete` op is applied to a tracked file
|
||||
- **THEN** the file is removed via `git rm` and staged into the run's single hygiene commit, with no copy relocated anywhere in the working tree
|
||||
|
||||
#### Scenario: A deleted directory-rule entry is removed recursively
|
||||
|
||||
- **WHEN** a `delete` op targets a directory-rule aggregate entry
|
||||
- **THEN** the entire directory is removed via a recursive `git rm` staged into the same commit
|
||||
|
||||
### Requirement: Deletion Autonomy Tier Matrix
|
||||
|
||||
Deletion autonomy SHALL be determined by evidence quality and recoverability,
|
||||
not file type, per the following matrix:
|
||||
|
||||
| Case | Behavior |
|
||||
|---|---|
|
||||
| IGNORE surface | never walked, never a delete candidate |
|
||||
| lifetime `keep` | scanned + reported, never deleted |
|
||||
| tracked + delete rule + clean worktree | auto |
|
||||
| tracked + delete rule + dirty worktree | confirm |
|
||||
| untracked + delete rule | confirm |
|
||||
| no rule match | unmanaged, never deleted |
|
||||
|
||||
Tracked/clean status SHALL be verified at runtime via `git ls-files` plus a
|
||||
dirty check, and SHALL NEVER be trusted from the rule's own claim or from a
|
||||
cached report field.
|
||||
|
||||
#### Scenario: Tracked and clean deletes automatically
|
||||
|
||||
- **WHEN** a file matches a delete rule, is tracked, and the worktree for that path is clean
|
||||
- **THEN** the deletion proceeds without a confirmation prompt
|
||||
|
||||
#### Scenario: Tracked but dirty requires confirmation
|
||||
|
||||
- **WHEN** a file matches a delete rule, is tracked, but has uncommitted changes
|
||||
- **THEN** the deletion is escalated to confirm — an uncommitted diff would otherwise be lost with the file
|
||||
|
||||
#### Scenario: Untracked requires confirmation
|
||||
|
||||
- **WHEN** a file matches a delete rule but is untracked
|
||||
- **THEN** the deletion is escalated to confirm — there is no git history to recover it from
|
||||
|
||||
#### Scenario: Runtime verification never trusts the rule
|
||||
|
||||
- **WHEN** a delete op is about to be applied
|
||||
- **THEN** the applier re-checks tracked/clean status via `git ls-files` and a dirty check at that moment, regardless of what the report or rule previously claimed
|
||||
|
||||
#### Scenario: No rule match is never deleted
|
||||
|
||||
- **WHEN** a file has no lifecycle rule match
|
||||
- **THEN** it is never a candidate for deletion on lifecycle grounds
|
||||
|
||||
### Requirement: Temporary Tier Retention Semantics
|
||||
|
||||
The `temporary` lifetime SHALL use retain-recent-N plus age, not age alone.
|
||||
`retain_recent` (default 3) SHALL always keep the N most recent matching
|
||||
entries for a rule regardless of age. An entry ranked `retain_recent + 1` or
|
||||
older SHALL become eligible for deletion once it exceeds `max_age_days`
|
||||
(default 3). The retention unit SHALL be the rule's own match granularity: a
|
||||
file-rule's unit is the individual file; a directory-rule's unit is the
|
||||
matched directory as a whole (e.g., a run directory), never files nested
|
||||
inside one such matched directory.
|
||||
|
||||
#### Scenario: The 3 newest entries are always kept regardless of age
|
||||
|
||||
- **WHEN** a temporary rule has `retain_recent: 3` and five matching entries, the three newest exceeding `max_age_days`
|
||||
- **THEN** the three newest are kept and only the two oldest (ranked 4th and 5th) are eligible for deletion
|
||||
|
||||
#### Scenario: An entry younger than max_age_days is kept even if not in the top N
|
||||
|
||||
- **WHEN** a temporary rule's 4th-ranked entry is younger than `max_age_days`
|
||||
- **THEN** it is not deleted this run
|
||||
|
||||
#### Scenario: Directory-rule retention unit is the whole directory
|
||||
|
||||
- **WHEN** a directory rule matches `autoresearch/<run-id>/` entries
|
||||
- **THEN** retain-recent-N and age are computed per matched run directory as a whole, not per file within the newest run
|
||||
|
||||
### Requirement: Temporary Tier Age Source
|
||||
|
||||
Age for the temporary tier SHALL be computed from the git commit time of the
|
||||
path's most recent commit, falling back to filesystem mtime only when the
|
||||
path is untracked. There SHALL be no per-rule `age_source` override field.
|
||||
|
||||
#### Scenario: Tracked file age comes from git commit time
|
||||
|
||||
- **WHEN** age is computed for a tracked file matching a temporary rule
|
||||
- **THEN** the age is derived from that file's most recent commit time, not its filesystem mtime
|
||||
|
||||
#### Scenario: Untracked file age falls back to mtime
|
||||
|
||||
- **WHEN** age is computed for an untracked file matching a temporary rule
|
||||
- **THEN** the age is derived from the file's filesystem mtime
|
||||
|
||||
#### Scenario: No per-rule age_source field exists
|
||||
|
||||
- **WHEN** a rule in the rulebook is inspected
|
||||
- **THEN** it has no `age_source` field — the git-commit-time-with-mtime-fallback behavior is fixed, not configurable per rule
|
||||
|
||||
### Requirement: Untracked Directory Entry Age Uses Directory Inode mtime
|
||||
|
||||
For an untracked directory matched by a directory rule, age SHALL be
|
||||
computed from a single `stat()` of the directory inode itself, not a
|
||||
recursive walk computing the maximum mtime of its contents.
|
||||
|
||||
#### Scenario: Directory age is one stat call, not a recursive scan
|
||||
|
||||
- **WHEN** age is computed for an untracked directory matching a directory rule
|
||||
- **THEN** the computation reads only the directory inode's own mtime and does not recurse into or stat any file inside it
|
||||
|
||||
### Requirement: delete-once-served Split by Evidence Quality
|
||||
|
||||
The `delete-once-served` lifetime SHALL support two mutually exclusive served
|
||||
signals per rule: `served_when_path`, a deterministic path pattern the
|
||||
scanner itself can prove satisfied (e.g. a sibling archive directory
|
||||
existing), and `served_when`, free text describing a condition the
|
||||
classifier must judge. A rule with `served_when_path` satisfied by the
|
||||
scanner MAY be deleted under the autonomy tier matrix (i.e., auto when
|
||||
tracked+clean). A rule relying on `served_when` SHALL ALWAYS be forced to
|
||||
confirm, regardless of tracked/clean status, because it depends on a model
|
||||
judgment rather than a provable filesystem fact.
|
||||
|
||||
#### Scenario: Scanner-proven served_when_path may auto-delete
|
||||
|
||||
- **WHEN** a rule's `served_when_path` condition is satisfied by the filesystem and the matched path is tracked and clean
|
||||
- **THEN** the deletion may proceed automatically under the tier matrix
|
||||
|
||||
#### Scenario: Classifier-judged served_when always forces confirm
|
||||
|
||||
- **WHEN** a rule uses `served_when` (free text) and the classifier judges the condition met
|
||||
- **THEN** the deletion is always escalated to confirm, even if the matched path is tracked and clean
|
||||
|
||||
#### Scenario: The LLM may propose but never silently destroy on served_when
|
||||
|
||||
- **WHEN** the classifier judges a `served_when` condition satisfied
|
||||
- **THEN** it produces a proposal for a human to confirm; it never causes an unattended deletion
|
||||
|
||||
### Requirement: Extract Modifier Routes Through Existing Knowledge Destinations Only
|
||||
|
||||
The `extract` modifier on a deletion SHALL distill durable content before
|
||||
deleting, routing exclusively through the existing knowledge-routing
|
||||
destinations: repo-durable residue SHALL be written into an ADR, `CLAUDE.md`,
|
||||
or a `docs/` file; cross-repo lessons SHALL be written to the SecondBrain
|
||||
vault via `/os-vault:write`. No new destination (e.g., a "retired specs"
|
||||
directory) SHALL be introduced.
|
||||
|
||||
#### Scenario: Repo-durable extraction targets ADR/CLAUDE.md/docs
|
||||
|
||||
- **WHEN** an `extract-then-delete` op is classified as repo-durable
|
||||
- **THEN** the extracted content is written into an ADR, `CLAUDE.md`, or a `docs/` file, never a new bespoke location
|
||||
|
||||
#### Scenario: Cross-repo extraction routes through /os-vault:write
|
||||
|
||||
- **WHEN** an `extract-then-delete` op is classified as cross-repo
|
||||
- **THEN** the extracted content is written to the SecondBrain vault via `/os-vault:write`, and no other cross-repo destination is used
|
||||
|
||||
#### Scenario: No new destination is introduced
|
||||
|
||||
- **WHEN** extraction routing is implemented
|
||||
- **THEN** it reuses only the destinations named above; it does not create a new "retired" or "archive" content store
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
# Spec: lifecycle-rulebook
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the global + optional per-project rulebook files that declare
|
||||
lifetime rules for known-clutter artifacts: their location and envelope,
|
||||
glob dialect, two-axis merge precedence, per-rule fields, validation
|
||||
behavior, the "unmatched means unmanaged" contract, the explicit IGNORE
|
||||
surface, and how the scanner walk consumes rules for pruning and lifecycle
|
||||
signal attachment.
|
||||
|
||||
## 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 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": [...]}`
|
||||
|
||||
### Requirement: Glob Dialect Is glob.translate
|
||||
|
||||
Rule `glob` patterns SHALL be compiled using stdlib
|
||||
`glob.translate(pattern, recursive=True, include_hidden=True)` (Python ≥
|
||||
3.13). Patterns SHALL be interpreted as repo-root-relative, and `**` SHALL
|
||||
match recursively. Each rule's glob SHALL be compiled once at rulebook load
|
||||
time, not per matched path.
|
||||
|
||||
#### Scenario: Recursive double-star matches subtrees
|
||||
|
||||
- **WHEN** a rule's glob is `autoresearch/*/**`
|
||||
- **THEN** it matches any file at any depth under any immediate subdirectory of `autoresearch/`
|
||||
|
||||
#### Scenario: Hidden files are matched when the pattern implies them
|
||||
|
||||
- **WHEN** a rule's glob targets a dotfile path
|
||||
- **THEN** `include_hidden=True` semantics apply and the dotfile is matchable
|
||||
|
||||
#### Scenario: Compilation happens once per load
|
||||
|
||||
- **WHEN** the rulebook loader parses the rules array
|
||||
- **THEN** each rule's glob is compiled to a matcher exactly once, and that compiled matcher is reused for every path checked during the run
|
||||
|
||||
### Requirement: Two-Axis Precedence with Add-Only Merge
|
||||
|
||||
The rulebook loader SHALL merge the project override over the global
|
||||
rulebook using add-only semantics: project rules are appended to, and never
|
||||
replace or delete, global rules. Precedence for a given path SHALL resolve
|
||||
in this order: project file-rule > project directory-rule > global
|
||||
file-rule > global directory-rule. Ties within the same precedence tier
|
||||
SHALL be broken first by longest `glob` pattern, then by last-defined order.
|
||||
A project SHALL neutralize a global rule only by adding a shadowing rule
|
||||
with `lifetime: "keep"` at equal-or-higher precedence; there SHALL be no
|
||||
rule-removal mechanism.
|
||||
|
||||
#### Scenario: Project file-rule outranks every other tier
|
||||
|
||||
- **WHEN** a path matches both a project file-rule and a global directory-rule
|
||||
- **THEN** the project file-rule's fields govern
|
||||
|
||||
#### Scenario: Ties broken by longest pattern then last-defined
|
||||
|
||||
- **WHEN** two rules in the same precedence tier match the same path with different-length globs
|
||||
- **THEN** the rule with the longer glob pattern governs; if the glob lengths are equal, the rule defined later in its rules array governs
|
||||
|
||||
#### Scenario: Neutralizing a global rule via keep-shadowing
|
||||
|
||||
- **WHEN** a project wants to exempt a path from a global delete rule
|
||||
- **THEN** it adds a project rule matching that path with `lifetime: "keep"`, and no mechanism exists to remove or edit the global rule itself
|
||||
|
||||
#### Scenario: Merge never deletes a global rule
|
||||
|
||||
- **WHEN** the project override is loaded alongside the global rulebook
|
||||
- **THEN** every global rule remains present and evaluable; the merge only adds project rules on top
|
||||
|
||||
### Requirement: Per-Rule Fields
|
||||
|
||||
A rule SHALL support the fields `glob`, `lifetime` (one of `keep`,
|
||||
`temporary`, `delete-once-served`), `extract` (boolean modifier), `served_when`
|
||||
(free text, classifier hint), `served_when_path` (deterministic sibling of
|
||||
`served_when`), `retain_recent` (default `3`), `max_age_days` (default `3`),
|
||||
`confirm` (boolean, human-settable-only escape hatch), `confirmed_by`
|
||||
(`human` or a strong-model identifier), `confirmed_on`, `source`, and `note`.
|
||||
A rule SHALL NOT support a `propagate_ignore` field in any form. A rule that
|
||||
matches no path yet is undefined behavior only in the sense that unmatched
|
||||
files receive no lifetime at all and flow through existing signals unchanged
|
||||
— unmatched SHALL always mean unmanaged, never an implicit lifetime.
|
||||
|
||||
#### Scenario: Defaults apply when retain_recent/max_age_days are omitted
|
||||
|
||||
- **WHEN** a `temporary`-lifetime rule omits `retain_recent` and `max_age_days`
|
||||
- **THEN** the loader applies `retain_recent = 3` and `max_age_days = 3`
|
||||
|
||||
#### Scenario: confirm:true may only be set by a human
|
||||
|
||||
- **WHEN** a rule is validated
|
||||
- **THEN** a rule with `confirm: true` is accepted only if it is not proposed by a model in the same validation pass as an unconfirmed state — a model-authored rule proposal SHALL NOT itself set `confirm: true`; it may only recommend that a human set it
|
||||
|
||||
#### Scenario: propagate_ignore is rejected as an unknown field
|
||||
|
||||
- **WHEN** a rule in either rulebook file contains a `propagate_ignore` field
|
||||
- **THEN** the loader treats it as an unrecognized field under the rule's validation (skip-and-warn, per the Validation requirement), since no such field is part of the schema
|
||||
|
||||
#### Scenario: Unmatched files receive no lifetime
|
||||
|
||||
- **WHEN** a file matches no rule in either rulebook
|
||||
- **THEN** the rulebook query returns no match for that path, and the file flows through the existing (non-lifecycle) scanner signals unchanged, becoming a `:calibrate` candidate
|
||||
|
||||
### Requirement: Skip-and-Warn Validation, Hard-Fail Only on Structural Errors
|
||||
|
||||
The rulebook loader SHALL skip and warn on a per-rule basis for any rule that
|
||||
is invalid or lacks `confirmed_by` — such a rule SHALL be loaded but marked
|
||||
inactive and SHALL never contribute a lifecycle signal, while the rest of
|
||||
the rulebook continues to load and function. The loader SHALL hard-fail
|
||||
(non-zero exit / raised error) only for unparseable JSON or an unrecognized
|
||||
`schema_version`.
|
||||
|
||||
#### Scenario: A rule missing confirmed_by is skipped, not fatal
|
||||
|
||||
- **WHEN** the rulebook contains one rule without `confirmed_by` and nine valid rules
|
||||
- **THEN** the loader loads all ten rules, marks the one missing `confirmed_by` inactive (it never emits a signal), and the other nine function normally
|
||||
|
||||
#### Scenario: Unparseable JSON hard-fails
|
||||
|
||||
- **WHEN** either rulebook file is not valid JSON
|
||||
- **THEN** the loader raises a hard failure and does not proceed with a partial rulebook
|
||||
|
||||
#### Scenario: Unknown schema_version hard-fails
|
||||
|
||||
- **WHEN** a rulebook file declares a `schema_version` the loader does not recognize
|
||||
- **THEN** the loader raises a hard failure
|
||||
|
||||
### Requirement: Unmatched Means Unmanaged
|
||||
|
||||
Files that match no rule in either rulebook SHALL receive no lifecycle
|
||||
signal and SHALL NOT be deleted, extracted, or otherwise treated as
|
||||
lifecycle-managed by any component of this change. They remain visible only
|
||||
through the existing stale/bloat signal pipeline and are the candidate pool
|
||||
for `:calibrate`.
|
||||
|
||||
#### Scenario: No rule match means no lifecycle behavior
|
||||
|
||||
- **WHEN** a file matches no rulebook rule
|
||||
- **THEN** no delete or extract-then-delete op is ever proposed for it on lifecycle grounds alone
|
||||
|
||||
### Requirement: IGNORE Surface Is an Explicit Seed List, Never Inferred from .gitignore
|
||||
|
||||
The rulebook SHALL define an explicit IGNORE surface as directory rules with
|
||||
no lifetime (paths never walked at all, distinct from `keep`, which is
|
||||
walked and reported but never deleted). The seed IGNORE members SHALL
|
||||
include `graphify-out/**` and `.dochygiene/**`, plus any entries needed to
|
||||
cover the plugin's actual current state directory (`.cc-os/**`, already
|
||||
covered by the scanner's pre-existing hardcoded self-exclusion — see the
|
||||
`doc-check` spec). The IGNORE surface SHALL NEVER be inferred from
|
||||
`.gitignore` — a gitignored path is neither automatically deletable nor
|
||||
automatically keepable.
|
||||
|
||||
#### Scenario: graphify-out is never walked
|
||||
|
||||
- **WHEN** the scanner walks a project containing `graphify-out/`
|
||||
- **THEN** no file beneath `graphify-out/` is opened, and no shortlist or signal entry is produced for it or its contents
|
||||
|
||||
#### Scenario: .dochygiene legacy state dir is never walked
|
||||
|
||||
- **WHEN** the scanner encounters `.dochygiene/` in a project that has not migrated to `.cc-os/dochygiene/`
|
||||
- **THEN** the directory is treated as IGNORE surface and never walked
|
||||
|
||||
#### Scenario: gitignored is not treated as IGNORE surface
|
||||
|
||||
- **WHEN** a path is listed in `.gitignore` but is not one of the explicit IGNORE seed members
|
||||
- **THEN** the scanner walks it normally per its other rules — being gitignored alone neither excludes it from the walk nor exempts it from deletion
|
||||
|
||||
### Requirement: Directory-Rule Walk Pruning
|
||||
|
||||
When a directory-rule (a lifecycle rule whose glob covers a subtree) matches
|
||||
a directory during the scanner walk, the scanner SHALL prune the walk at
|
||||
that directory: no file beneath it SHALL be opened or read. For directory
|
||||
rules carrying a real lifetime (`temporary` or `delete-once-served`), the
|
||||
scanner SHALL emit exactly one aggregate shortlist/signal entry for the
|
||||
directory path itself, carrying the lifecycle signal (rule reference,
|
||||
lifetime, and `served_when`/`served_when_path`). For IGNORE-surface
|
||||
directory rules (no lifetime), the scanner SHALL emit no entry at all.
|
||||
|
||||
#### Scenario: A temporary directory rule prunes and emits one aggregate entry
|
||||
|
||||
- **WHEN** `autoresearch/run-2026-07-01/` matches a directory rule with `lifetime: temporary`
|
||||
- **THEN** the scanner does not open any file inside that directory and emits exactly one shortlist entry for the directory path carrying the lifecycle signal
|
||||
|
||||
#### Scenario: An IGNORE-surface directory rule prunes with no entry
|
||||
|
||||
- **WHEN** `graphify-out/` matches the IGNORE-surface rule
|
||||
- **THEN** the scanner does not open any file inside it and produces no shortlist or signal entry for it
|
||||
|
||||
### Requirement: Lifecycle Signal Attachment on File-Rule Matches
|
||||
|
||||
When a file-rule matches a path not caught by a directory-rule prune, the
|
||||
scanner SHALL attach a lifecycle signal (rule reference, lifetime,
|
||||
`served_when`/`served_when_path`) to that file's shortlist entry, alongside
|
||||
any pre-existing objective signals for the same file. The lifecycle signal
|
||||
SHALL be a new signal class consumed by the classification subagent like any
|
||||
other signal.
|
||||
|
||||
#### Scenario: A file-rule match adds a lifecycle signal without displacing existing signals
|
||||
|
||||
- **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
|
||||
|
|
@ -14,19 +14,26 @@ explicit human approval.
|
|||
|
||||
The machine report SHALL be a single JSON object containing `schema_version`,
|
||||
`tool_version`, `generated_at` (ISO-8601 UTC), a `scan` metadata object, a
|
||||
`shortlist` array, and an `entries` array. The `scan` object SHALL include
|
||||
`project_root`, `scope_globs`, `excluded_dirs`, and `files_scanned`. The
|
||||
`generated_at` timestamp SHALL be the check time that the clean step uses for
|
||||
its mtime guard.
|
||||
`shortlist` array, an `entries` array, and a `promotion_candidates` array.
|
||||
The `scan` object SHALL include `project_root`, `scope_globs`,
|
||||
`excluded_dirs`, and `files_scanned`. The `generated_at` timestamp SHALL be
|
||||
the check time that the clean step uses for its mtime guard. The
|
||||
`promotion_candidates` array SHALL be present (possibly empty) on every
|
||||
report and SHALL list, per candidate, the classifier-judged entry it applies
|
||||
to, the recommended `conventions.json` entry name, and its one-line pitch.
|
||||
|
||||
#### Scenario: Check writes a well-formed report
|
||||
- **WHEN** the `check` skill completes a scan and classification pass
|
||||
- **THEN** it writes one JSON object with `schema_version`, `tool_version`, `generated_at`, `scan`, `shortlist`, and `entries`
|
||||
- **THEN** it writes one JSON object with `schema_version`, `tool_version`, `generated_at`, `scan`, `shortlist`, `entries`, and `promotion_candidates`
|
||||
|
||||
#### Scenario: Clean reads the check timestamp
|
||||
- **WHEN** the `clean` skill loads a report
|
||||
- **THEN** it reads `generated_at` and uses it as the reference time for the per-op mtime guard
|
||||
|
||||
#### Scenario: promotion_candidates is always present, possibly empty
|
||||
- **WHEN** a check run has no classifier-judged entry with an applicable unadopted convention
|
||||
- **THEN** `promotion_candidates` is written as an empty array, not omitted
|
||||
|
||||
### Requirement: Shortlist Precedes Entries
|
||||
|
||||
The `shortlist` SHALL contain the project-root-relative paths the deterministic
|
||||
|
|
@ -108,17 +115,38 @@ validated deterministically; an entry that violates it is invalid.
|
|||
|
||||
### Requirement: Safety-Tier Is Derived Deterministically
|
||||
|
||||
`deterministic` ops SHALL be exact edits the check pre-computes and the cleaner
|
||||
applies with no model. `generative` ops SHALL be prose transformations requiring
|
||||
a model at clean time. The `safety_tier` SHALL be **computed** by a deterministic
|
||||
script function `safety_tier(op_type, is_destructive, is_reversible)` and SHALL
|
||||
NOT be assigned by the model; the report records the computed value. The function
|
||||
SHALL return `confirm` when `op_type` is `generative`, OR when `is_destructive`
|
||||
is true, OR when `is_reversible` is false; and SHALL return `auto` only when the
|
||||
op is `deterministic` AND non-destructive AND reversible (hence objective). The
|
||||
function SHALL NEVER return `auto` for a `generative` op or for any destructive
|
||||
or irreversible op, so the model cannot violate invariant #7. `auto`-tier ops
|
||||
SHALL run without a prompt; `confirm`-tier ops SHALL be escalated for approval.
|
||||
`deterministic` ops SHALL be exact edits the check pre-computes and the
|
||||
cleaner applies with no model. `generative` ops SHALL be prose
|
||||
transformations requiring a model at clean time. The `safety_tier` SHALL be
|
||||
**computed** by a deterministic script function `derive_safety_tier(op_type,
|
||||
is_destructive, is_reversible, lifecycle=None)` and SHALL NOT be assigned by
|
||||
the model; the report records the computed value. This function remains the
|
||||
single source of truth for tier derivation across the whole report schema,
|
||||
including lifecycle ops — no second tier-deriving function or code path is
|
||||
introduced.
|
||||
|
||||
For non-lifecycle ops, the function SHALL return `confirm` when `op_type` is
|
||||
`generative`, OR when `is_destructive` is true, OR when `is_reversible` is
|
||||
false; and SHALL return `auto` only when the op is `deterministic` AND
|
||||
non-destructive AND reversible (hence objective).
|
||||
|
||||
For `delete` and `extract-then-delete` ops, the function SHALL additionally
|
||||
consult the `lifecycle` argument and SHALL return `auto` only when ALL of the
|
||||
following hold: the lifecycle evidence is scanner-proven (a satisfied
|
||||
`served_when_path`, or a temporary-tier retain-recent/age computation) AND
|
||||
the path is tracked AND the worktree at that path is clean at the time of
|
||||
derivation. Every other combination for a lifecycle op — a classifier-judged
|
||||
`served_when`, a dirty worktree, or an untracked path — SHALL derive to
|
||||
`confirm`, regardless of `is_destructive`/`is_reversible` inputs. The
|
||||
function SHALL NEVER return `auto` for a `generative` op, for any
|
||||
non-lifecycle destructive or irreversible op, or for any lifecycle op whose
|
||||
evidence is classifier-judged or whose git state is not tracked+clean, so
|
||||
the model cannot violate invariant #7.
|
||||
|
||||
`auto`-tier ops SHALL run without a prompt; `confirm`-tier ops SHALL be
|
||||
escalated for approval; `delete`/`extract-then-delete` `auto` verdicts SHALL
|
||||
additionally be re-verified against live git state at apply time per the
|
||||
`lifecycle-deletion` spec before being applied without a prompt.
|
||||
|
||||
#### Scenario: Deterministic reversible op derives to auto
|
||||
- **WHEN** an entry has `op_type` = `deterministic`, `is_destructive` = false, and `is_reversible` = true
|
||||
|
|
@ -133,9 +161,64 @@ SHALL run without a prompt; `confirm`-tier ops SHALL be escalated for approval.
|
|||
- **THEN** the derived `safety_tier` is `confirm` and the op is delegated to a Sonnet subagent at clean time
|
||||
|
||||
#### Scenario: Function can never emit auto for a generative or destructive op
|
||||
- **WHEN** `safety_tier(op_type, is_destructive, is_reversible)` is evaluated for any input where `op_type` = `generative`, or `is_destructive` = true, or `is_reversible` = false
|
||||
- **WHEN** `derive_safety_tier(op_type, is_destructive, is_reversible, lifecycle)` is evaluated for any input where `op_type` = `generative`, or (for a non-lifecycle op) `is_destructive` = true, or `is_reversible` = false
|
||||
- **THEN** the result is `confirm`, never `auto`
|
||||
|
||||
#### Scenario: A lifecycle delete with scanner-proven evidence and tracked+clean state derives to auto
|
||||
- **WHEN** a `delete` entry's `lifecycle` argument shows a satisfied `served_when_path` and the path is tracked and clean
|
||||
- **THEN** the derived `safety_tier` is `auto`
|
||||
|
||||
#### Scenario: A lifecycle delete with classifier-judged evidence always derives to confirm
|
||||
- **WHEN** a `delete` or `extract-then-delete` entry's `lifecycle` argument carries `served_when` (classifier-judged)
|
||||
- **THEN** the derived `safety_tier` is `confirm`, regardless of tracked/clean state
|
||||
|
||||
#### Scenario: A lifecycle delete on a dirty or untracked path derives to confirm
|
||||
- **WHEN** a `delete` entry's evidence is scanner-proven but the path is dirty or untracked
|
||||
- **THEN** the derived `safety_tier` is `confirm`
|
||||
|
||||
### Requirement: Lifecycle Signal Fields on Shortlist and Entries
|
||||
|
||||
A shortlist entry or report entry carrying a lifecycle signal SHALL include a
|
||||
`lifecycle` object with `rule_ref` (identifying which rulebook rule matched),
|
||||
`lifetime` (`keep`, `temporary`, or `delete-once-served`), and exactly one of
|
||||
`served_when_path` (deterministic) or `served_when` (classifier-judged, free
|
||||
text), mirroring the rulebook rule's own fields. A directory-rule aggregate
|
||||
entry SHALL carry the same `lifecycle` object shape as a file entry, with its
|
||||
`path` set to the matched directory.
|
||||
|
||||
#### Scenario: A file-rule match carries the lifecycle object
|
||||
|
||||
- **WHEN** a file matches a `temporary`-lifetime file rule
|
||||
- **THEN** its shortlist/report entry includes a `lifecycle` object with `rule_ref`, `lifetime: "temporary"`, and no `served_when`/`served_when_path` (temporary entries are keyed on age, not a served signal)
|
||||
|
||||
#### Scenario: A delete-once-served match carries exactly one served field
|
||||
|
||||
- **WHEN** a file matches a `delete-once-served` rule using `served_when_path`
|
||||
- **THEN** its `lifecycle` object includes `served_when_path` and omits `served_when`, never both
|
||||
|
||||
#### Scenario: A directory-rule aggregate entry carries the same shape
|
||||
|
||||
- **WHEN** a directory rule produces one aggregate shortlist entry
|
||||
- **THEN** that entry's `lifecycle` object has the same fields as a file entry's, with `path` set to the directory
|
||||
|
||||
### Requirement: Promotion-Candidates Section Schema
|
||||
|
||||
The top-level `promotion_candidates` array SHALL contain, per candidate, the
|
||||
`path` of the classifier-judged entry it applies to, the recommended
|
||||
`conventions.json` entry `name`, and the convention's one-line `pitch`. This
|
||||
array SHALL be computed only by the deterministic finalize pass (never the
|
||||
model) and SHALL be empty, not omitted, when no candidate applies.
|
||||
|
||||
#### Scenario: A promotion candidate references its source entry and convention
|
||||
|
||||
- **WHEN** an entry with `served_when` has an applicable unadopted convention
|
||||
- **THEN** the `promotion_candidates` array includes an object with that entry's `path`, the convention `name`, and its `pitch`
|
||||
|
||||
#### Scenario: promotion_candidates is model-free
|
||||
|
||||
- **WHEN** the finalize pass computes `promotion_candidates`
|
||||
- **THEN** it does so without any subagent dispatch, using only `conventions.json` and the report's own lifecycle entries
|
||||
|
||||
### Requirement: Exact-Edit Presence Tied to Op-Type
|
||||
|
||||
An entry SHALL include an `exact_edit` object when, and only when, `op_type` is
|
||||
|
|
@ -159,11 +242,14 @@ refuse to apply it to a file changed since `generated_at`. Entries with
|
|||
### Requirement: Exact-Edit Kind Is a Closed Enum
|
||||
|
||||
Every `exact_edit` SHALL carry a `kind` drawn from the closed set
|
||||
`delete-range`, `move-to-archive`, `insert-frontmatter`, `replace-text`, and
|
||||
`dedupe` — one per deterministic op family the PRD names. No other `kind` is
|
||||
permitted. Each `kind` SHALL carry its required sub-fields and SHALL have a fixed
|
||||
inherent `(is_destructive, is_reversible)` characterization that feeds the
|
||||
`safety_tier` derivation:
|
||||
`delete-range`, `move-to-archive`, `insert-frontmatter`, `replace-text`,
|
||||
`dedupe`, `delete`, and `extract-then-delete` — one per deterministic op
|
||||
family the PRD and the lifecycle-aware design name. No other `kind` is
|
||||
permitted. Each `kind` SHALL carry its required sub-fields and SHALL have a
|
||||
fixed inherent `(is_destructive, is_reversible)` characterization that feeds
|
||||
the `safety_tier` derivation, except where noted below that lifecycle kinds'
|
||||
tier also depends on runtime git state and evidence quality (see the
|
||||
Safety-Tier Is Derived Deterministically requirement):
|
||||
|
||||
- `delete-range` SHALL carry `anchor` with `start_line` and `end_line`; it is
|
||||
destructive and irreversible (derives to `confirm`).
|
||||
|
|
@ -181,6 +267,17 @@ inherent `(is_destructive, is_reversible)` characterization that feeds the
|
|||
is lost, so it is non-destructive and reversible and derives to `auto`. (Contrast
|
||||
`delete-range`, which removes content kept nowhere else, is destructive, and
|
||||
derives to `confirm`.)
|
||||
- `delete` SHALL carry a full-file (or, for a directory-rule aggregate entry,
|
||||
whole-directory) `anchor` and a `lifecycle` object (`rule_ref`, `lifetime`,
|
||||
and exactly one of `served_when_path` or `served_when`); it is destructive
|
||||
(git history is the only recovery path) and its `is_reversible`
|
||||
characterization is inherently git-history-dependent rather than fixed —
|
||||
see the Safety-Tier requirement for how its tier is actually derived.
|
||||
- `extract-then-delete` SHALL carry the same `lifecycle` object as `delete`
|
||||
plus an `extraction_target` classification (`repo-durable` or `cross-repo`)
|
||||
and, for `repo-durable`, a target document reference; it has the same
|
||||
destructive/tier characterization as `delete`, gated additionally on the
|
||||
extraction step succeeding before the delete half applies.
|
||||
|
||||
A validator SHALL reject an `exact_edit` whose `kind` is outside the closed set
|
||||
or that omits a required sub-field for its `kind`.
|
||||
|
|
@ -190,7 +287,7 @@ or that omits a required sub-field for its `kind`.
|
|||
- **THEN** the `exact_edit` includes `anchor` (`start_line`, `end_line`) and `dest_path`, and the entry is valid
|
||||
|
||||
#### Scenario: Unknown kind is rejected
|
||||
- **WHEN** an `exact_edit` has a `kind` outside the closed set `delete-range`, `move-to-archive`, `insert-frontmatter`, `replace-text`, `dedupe`
|
||||
- **WHEN** an `exact_edit` has a `kind` outside the closed set `delete-range`, `move-to-archive`, `insert-frontmatter`, `replace-text`, `dedupe`, `delete`, `extract-then-delete`
|
||||
- **THEN** the report is invalid
|
||||
|
||||
#### Scenario: Missing required sub-field is rejected
|
||||
|
|
@ -201,6 +298,15 @@ or that omits a required sub-field for its `kind`.
|
|||
- **WHEN** an `exact_edit` has `kind` = `delete-range`
|
||||
- **THEN** the entry's `is_destructive` is true and `is_reversible` is false, so the derived `safety_tier` is `confirm`
|
||||
|
||||
#### Scenario: delete and extract-then-delete require the lifecycle object
|
||||
- **WHEN** an `exact_edit` has `kind` = `delete` or `kind` = `extract-then-delete`
|
||||
- **THEN** it includes a `lifecycle` object with `rule_ref`, `lifetime`, and exactly one of `served_when_path` or `served_when`; omitting the `lifecycle` object or supplying both `served_when_path` and `served_when` makes the report invalid
|
||||
|
||||
#### Scenario: extract-then-delete requires an extraction_target
|
||||
|
||||
- **WHEN** an `exact_edit` has `kind` = `extract-then-delete`
|
||||
- **THEN** it includes `extraction_target` set to `repo-durable` or `cross-repo`, with a target document reference required when `repo-durable`
|
||||
|
||||
### Requirement: Per-Entry Token Estimate
|
||||
|
||||
Each entry SHALL include a `token_estimate`. In v1, only `raw_tokens` (a
|
||||
|
|
|
|||
Loading…
Reference in New Issue