# Spec: lifecycle-deletion

## ADDED Requirements

### Requirement: Delete Is True Git Deletion in a Dedicated Hygiene Commit

A lifecycle `delete` op SHALL perform a true `git rm` (file or, for a
directory-rule aggregate entry, recursive directory removal) staged into the
same single hygiene commit the run produces. There SHALL be no archive
directory, graveyard branch, or other relocation of the deleted content —
git history SHALL be the sole archive.

#### Scenario: A deleted file is git-rm'd, not moved

- **WHEN** a `delete` op is applied to a tracked file
- **THEN** the file is removed via `git rm` and staged into the run's single hygiene commit, with no copy relocated anywhere in the working tree

#### Scenario: A deleted directory-rule entry is removed recursively

- **WHEN** a `delete` op targets a directory-rule aggregate entry
- **THEN** the entire directory is removed via a recursive `git rm` staged into the same commit

### Requirement: Deletion Autonomy Tier Matrix

Deletion autonomy SHALL be determined by evidence quality and recoverability,
not file type, per the following matrix:

| Case | Behavior |
|---|---|
| IGNORE surface | never walked, never a delete candidate |
| lifetime `keep` | scanned + reported, never deleted |
| tracked + delete rule + clean worktree | auto |
| tracked + delete rule + dirty worktree | confirm |
| untracked + delete rule | confirm |
| no rule match | unmanaged, never deleted |

Tracked/clean status SHALL be verified at runtime via `git ls-files` plus a
dirty check, and SHALL NEVER be trusted from the rule's own claim or from a
cached report field.

#### Scenario: Tracked and clean deletes automatically

- **WHEN** a file matches a delete rule, is tracked, and the worktree for that path is clean
- **THEN** the deletion proceeds without a confirmation prompt

#### Scenario: Tracked but dirty requires confirmation

- **WHEN** a file matches a delete rule, is tracked, but has uncommitted changes
- **THEN** the deletion is escalated to confirm — an uncommitted diff would otherwise be lost with the file

#### Scenario: Untracked requires confirmation

- **WHEN** a file matches a delete rule but is untracked
- **THEN** the deletion is escalated to confirm — there is no git history to recover it from

#### Scenario: Runtime verification never trusts the rule

- **WHEN** a delete op is about to be applied
- **THEN** the applier re-checks tracked/clean status via `git ls-files` and a dirty check at that moment, regardless of what the report or rule previously claimed

#### Scenario: No rule match is never deleted

- **WHEN** a file has no lifecycle rule match
- **THEN** it is never a candidate for deletion on lifecycle grounds

### Requirement: Temporary Tier Retention Semantics

The `temporary` lifetime SHALL use retain-recent-N plus age, not age alone.
`retain_recent` (default 3) SHALL always keep the N most recent matching
entries for a rule regardless of age. An entry ranked `retain_recent + 1` or
older SHALL become eligible for deletion once it exceeds `max_age_days`
(default 3). The retention unit SHALL be the rule's own match granularity: a
file-rule's unit is the individual file; a directory-rule's unit is the
matched directory as a whole (e.g., a run directory), never files nested
inside one such matched directory.

#### Scenario: The 3 newest entries are always kept regardless of age

- **WHEN** a temporary rule has `retain_recent: 3` and five matching entries, the three newest exceeding `max_age_days`
- **THEN** the three newest are kept and only the two oldest (ranked 4th and 5th) are eligible for deletion

#### Scenario: An entry younger than max_age_days is kept even if not in the top N

- **WHEN** a temporary rule's 4th-ranked entry is younger than `max_age_days`
- **THEN** it is not deleted this run

#### Scenario: Directory-rule retention unit is the whole directory

- **WHEN** a directory rule matches `autoresearch/<run-id>/` entries
- **THEN** retain-recent-N and age are computed per matched run directory as a whole, not per file within the newest run

### Requirement: Temporary Tier Age Source

Age for the temporary tier SHALL be computed from the git commit time of the
path's most recent commit, falling back to filesystem mtime only when the
path is untracked. There SHALL be no per-rule `age_source` override field.

#### Scenario: Tracked file age comes from git commit time

- **WHEN** age is computed for a tracked file matching a temporary rule
- **THEN** the age is derived from that file's most recent commit time, not its filesystem mtime

#### Scenario: Untracked file age falls back to mtime

- **WHEN** age is computed for an untracked file matching a temporary rule
- **THEN** the age is derived from the file's filesystem mtime

#### Scenario: No per-rule age_source field exists

- **WHEN** a rule in the rulebook is inspected
- **THEN** it has no `age_source` field — the git-commit-time-with-mtime-fallback behavior is fixed, not configurable per rule

### Requirement: Untracked Directory Entry Age Uses Directory Inode mtime

For an untracked directory matched by a directory rule, age SHALL be
computed from a single `stat()` of the directory inode itself, not a
recursive walk computing the maximum mtime of its contents.

#### Scenario: Directory age is one stat call, not a recursive scan

- **WHEN** age is computed for an untracked directory matching a directory rule
- **THEN** the computation reads only the directory inode's own mtime and does not recurse into or stat any file inside it

### Requirement: delete-once-served Split by Evidence Quality

The `delete-once-served` lifetime SHALL support two mutually exclusive served
signals per rule: `served_when_path`, a deterministic path pattern the
scanner itself can prove satisfied (e.g. a sibling archive directory
existing), and `served_when`, free text describing a condition the
classifier must judge. A rule with `served_when_path` satisfied by the
scanner MAY be deleted under the autonomy tier matrix (i.e., auto when
tracked+clean). A rule relying on `served_when` SHALL ALWAYS be forced to
confirm, regardless of tracked/clean status, because it depends on a model
judgment rather than a provable filesystem fact.

#### Scenario: Scanner-proven served_when_path may auto-delete

- **WHEN** a rule's `served_when_path` condition is satisfied by the filesystem and the matched path is tracked and clean
- **THEN** the deletion may proceed automatically under the tier matrix

#### Scenario: Classifier-judged served_when always forces confirm

- **WHEN** a rule uses `served_when` (free text) and the classifier judges the condition met
- **THEN** the deletion is always escalated to confirm, even if the matched path is tracked and clean

#### Scenario: The LLM may propose but never silently destroy on served_when

- **WHEN** the classifier judges a `served_when` condition satisfied
- **THEN** it produces a proposal for a human to confirm; it never causes an unattended deletion

### Requirement: Extract Modifier Routes Through Existing Knowledge Destinations Only

The `extract` modifier on a deletion SHALL distill durable content before
deleting, routing exclusively through the existing knowledge-routing
destinations: repo-durable residue SHALL be written into an ADR, `CLAUDE.md`,
or a `docs/` file; cross-repo lessons SHALL be written to the SecondBrain
vault via `/os-vault:write`. No new destination (e.g., a "retired specs"
directory) SHALL be introduced.

#### Scenario: Repo-durable extraction targets ADR/CLAUDE.md/docs

- **WHEN** an `extract-then-delete` op is classified as repo-durable
- **THEN** the extracted content is written into an ADR, `CLAUDE.md`, or a `docs/` file, never a new bespoke location

#### Scenario: Cross-repo extraction routes through /os-vault:write

- **WHEN** an `extract-then-delete` op is classified as cross-repo
- **THEN** the extracted content is written to the SecondBrain vault via `/os-vault:write`, and no other cross-repo destination is used

#### Scenario: No new destination is introduced

- **WHEN** extraction routing is implemented
- **THEN** it reuses only the destinations named above; it does not create a new "retired" or "archive" content store
