14 KiB
doc-check Specification
Purpose
TBD - created by archiving change add-check. Update Purpose after archive.
Requirements
Requirement: /hygiene Command Surface
The plugin SHALL provide a single /hygiene command that dispatches on its
arguments. /os-doc-hygiene:check [--scope <glob-or-path>] [--category <class|subtype>]
SHALL invoke the check skill. /os-doc-hygiene:status SHALL read and report the
lifecycle timestamps (last_check, last_clean, last_reminded) and whether a
report exists, using no scan and no model. /os-doc-hygiene:clean and /os-doc-hygiene:sweep
SHALL be reserved and SHALL report that they are not yet implemented (Phase 4). No
arguments or unknown arguments SHALL print usage plus the current status.
Scenario: Check dispatches to the skill
- WHEN the user runs
/os-doc-hygiene:check - THEN the command invokes the
checkskill, passing through any--scopeor--categoryflag
Scenario: Status is read-only
- WHEN the user runs
/os-doc-hygiene:status - THEN the command reports
last_check,last_clean,last_reminded, and whether a report exists, without running a scan or any model
Scenario: Clean and sweep are reserved
- WHEN the user runs
/os-doc-hygiene:cleanor/os-doc-hygiene:sweep - THEN the command reports that the subcommand is not yet implemented (Phase 4) and does not mutate anything
Scenario: Unknown arguments print usage
- WHEN the user runs
/hygienewith no arguments or an unrecognized subcommand - THEN the command prints usage and the current status
Requirement: Check Skill Orchestrates Scan, Classification, and Report Writing
The check skill SHALL orchestrate the check pipeline: load the lifecycle
rulebook (global plus any project override), run the deterministic scanner
(consuming the rulebook so directory-rule matches prune the walk and
lifecycle signals are attached per the lifecycle-rulebook spec), dispatch
a Sonnet subagent for judgment-only classification of the signal-bearing
candidates, run the deterministic finalize pass (which also computes
promotion_candidates from conventions.json), validate, write the report
pair, and stamp last_check. The skill SHALL run all non-judgment steps as
deterministic scripts with no model (invariant #6). Zero-signal shortlisted
files SHALL be treated as presumptively cleared: they SHALL remain in the
shortlist, produce no entries, and SHALL NOT be read by the model. A
--scope argument SHALL narrow the scanner; a --category argument SHALL
filter which entries are produced after classification; both SHALL be
recorded in the human-report header.
Scenario: Skill runs the full pipeline
- WHEN the
checkskill runs - THEN it loads the rulebook, scans (deterministic, rulebook-aware), classifies signal-bearing candidates (Sonnet), finalizes (deterministic, including promotion candidates), validates (deterministic), writes the report pair (deterministic), and stamps
last_check(deterministic)
Scenario: Zero-signal files are not read by the model
- WHEN a shortlisted file carries no scanner signals
- THEN it remains in the shortlist, produces no entry, and is not read by the classification model
Scenario: Scope and category are recorded and applied
- WHEN the user passes
--scope docs/**/*.mdand--category bloat - THEN the scanner is narrowed by the scope, only
bloatentries are produced after classification, and both the scope and the category are recorded in the human-report header
Scenario: Rulebook load failure is a hard failure, not a silent skip
- WHEN the rulebook loader hard-fails (unparseable JSON or unknown
schema_versionin either rulebook file) - THEN the check skill stops and reports the rulebook error before running the scanner, rather than proceeding with lifecycle signals silently disabled
Requirement: Scanner Consumes the Rulebook for Pruning and Lifecycle Signals
The deterministic scanner SHALL consult the loaded rulebook during its walk.
A directory-rule match (including IGNORE-surface entries) SHALL prune the
walk beneath that directory per the lifecycle-rulebook spec. A file-rule
match SHALL attach a lifecycle signal to that file's shortlist entry. These
lifecycle signals SHALL flow into the classification subagent as a new
signal class alongside the pre-existing stale/bloat signals, and MAY drive
op/op_type selection toward delete or extract-then-delete per the
lifecycle-deletion spec.
Scenario: A directory-rule prune is reflected in the scan artifact
- WHEN the scanner encounters a directory matching a directory rule
- THEN the scan artifact reflects the prune (no files beneath it are in
files_scanned), and, for non-IGNORE directory rules, exactly one aggregate shortlist entry appears for that directory
Scenario: A file-rule lifecycle signal reaches the classifier
- WHEN a file matches a file-rule with
lifetime: delete-once-served - THEN the classification subagent receives the lifecycle signal (rule reference, lifetime, served_when/served_when_path) as part of that file's signals, verbatim, per the existing "signals are passed through verbatim" contract
Requirement: Report Gains a Promotion-Candidates Section
The machine and human reports produced by :check SHALL include a
promotion_candidates section (top-level, sibling to entries), populated
deterministically by the finalize pass from conventions.json for every
classifier-judged lifecycle entry with an applicable, not-yet-adopted
convention. This section SHALL be present (possibly empty) on every run,
including runs with no lifecycle entries.
Scenario: A run with an applicable convention names it in both reports
- WHEN a classifier-judged entry has an applicable, unadopted convention
- THEN both the machine report's
promotion_candidatesarray and the human report show the candidate with its one-line pitch
Scenario: A run with no applicable conventions still has the section, empty
- WHEN no classifier-judged entry has an applicable unadopted convention
- THEN
promotion_candidatesis present as an empty array/section rather than omitted
Requirement: Classification Subagent Returns Judgment-Only Proposals
The Sonnet classification subagent SHALL return, per signal-bearing candidate, a slim
proposal containing only judgment fields: category (class and subtype from the
closed enum, justified by cited signals), the scanner signals passed through
verbatim with an optional one-line gloss in detail, op (a human sentence),
op_type (deterministic or generative, a property of the chosen op per invariant
#11), and confidence. When op_type is deterministic, the proposal SHALL carry an
exact_edit skeleton (kind plus the kind's required sub-fields and anchor where
required) and SHALL NOT carry expected_sha256, is_destructive, is_reversible, or
safety_tier. When op_type is generative, the proposal SHALL carry no
exact_edit and instead a non-persisted reducible_range so the finalize pass can
count raw_tokens over the real span. Low-confidence hard distinctions
(stale-vs-bloat; destructive-deletion-vs-generative-rewrite) MAY be escalated to Opus.
Scenario: Deterministic proposal carries only the exact-edit skeleton
- WHEN the subagent classifies a file as a deterministic op
- THEN the proposal includes the
exact_editskeleton (kind, required sub-fields,anchorwhere required) and omitsexpected_sha256,is_destructive,is_reversible, andsafety_tier
Scenario: Generative proposal carries a reducible range, not an exact edit
- WHEN the subagent classifies a file as a generative op
- THEN the proposal carries no
exact_editand instead a non-persistedreducible_rangefor the finalize pass to count tokens over
Scenario: Signals are passed through verbatim
- WHEN the subagent emits a proposal
- THEN its
signalsare the scanner's signal names verbatim, with any added wording confined to the optionaldetailgloss
Requirement: Deterministic Finalize Pass Owns the Non-Model Fields
A standalone, model-free finalize pass (report_builder.py) SHALL sit between model
classification and the report write, and SHALL author the four per-entry fields that
the model must not author. For each proposal it SHALL compute
exact_edit.expected_sha256 over the file's current bytes for anchor-bearing kinds,
SHALL set (is_destructive, is_reversible) from KIND_TABLE[kind], SHALL compute
safety_tier by calling derive_safety_tier(op_type, is_destructive, is_reversible)
imported from validate_report.py (the single source of truth, invariant #10), and
SHALL source token_estimate.raw_tokens from the local token estimator
(default_estimator().estimate_for_report(span_text), invariant #6). It SHALL stamp
each entry's generated_at at that file's hash instant and set the envelope
generated_at to the run instant. The model SHALL NOT supply any of these four
fields.
Scenario: The finalize pass computes the content hash and derives the tier
- WHEN the finalize pass processes a deterministic proposal with
kind=move-to-archive - THEN it computes
expected_sha256over the file's current bytes, setsis_destructive= false andis_reversible= true fromKIND_TABLE, and derivessafety_tier=autoviaderive_safety_tier
Scenario: raw_tokens comes from the local estimator, never the model
- WHEN the finalize pass sets a
token_estimate - THEN
raw_tokensis the local estimator's count of the span (no model, no API call), with the weighting fields null in v1
Scenario: The model cannot author the derived fields
- WHEN a proposal arrives from the classification subagent
- THEN
expected_sha256,safety_tier, and (for deterministic ops)is_destructive/is_reversibleare absent from the proposal and are authored only by the finalize pass
Requirement: Validate Before Rollover
The check SHALL validate the assembled report with validate_report.py on a scratch
path (not under .dochygiene/) and SHALL write the report pair only on validator exit
0. Because StateStore.write_report deletes the prior report pair before writing the
new one (invariant #4), validation SHALL NOT run against .dochygiene/, so a
validation failure never destroys the last good report. On a validation failure (exit
- the check SHALL NOT write the report; on an empty shortlist or no signal-bearing
files the check SHALL still write a valid empty-
entriesreport and stamplast_check.
Scenario: Invalid report is never written
- WHEN the assembled report fails validation (exit 1)
- THEN the check does not call
write_report, the prior report pair is preserved, and the offending entries are re-prompted or dropped before re-validating
Scenario: Validation runs on a scratch path
- WHEN the check validates the assembled report
- THEN validation runs against a scratch path outside
.dochygiene/, so the last good report in.dochygiene/is never deleted by a failed run
Scenario: Empty shortlist still produces a valid report and stamp
- WHEN the scanner returns no signal-bearing files
- THEN the check writes a valid report with empty
entriesand stampslast_check
Requirement: Report Pair Is Written and last_check Stamped
On a successful check, the skill SHALL write exactly one machine report
(.dochygiene/report.json) and one human report (.dochygiene/report.md) via
StateStore.write_report (atomic, rollover-bounded to one pair per invariant #4), and
SHALL stamp last_check to the same run instant used as the envelope generated_at.
The human report SHALL be a deterministic skeleton grouping entries by Stale, Bloat,
and Cleared with per-entry path, category, op, tier, token count, and signal, and a
header showing the timestamp, scope, files scanned, and candidate/cleared counts; only
an optional per-entry "why" gloss MAY be model-written.
Scenario: One report pair survives the write
- WHEN the check completes successfully
- THEN exactly one
report.jsonand onereport.mdexist in.dochygiene/, and any prior pair has been rolled over
Scenario: last_check matches the envelope timestamp
- WHEN the check writes the report
- THEN
last_checkis stamped to the same run instant recorded as the envelopegenerated_at
Requirement: Classifier Golden Examples Are Hermetic and Human-Gated
Classifier golden examples SHALL live under examples/golden/classifier/<n>-<name>/
(an input/ fixture tree with stable hashes, an expected.json schema-valid report,
and an optional notes.md), distinct from the schema-shape fixtures
(valid_report.json / invalid_report.json). The golden unit harness SHALL be
hermetic — it SHALL NOT call a live model — and SHALL assert only deterministic,
stable parts: that the scanner emits the expected signals on the right paths, that
each expected.json validates (exit 0), and that the stable fields (category.class,
category.subtype, op_type, derived safety_tier, exact_edit.kind) match a
captured/committed check output. Op-prose and exact anchor line numbers SHALL be
advisory (flagged for review, not hard-failed). The live model-classification
regression SHALL be a separate, manually or agent-invoked harness, not part of the
unit suite. Adding or changing classifier goldens SHALL be human-gated per the
META-RULE.
Scenario: Golden harness makes no live model call
- WHEN the classifier golden unit tests run
- THEN they assert scanner signals,
expected.jsonvalidity, and stable-field matches against a committed capture, with no live model invocation
Scenario: Classifier goldens are distinct from schema fixtures
- WHEN a contributor looks for the schema-shape fixtures versus the classifier goldens
- THEN the schema fixtures (
valid_report.json/invalid_report.json) and the classifier goldens (examples/golden/classifier/) are separate, andexamples/golden/CONTEXT.mddocuments the distinction
Scenario: Changing a golden requires human approval
- WHEN a contributor adds or changes a classifier golden example
- THEN the change is gated on explicit human approval per the META-RULE before it takes effect