cc-os/plugins/os-doc-hygiene/openspec/changes/lifecycle-aware-doc-hygiene/specs/report-schema/spec.md

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 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