cc-os/plugins/os-doc-hygiene/openspec/changes/archive/2026-06-24-add-clean/specs/doc-clean/spec.md

13 KiB
Raw Blame History

ADDED Requirements

Requirement: Per-File Transaction with Content-Hash Guard

The patch applier SHALL apply all entries for a given file as a single in-memory transaction. It SHALL read the file's bytes once, verify that sha256(bytes) matches expected_sha256 for every anchored entry on that file, and on any mismatch skip the entire file batch with reason content-changed-since-check (recommending re-analysis) while continuing to process other files. It SHALL apply anchored edits in memory in descending order by anchor.start_line, and SHALL apply insert-frontmatter last (after all anchor edits) because it prepends and shifts line numbers. It SHALL write the file once and stage once after all in-memory edits succeed.

Scenario: Mismatch skips the entire file batch

  • WHEN a file's current sha256 differs from any anchored entry's expected_sha256
  • THEN the applier skips the entire file batch with reason content-changed-since-check, leaves the file untouched, and continues processing other files

Scenario: Descending-anchor application preserves line offsets

  • WHEN a file has multiple anchored entries (e.g., line 40 and line 10)
  • THEN the applier applies the line-40 edit first, then the line-10 edit, so the second edit lands on the correct line in the still-unmodified earlier portion of the file

Scenario: insert-frontmatter is applied last

  • WHEN a file has both an anchored edit and an insert-frontmatter entry
  • THEN the anchored edit is applied first in memory, and insert-frontmatter is appended last (after all anchor edits) before the single file write

Scenario: Write and stage occur once per file

  • WHEN a file has multiple entries applied in memory
  • THEN the file is written to disk exactly once and staged exactly once after all in-memory edits succeed

Requirement: insert-frontmatter Re-Derives Freshness at Apply Time

The patch applier SHALL re-derive frontmatter state at apply time for insert-frontmatter entries (which carry no expected_sha256 because they have no anchor) by re-reading the file and parsing its frontmatter. If the target key is already present with the target value, the applier SHALL treat the operation as an idempotent no-op (success, no write). If the target key is present with a different value, the applier SHALL skip the entry with reason frontmatter-key-conflict and SHALL NOT overwrite the existing value. If the key is absent, the applier SHALL insert key: value (creating a --- block if none exists) as the last in-memory step.

Scenario: Key already present with target value — idempotent no-op

  • WHEN the applier processes an insert-frontmatter entry and the file already contains hygiene: frozen
  • THEN the applier treats the entry as a success and does not modify the file

Scenario: Key present with conflicting value — skip

  • WHEN the applier processes an insert-frontmatter entry and the file already contains hygiene: review
  • THEN the applier skips the entry with reason frontmatter-key-conflict and does not overwrite the existing value

Scenario: Key absent — insert

  • WHEN the applier processes an insert-frontmatter entry and the file has no hygiene frontmatter key
  • THEN the applier inserts the key-value pair (creating a --- block if needed) as the last in-memory step before writing

Requirement: Incompatible-Ops Detection Skips the File

The patch applier SHALL detect incompatible-op combinations on a per-file basis before performing any edits. If move-to-archive co-occurs with any other op on the same file, or if any anchor ranges overlap, the applier SHALL skip the entire file with reason incompatible-ops-on-file and SHALL recommend re-analysis. The applier SHALL NOT attempt to compose or sequence incompatible ops in v1.

Scenario: move-to-archive with another op — skip

  • WHEN a file has both a move-to-archive entry and a replace-text entry
  • THEN the applier skips the entire file with reason incompatible-ops-on-file and records a re-analysis recommendation

Scenario: Overlapping anchors — skip

  • WHEN two entries on the same file have anchor ranges that overlap (e.g., lines 515 and lines 1020)
  • THEN the applier skips the entire file with reason incompatible-ops-on-file

Scenario: Non-conflicting entries on a file proceed normally

  • WHEN a file has two entries with non-overlapping anchors and no move-to-archive
  • THEN the applier applies both entries via the descending-anchor transaction

Requirement: Safety-Tier Gating Before Any Mutation

The clean skill SHALL partition report entries by safety tier (read from the report — never recomputed) into auto entries (applied without prompt) and confirm entries (escalated before any mutation). It SHALL present all confirm-tier entries as a single batch-confirm list showing path, category, op, token count, and rationale with per-entry opt-out, visually distinguishing irreversible delete-range entries from reversible entries. The approved set SHALL be all auto entries plus any user-approved confirm entries. The gate SHALL run identically under sweep — the /hygiene sweep convenience path SHALL NOT bypass invariant #7.

Scenario: auto entries apply without prompt

  • WHEN the report contains only auto-tier entries (move-to-archive, insert-frontmatter, replace-text, dedupe)
  • THEN the clean skill applies them without presenting a confirm prompt

Scenario: confirm entries escalate before any mutation

  • WHEN the report contains confirm-tier entries (delete-range or any generative op)
  • THEN the clean skill presents a batch-confirm list before any file is modified, and applies only user-approved entries

Scenario: per-entry opt-out is respected

  • WHEN the user approves some confirm entries and opts out of others
  • THEN the skill applies the approved entries and skips the opted-out entries

Scenario: sweep does not bypass the gate

  • WHEN the user runs /hygiene sweep and the report contains confirm-tier entries
  • THEN the confirm gate runs identically to a standalone /hygiene clean

Requirement: Git-Safe Single Commit

The clean skill SHALL produce exactly one git commit per run. Before any mutation it SHALL resolve the project root via StateStore, run git status --porcelain, and if the tree is dirty, SHALL automatically create a WIP checkpoint commit of the user's work before proceeding, so the cleanup commit remains exactly one. The cleanup commit SHALL be created via git-context commit-apply --message-stdin with a generated message summarizing auto/confirmed/skipped counts and op breakdown. Staging SHALL be precise: for non-move ops the skill calls git add <staged_paths> from the applier result; for move-to-archive the applier calls git mv (staging both sides) and the skill SHALL NOT git add the destination path again. The skill SHALL NEVER use git add -A or git add .. If a hard failure occurs (applier exit 2, git mv fail, or write error), the skill SHALL roll back via git restore/reset to the pre-run baseline and abort with a structured error. Partial success (some file batches guard-skipped) SHALL NOT trigger rollback — the skill SHALL commit what applied and report skipped files. Untracked candidate docs SHALL be skipped and reported (tracked-files-only). last_clean SHALL be stamped to the commit instant, not the run-start instant.

Scenario: Clean tree produces exactly one commit

  • WHEN the user runs /hygiene clean with a clean git tree
  • THEN the run produces exactly one git commit containing all applied edits

Scenario: Dirty tree gets a WIP checkpoint then one cleanup commit

  • WHEN the user runs /hygiene clean with unstaged changes in the working tree
  • THEN the skill auto-creates a WIP checkpoint commit of the user's work, then produces exactly one cleanup commit — two commits total, cleanup still exactly one

Scenario: All-skipped produces zero commits

  • WHEN all entries are guard-skipped (every file changed since check)
  • THEN the skill produces zero commits and reports all files as skipped with re-analysis recommended

Scenario: Hard failure triggers rollback

  • WHEN a write error or applier exit 2 occurs mid-run
  • THEN the skill rolls back to the pre-run baseline and aborts with a structured error; no partial commit is created

Scenario: Partial success commits what applied

  • WHEN some file batches are guard-skipped (applier exit 1) and others succeed
  • THEN the skill commits the applied edits and reports the skipped files, without rolling back the applied changes

Scenario: move-to-archive is not double-staged

  • WHEN the applier stages a move-to-archive via git mv (both source and dest)
  • THEN the skill does not call git add on the destination path again

Requirement: Clean Skill Orchestration

The hygiene-clean skill SHALL load the current report via StateStore.read_report (if none, it SHALL tell the user to run /hygiene check and stop). It SHALL re-validate the loaded report via validate_report.py (if invalid, stop). It SHALL apply a scope/category filter to entries. It SHALL partition entries into auto+deterministic, confirm+deterministic (i.e., delete-range), and generative (always confirm) subsets. It SHALL gate confirm entries before any mutation. It SHALL run the git preflight (clean check / WIP checkpoint). It SHALL apply deterministic approved entries via patch_applier.py. It SHALL dispatch generative approved entries to a Sonnet subagent via workflows/distill.md (LOOP-GUARD subagent pointer). It SHALL stage precisely and commit once via git-context commit-apply --message-stdin. It SHALL stamp last_clean. It SHALL surface applied/skipped (with re-analysis notes) / confirmed counts and the commit SHA.

Scenario: No report — prompt to check first

  • WHEN the user runs /hygiene clean and no report exists in .dochygiene/
  • THEN the skill tells the user to run /hygiene check first and stops without modifying anything

Scenario: Invalid report — stop

  • WHEN the loaded report fails re-validation
  • THEN the skill stops and reports the validation error without modifying anything

Scenario: Zero in-scope entries — no-op report

  • WHEN all entries are filtered out by scope/category
  • THEN the skill reports zero in-scope entries and exits without modifying the tree or creating a commit

Scenario: Full pipeline succeeds

  • WHEN the report contains valid entries and the user approves all confirm entries
  • THEN the skill applies deterministic entries via the applier, dispatches generative entries to Sonnet, stages precisely, commits once, stamps last_clean, and surfaces the commit SHA

Requirement: Generative Distillation via Live-Read Sonnet Subagent

For approved generative entries, the clean skill SHALL confirm the file still exists (a preceding move-to-archive on the same file would have removed it). It SHALL read the live file contents at dispatch time (not from the report cache) to guarantee freshness, because generative entries carry no expected_sha256. It SHALL dispatch to a Sonnet subagent pointing at workflows/distill.md (LOOP-GUARD — the subagent reads that workflow, not SKILL.md, to avoid recursion). The subagent SHALL return new prose (or new-primary + archived-section for split). The skill SHALL write and stage the result. A file with both a generative entry and a deterministic entry SHALL be treated as incompatible-ops-on-file and skipped.

Scenario: File missing at dispatch time — skip

  • WHEN a generative entry targets a file that no longer exists (e.g., moved by a prior step)
  • THEN the skill skips the generative entry and reports it as skipped

Scenario: Live read ensures freshness

  • WHEN the skill dispatches a generative entry to Sonnet
  • THEN it reads the file's current bytes immediately before dispatch, not from the cached report span

Scenario: Generative + deterministic on same file — incompatible

  • WHEN a file has both a generative entry and a deterministic entry
  • THEN the applier (or skill) skips the file with reason incompatible-ops-on-file and recommends re-analysis

Requirement: Sweep Composes Check Then Clean at Command Level

/hygiene sweep SHALL invoke the hygiene-check skill followed by the hygiene-clean skill, passing --scope and --category to both. The sweep SHALL NOT produce a double-commit: check writes only to the gitignored .dochygiene/ directory and never commits; the single cleanup commit from clean is the only git commit the sweep produces. The sweep routing SHALL live in commands/hygiene.md, not in a third skill or by having the clean skill invoke the check skill internally.

Scenario: Sweep routes through the command, not a third skill

  • WHEN the user runs /hygiene sweep
  • THEN commands/hygiene.md invokes hygiene-check then hygiene-clean sequentially, passing shared flags

Scenario: Sweep produces at most one cleanup commit

  • WHEN the user runs /hygiene sweep on a clean tree
  • THEN at most one git commit is produced (the cleanup commit from clean); check writes nothing to git