# 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 5–15 and lines 10–20) - **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 `/os-doc-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 /os-doc-hygiene:sweep and the report contains confirm-tier entries - **THEN** the confirm gate runs identically to a standalone /os-doc-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 ` 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 /os-doc-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 /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 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 `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