14 KiB
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
checkskill completes a scan and classification pass - THEN it writes one JSON object with
schema_version,tool_version,generated_at,scan,shortlist, andentries
Scenario: Clean reads the check timestamp
- WHEN the
cleanskill loads a report - THEN it reads
generated_atand 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
shortlistbut produces no entry inentries
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, andtoken_estimate
Scenario: Op-type and safety-tier are constrained enums
- WHEN an entry is written
- THEN
op_typeisdeterministicorgenerativeandsafety_tierisautoorconfirm
Scenario: Op characterization inputs are present for derivation
- WHEN an entry is written
- THEN
is_destructiveandis_reversibleare present as booleans sosafety_tiercan 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=staleandcategory.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=bloatandcategory.subtype=distill
Scenario: Unknown subtype is rejected
- WHEN a report contains a
category.subtypeoutside 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
contradictedblock is recommended for deterministic deletion in one entry and generative rewrite in another - THEN both entries are valid, each with
op_typeconsistent with its chosenoprather than derived from the shared subtype
Scenario: Op-type and exact-edit consistency is validated
- WHEN an entry has
op_type=generativebut also carries anexact_edit(orop_type=deterministicbut omitsexact_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, andis_reversible= true - THEN the derived
safety_tierisautoand the cleaner applies theexact_editmechanically without a prompt
Scenario: Destructive op derives to confirm
- WHEN an op removes information not preserved elsewhere (
is_destructive= true, e.g. adelete-range) - THEN the derived
safety_tierisconfirmregardless ofop_type
Scenario: Generative op derives to confirm
- WHEN an entry has
op_type=generative - THEN the derived
safety_tierisconfirmand 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 whereop_type=generative, oris_destructive= true, oris_reversible= false - THEN the result is
confirm, neverauto
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_editwith the edit operation andexpected_sha256
Scenario: Generative entry omits exact edit
- WHEN an entry has
op_type=generative - THEN it has no
exact_editfield 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-rangeSHALL carryanchorwithstart_lineandend_line; it is destructive and irreversible (derives toconfirm).move-to-archiveSHALL carryanchor(start_line,end_line) anddest_path; it is non-destructive and reversible (derives toauto).insert-frontmatterSHALL carry the frontmatterkeyandvalueto inject (for examplehygiene: frozen); it is non-destructive and reversible (derives toauto).replace-textSHALL carryanchor(start_line,end_line), amatchstring, and areplacementstring; it is non-destructive and reversible (derives toauto).dedupeSHALL carryanchor(start_line,end_line) of the removed duplicate span and acanonical_refto the kept location; because the removed span is an exact duplicate preserved verbatim atcanonical_ref, no information is lost, so it is non-destructive and reversible and derives toauto. (Contrastdelete-range, which removes content kept nowhere else, is destructive, and derives toconfirm.)
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=deterministicwith anexact_editofkind=move-to-archive - THEN the
exact_editincludesanchor(start_line,end_line) anddest_path, and the entry is valid
Scenario: Unknown kind is rejected
- WHEN an
exact_edithas akindoutside the closed setdelete-range,move-to-archive,insert-frontmatter,replace-text,dedupe - THEN the report is invalid
Scenario: Missing required sub-field is rejected
- WHEN an
exact_editofkind=replace-textomits itsmatchorreplacementfield - THEN the report is invalid
Scenario: Kind characterization feeds the safety-tier derivation
- WHEN an
exact_edithaskind=delete-range - THEN the entry's
is_destructiveis true andis_reversibleis false, so the derivedsafety_tierisconfirm
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_tokensis present (a local-tokenizer count, no API call) andinjection_frequencyandweighted_tokensmay benullor 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-sessionandweighted_tokensreflects 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-demandandweighted_tokensreflects 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.mdupdate 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