252 lines
14 KiB
Markdown
252 lines
14 KiB
Markdown
# Spec: report-schema
|
|
|
|
## Purpose
|
|
|
|
Defines the machine report schema — the frozen contract that the `check` skill
|
|
produces and every downstream component (`clean`, `sweep`, token-estimator,
|
|
schema-validator) consumes. All field shapes, enum values, and derivation
|
|
semantics are fixed here; any change requires updating `invariants.md` with
|
|
explicit human approval.
|
|
|
|
## 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, 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.
|
|
|
|
#### 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`
|
|
|
|
#### 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
|
|
|
|
### Requirement: Shortlist Precedes Entries
|
|
|
|
The `shortlist` SHALL contain the project-root-relative paths the deterministic
|
|
scanner surfaced as candidates. Every `entries[].path` SHALL be a member of
|
|
`shortlist`. The scanner produces `shortlist`; the AI pass produces `entries`.
|
|
|
|
#### Scenario: Entry path is a shortlist member
|
|
- **WHEN** the AI pass adds an entry for a file
|
|
- **THEN** that file's path already appears in `shortlist`
|
|
|
|
#### Scenario: A shortlisted file may be cleared
|
|
- **WHEN** the AI pass judges a shortlisted file as not stale and not bloated
|
|
- **THEN** the path remains in `shortlist` but produces no entry in `entries`
|
|
|
|
### Requirement: Per-File Entry Fields
|
|
|
|
Each entry SHALL contain `path`, `category`, `signals`, `op`, `op_type`,
|
|
`is_destructive`, `is_reversible`, `safety_tier`, and `token_estimate`. The
|
|
`op_type` SHALL be one of `deterministic` or `generative`. The `is_destructive`
|
|
and `is_reversible` fields SHALL be booleans that objectively characterize the
|
|
chosen `op`. An op SHALL be considered **destructive if and only if it removes or
|
|
overwrites information that is not preserved elsewhere in the repository**;
|
|
text-level line removal is not by itself destructive when the information
|
|
survives (for example a `dedupe` whose removed span is preserved verbatim at
|
|
`canonical_ref` is non-destructive). The `safety_tier` SHALL be one of `auto` or `confirm` and SHALL be
|
|
derived (see the Op-Type and Safety-Tier Semantics requirement), not assigned by
|
|
the model. The `signals` field SHALL list the objective scanner facts that
|
|
support the classification.
|
|
|
|
#### Scenario: Entry carries the full classification
|
|
- **WHEN** the AI pass classifies a candidate file
|
|
- **THEN** the entry includes `path`, `category`, `signals`, `op`, `op_type`, `is_destructive`, `is_reversible`, `safety_tier`, and `token_estimate`
|
|
|
|
#### Scenario: Op-type and safety-tier are constrained enums
|
|
- **WHEN** an entry is written
|
|
- **THEN** `op_type` is `deterministic` or `generative` and `safety_tier` is `auto` or `confirm`
|
|
|
|
#### Scenario: Op characterization inputs are present for derivation
|
|
- **WHEN** an entry is written
|
|
- **THEN** `is_destructive` and `is_reversible` are present as booleans so `safety_tier` can be computed deterministically from them
|
|
|
|
### Requirement: Category Taxonomy Is a Closed Enum
|
|
|
|
The `category` field SHALL be an object with `class` and `subtype`. The `class`
|
|
SHALL be `stale` or `bloat`. When `class` is `stale`, `subtype` SHALL be one of
|
|
`contradicted`, `orphaned`, `superseded`, `provisional`, `completed-in-place`,
|
|
or `duplicated`. When `class` is `bloat`, `subtype` SHALL be one of `distill`,
|
|
`split`, or `freeze`. No other classes or subtypes are permitted.
|
|
|
|
#### Scenario: Stale file is categorized with a stale subtype
|
|
- **WHEN** a doc references a file that no longer exists
|
|
- **THEN** its entry has `category.class` = `stale` and `category.subtype` = `orphaned`
|
|
|
|
#### Scenario: Bloated file is categorized with a bloat subtype
|
|
- **WHEN** a doc is a long resolved-problem narrative that should be condensed
|
|
- **THEN** its entry has `category.class` = `bloat` and `category.subtype` = `distill`
|
|
|
|
#### Scenario: Unknown subtype is rejected
|
|
- **WHEN** a report contains a `category.subtype` outside the closed enum
|
|
- **THEN** the report is invalid
|
|
|
|
### Requirement: Op-Type Is a Property of the Chosen Op
|
|
|
|
`op_type` SHALL describe the operation the classifier selected and SHALL be
|
|
consistent with it: it SHALL NOT be a free field, and it SHALL NOT be looked up
|
|
from `category.subtype` (a single subtype may map to either a deterministic or a
|
|
generative op depending on the chosen `op`). An entry SHALL include `exact_edit`
|
|
when, and only when, `op_type` is `deterministic`. An entry with `op_type` =
|
|
`generative` SHALL NOT include `exact_edit`. This biconditional SHALL be
|
|
validated deterministically; an entry that violates it is invalid.
|
|
|
|
#### Scenario: Same subtype admits either op-type
|
|
- **WHEN** a `contradicted` block is recommended for deterministic deletion in one entry and generative rewrite in another
|
|
- **THEN** both entries are valid, each with `op_type` consistent with its chosen `op` rather than derived from the shared subtype
|
|
|
|
#### Scenario: Op-type and exact-edit consistency is validated
|
|
- **WHEN** an entry has `op_type` = `generative` but also carries an `exact_edit` (or `op_type` = `deterministic` but omits `exact_edit`)
|
|
- **THEN** the entry 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.
|
|
|
|
#### 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** `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
|
|
- **THEN** the result is `confirm`, never `auto`
|
|
|
|
### Requirement: Exact-Edit Presence Tied to Op-Type
|
|
|
|
An entry SHALL include an `exact_edit` object when, and only when, `op_type` is
|
|
`deterministic`. The `exact_edit` SHALL be mechanically applicable and SHALL
|
|
carry a content fingerprint (`expected_sha256`) so the cleaner's mtime guard can
|
|
refuse to apply it to a file changed since `generated_at`. Entries with
|
|
`op_type` = `generative` SHALL omit `exact_edit`.
|
|
|
|
#### Scenario: Deterministic entry includes exact edit
|
|
- **WHEN** an entry has `op_type` = `deterministic`
|
|
- **THEN** it includes an `exact_edit` with the edit operation and `expected_sha256`
|
|
|
|
#### Scenario: Generative entry omits exact edit
|
|
- **WHEN** an entry has `op_type` = `generative`
|
|
- **THEN** it has no `exact_edit` field and the edit is produced at clean time
|
|
|
|
#### Scenario: mtime guard refuses a stale edit
|
|
- **WHEN** a file's current content hash differs from the entry's `expected_sha256`
|
|
- **THEN** the cleaner skips the cached edit and recommends re-analysis
|
|
|
|
### 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` 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`.)
|
|
|
|
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`
|
|
- **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`
|
|
|
|
### Requirement: Per-Entry Token Estimate
|
|
|
|
Each entry SHALL include a `token_estimate`. In v1, only `raw_tokens` (a
|
|
local-tokenizer count of the removed or reduced span, with no API call) SHALL be
|
|
required. The `injection_frequency` (`per-session` or `on-demand`) and
|
|
`weighted_tokens` (`raw_tokens` adjusted by injection frequency) fields SHALL be
|
|
optional and MAY be `null` or omitted in v1; populating them (injection-frequency
|
|
weighting plus bottom-up rollup) is the v2 bonus per the PRD build order. When
|
|
populated, auto-injected files SHALL be weighted as real per-session savings and
|
|
on-demand docs SHALL be weighted as theoretical-max savings. Roll-up to category
|
|
and total SHALL be computed bottom-up from the entries.
|
|
|
|
#### Scenario: v1 entry carries only the required raw token count
|
|
- **WHEN** a v1 check writes an entry without the weighting fields
|
|
- **THEN** `token_estimate.raw_tokens` is present (a local-tokenizer count, no API call) and `injection_frequency` and `weighted_tokens` may be `null` or omitted
|
|
|
|
#### Scenario: Auto-injected file weighted per session
|
|
- **WHEN** the weighting fields are populated (v2) and the affected file is auto-injected (for example `CLAUDE.md`)
|
|
- **THEN** `injection_frequency` = `per-session` and `weighted_tokens` reflects real per-session savings
|
|
|
|
#### Scenario: On-demand doc weighted as theoretical max
|
|
- **WHEN** the weighting fields are populated (v2) and the affected file is read only on demand
|
|
- **THEN** `injection_frequency` = `on-demand` and `weighted_tokens` reflects theoretical-max savings
|
|
|
|
### Requirement: Schema Is a Frozen Contract
|
|
|
|
The report schema SHALL be treated as a frozen contract that every other
|
|
component consumes. The freeze SHALL be enforced by `invariants.md` (which
|
|
carries the schema-freeze and `safety_tier` derivation invariants); that file is
|
|
the authoritative enforcement mechanism for this contract. Any change to a field,
|
|
an enum value, or a documented semantic SHALL require updating `invariants.md`
|
|
with explicit human approval before it takes effect. Schema-shape verification
|
|
SHALL be carried by a schema-validator script and hand-authored **schema
|
|
fixtures** (one valid machine report, one invalid) under `examples/golden/`;
|
|
these schema-shape fixtures are distinct from classifier golden examples, which
|
|
are deferred until the `check` change exists.
|
|
|
|
#### Scenario: Schema change requires invariants update
|
|
- **WHEN** a contributor proposes adding, renaming, or removing a field or enum value
|
|
- **THEN** the change is accompanied by an `invariants.md` update with human approval before it takes effect
|
|
|
|
#### Scenario: Validator accepts a well-formed report and rejects a malformed one
|
|
- **WHEN** the schema-validator script runs against the valid and invalid schema fixtures under `examples/golden/`
|
|
- **THEN** it accepts the valid machine report and rejects the invalid one
|
|
|
|
#### Scenario: Components rely on the frozen shape
|
|
- **WHEN** the `clean`, `sweep`, or token-estimator components are built
|
|
- **THEN** they consume the report without re-deriving its structure
|