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

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