12 KiB
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
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: 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: 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
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
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