20 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, 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
checkskill completes a scan and classification pass - THEN it writes one JSON object with
schema_version,tool_version,generated_at,scan,shortlist,entries, andpromotion_candidates
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
Scenario: promotion_candidates is always present, possibly empty
- WHEN a check run has no classifier-judged entry with an applicable unadopted convention
- THEN
promotion_candidatesis written as an empty array, not omitted
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 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, 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
derive_safety_tier(op_type, is_destructive, is_reversible, lifecycle)is evaluated for any input whereop_type=generative, or (for a non-lifecycle op)is_destructive= true, oris_reversible= false - THEN the result is
confirm, neverauto
Scenario: A lifecycle delete with scanner-proven evidence and tracked+clean state derives to auto
- WHEN a
deleteentry'slifecycleargument shows a satisfiedserved_when_pathand the path is tracked and clean - THEN the derived
safety_tierisauto
Scenario: A lifecycle delete with classifier-judged evidence always derives to confirm
- WHEN a
deleteorextract-then-deleteentry'slifecycleargument carriesserved_when(classifier-judged) - THEN the derived
safety_tierisconfirm, regardless of tracked/clean state
Scenario: A lifecycle delete on a dirty or untracked path derives to confirm
- WHEN a
deleteentry's evidence is scanner-proven but the path is dirty or untracked - THEN the derived
safety_tierisconfirm
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
lifecycleobject withrule_ref,lifetime: "temporary", and noserved_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-servedrule usingserved_when_path - THEN its
lifecycleobject includesserved_when_pathand omitsserved_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
lifecycleobject has the same fields as a file entry's, withpathset 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_whenhas an applicable unadopted convention - THEN the
promotion_candidatesarray includes an object with that entry'spath, the conventionname, and itspitch
Scenario: promotion_candidates is model-free
- WHEN the finalize pass computes
promotion_candidates - THEN it does so without any subagent dispatch, using only
conventions.jsonand 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
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,
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-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.)deleteSHALL carry a full-file (or, for a directory-rule aggregate entry, whole-directory)anchorand alifecycleobject (rule_ref,lifetime, and exactly one ofserved_when_pathorserved_when); it is destructive (git history is the only recovery path) and itsis_reversiblecharacterization is inherently git-history-dependent rather than fixed — see the Safety-Tier requirement for how its tier is actually derived.extract-then-deleteSHALL carry the samelifecycleobject asdeleteplus anextraction_targetclassification (repo-durableorcross-repo) and, forrepo-durable, a target document reference; it has the same destructive/tier characterization asdelete, 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=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,delete,extract-then-delete - 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
Scenario: delete and extract-then-delete require the lifecycle object
- WHEN an
exact_edithaskind=deleteorkind=extract-then-delete - THEN it includes a
lifecycleobject withrule_ref,lifetime, and exactly one ofserved_when_pathorserved_when; omitting thelifecycleobject or supplying bothserved_when_pathandserved_whenmakes the report invalid
Scenario: extract-then-delete requires an extraction_target
- WHEN an
exact_edithaskind=extract-then-delete - THEN it includes
extraction_targetset torepo-durableorcross-repo, with a target document reference required whenrepo-durable
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