204 lines
12 KiB
Markdown
204 lines
12 KiB
Markdown
|
|
# Spec: report-schema (delta)
|
||
|
|
|
||
|
|
## MODIFIED Requirements
|
||
|
|
|
||
|
|
### Requirement: Top-Level Report Envelope
|
||
|
|
|
||
|
|
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, 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`, `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: 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`,
|
||
|
|
`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`).
|
||
|
|
- `move-to-archive` SHALL carry `anchor` (`start_line`, `end_line`) and
|
||
|
|
`dest_path`; it is non-destructive and reversible (derives to `auto`).
|
||
|
|
- `insert-frontmatter` SHALL carry the frontmatter `key` and `value` to inject
|
||
|
|
(for example `hygiene: frozen`); it is non-destructive and reversible (derives
|
||
|
|
to `auto`).
|
||
|
|
- `replace-text` SHALL carry `anchor` (`start_line`, `end_line`), a `match`
|
||
|
|
string, and a `replacement` string; it is non-destructive and reversible
|
||
|
|
(derives to `auto`).
|
||
|
|
- `dedupe` SHALL carry `anchor` (`start_line`, `end_line`) of the removed
|
||
|
|
duplicate span and a `canonical_ref` to the kept location; because the removed
|
||
|
|
span is an exact duplicate preserved verbatim at `canonical_ref`, no information
|
||
|
|
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`.
|
||
|
|
|
||
|
|
#### Scenario: Each kind carries its required fields
|
||
|
|
- **WHEN** an entry has `op_type` = `deterministic` with an `exact_edit` of `kind` = `move-to-archive`
|
||
|
|
- **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`, `delete`, `extract-then-delete`
|
||
|
|
- **THEN** the report is invalid
|
||
|
|
|
||
|
|
#### Scenario: Missing required sub-field is rejected
|
||
|
|
- **WHEN** an `exact_edit` of `kind` = `replace-text` omits its `match` or `replacement` field
|
||
|
|
- **THEN** the report is invalid
|
||
|
|
|
||
|
|
#### Scenario: Kind characterization feeds the safety-tier derivation
|
||
|
|
- **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: 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 `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
|
||
|
|
- **THEN** the derived `safety_tier` is `auto` and the cleaner applies the `exact_edit` mechanically without a prompt
|
||
|
|
|
||
|
|
#### Scenario: Destructive op derives to confirm
|
||
|
|
- **WHEN** an op removes information not preserved elsewhere (`is_destructive` = true, e.g. a `delete-range`)
|
||
|
|
- **THEN** the derived `safety_tier` is `confirm` regardless of `op_type`
|
||
|
|
|
||
|
|
#### Scenario: Generative op derives to confirm
|
||
|
|
- **WHEN** an entry has `op_type` = `generative`
|
||
|
|
- **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** `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`
|
||
|
|
|
||
|
|
## ADDED Requirements
|
||
|
|
|
||
|
|
### 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
|