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

17 KiB
Raw Blame History

doc-clean Specification

Purpose

TBD - created by archiving change add-clean. Update Purpose after archive.

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, delete, and extract-then-delete 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 /os-doc-hygiene:sweep convenience path SHALL NOT bypass invariant #7. Regardless of the tier recorded in the report, any delete or extract-then-delete entry SHALL be re-verified at apply time per the lifecycle-deletion tier matrix (tracked

  • clean required for auto) before being applied without a prompt.

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, delete, extract-then-delete, 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 /os-doc-hygiene:sweep and the report contains confirm-tier entries
  • THEN the confirm gate runs identically to a standalone /os-doc-hygiene:clean

Scenario: A report-tier auto delete is downgraded if runtime state has changed

  • WHEN a delete entry was tiered auto in the report but the applier's runtime check finds the file is now dirty or untracked
  • THEN the applier does not apply it silently; it is skipped and reported for re-analysis, never trusted from the cached tier

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, including lifecycle delete/extract-then-delete counts when present. Staging SHALL be precise: for non-move, non-delete 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; for delete and the delete half of extract-then-delete the applier calls git rm (staging the removal itself) and the skill SHALL NOT separately stage the removed path. The skill SHALL NEVER use git add -A or git add .. If a hard failure occurs (applier exit 2, git mv/git rm 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) except where a lifecycle rule explicitly escalates an untracked delete to confirm and the user approves it. 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 /os-doc-hygiene:clean with a clean git tree
  • THEN the run produces exactly one git commit containing all applied edits, including any lifecycle deletes

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

  • WHEN the user runs /os-doc-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, applier exit 2, or a git rm failure 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

Scenario: A lifecycle delete is not double-staged

  • WHEN the applier stages a delete via git rm
  • THEN the skill does not separately call git add or any other staging command on the removed path

Requirement: Applier Applies Delete and Extract-Then-Delete Under the Tier Matrix

The patch applier SHALL support two new op kinds, delete and extract-then-delete. For both, immediately before applying, it SHALL re-run git ls-files <path> and a dirty check against that specific path — never trusting the cached report tier or rule claim — and SHALL apply the tier matrix from the lifecycle-deletion spec to decide whether the entry may proceed as auto or must be treated as confirm (already gated upstream by the clean skill). delete SHALL perform a git rm (recursive for a directory-rule aggregate entry) staged into the run's single hygiene commit. extract-then-delete SHALL first complete its generative extraction step (repo-durable via the existing live-read Sonnet distillation path writing into an ADR/CLAUDE.md/docs target, or cross-repo via /os-vault:write) and SHALL only perform the git rm once extraction has succeeded; both steps SHALL land in the same single hygiene commit, or, on extraction failure, neither SHALL be applied for that entry (skip, not a run-level hard failure, unless the failure matches an existing hard-failure trigger).

Scenario: delete performs a true git rm at apply time

  • WHEN the applier applies a delete entry
  • THEN it re-verifies tracked/clean status via git ls-files and a dirty check, then performs a git rm staged into the single hygiene commit

Scenario: extract-then-delete only deletes after extraction succeeds

  • WHEN the applier applies an extract-then-delete entry
  • THEN it completes the extraction write (ADR/CLAUDE.md/docs or vault) first, and performs the git rm only after that write succeeds

Scenario: A failed extraction skips the delete for that entry

  • WHEN the extraction step of an extract-then-delete entry fails
  • THEN the delete is not applied for that entry, the entry is reported as skipped, and the run is not treated as a hard failure unless the failure matches an existing hard-failure trigger (applier exit 2, write error)

Scenario: Directory-rule aggregate delete removes the whole directory

  • WHEN the applier applies a delete entry whose path is a directory-rule aggregate entry
  • THEN it performs a recursive git rm removing the entire matched directory in one operation staged into the single hygiene commit

Requirement: Clean Skill Orchestration

The clean skill SHALL load the current report via StateStore.read_report (if none, it SHALL tell the user to run /os-doc-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 /os-doc-hygiene:clean and no report exists in .dochygiene/
  • THEN the skill tells the user to run /os-doc-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

/os-doc-hygiene:sweep SHALL invoke the check skill followed by the 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 its own sweep skill (skills/sweep/SKILL.md), not by having the clean skill invoke check internally.

Scenario: Sweep routes through its own skill, not by nesting clean inside check

  • WHEN the user runs /os-doc-hygiene:sweep
  • THEN the sweep skill invokes the check skill then the clean skill sequentially, passing shared flags

Scenario: Sweep produces at most one cleanup commit

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