# Change: Add Deterministic Core

## Why

The report schema (build step #1) is frozen, but no code exists yet. The plugin
cannot do anything until the deterministic substrate is built: the only thing the
user ever sees unprompted (the `SessionStart` staleness reminder), the thing that
produces the candidate set the AI later classifies (the scanner), and the thing
that holds lifecycle state and reports per-project (the state store). All three
are **deterministic, zero-model** seams (invariant #6) and must exist before the
`check` skill (the first AI pass) can be built on top of them.

This is build step #2 per CLAUDE.md. Because no plugin in this collection has ever
wired a Claude Code hook, the change leads with a **build-spike**: a trivial
`SessionStart` hook emitting a `systemMessage` banner, confirmed to render
visibly in a real session, de-risking the entire reminder mechanism before the
deterministic logic is built on it.

## What Changes

- **Build-spike (sequential gate, first):** stand up a trivial `hooks/hooks.json`
  wiring a `SessionStart` hook (`matcher: startup|resume`) that emits a fixed
  `systemMessage` banner, and confirm it renders visibly in a real session. This
  proves the one mechanism with no precedent in the collection before any logic
  depends on it.
- **Reminder hook (`session-reminder`):** a deterministic reminder script invoked
  by the `SessionStart` hook. It reads state, compares `last_check` to a staleness
  threshold, and emits the `systemMessage` banner (naming the slash command to
  run) only when stale. It spends ZERO AI tokens, runs no scan or classification,
  exits 0 within a low timeout, and mutates nothing except writing `last_reminded`
  (invariant #1). It snoozes to at most once per calendar day while stale, gated
  by `last_reminded` (invariant #2). It is unit-testable with an injected clock.
- **Scanner (`doc-scanner`):** a deterministic walker that produces the
  **shortlist** (candidate project-root-relative paths) and the per-path
  **signals** the report schema consumes — broken references, version skew,
  edit-recency vs git churn, location, append-only growth, archive-to-live ratio,
  frontmatter markers. It honors all exclusions (invariant #9): `hygiene: frozen`
  frontmatter, `.dochygiene-ignore` patterns, detected append-only logs,
  self-exclusion of `.dochygiene/`, and the configured `scan.excluded_dirs`
  (build, vendor, archive, graphify-out, .dochygiene). It uses **no model**
  (invariant #6). It emits an **intermediate** structured artifact (shortlist +
  per-path signals), NOT a full machine report — it does not classify, write
  `entries`, or populate `token_estimate`.
- **State store (`state-store`):** an in-project store rooted at the resolved
  project root (git root, fallback cwd) under a gitignored `.dochygiene/`
  directory. It reads/writes the lifecycle timestamps (`last_check`,
  `last_clean`, `last_reminded`), enforces report rollover (keep exactly the
  latest `.json` + `.md` pair, invariant #4), uses atomic writes, confines all
  writes to `.dochygiene/`, keeps no global cross-project index, and never
  silently edits the user's `.gitignore` (invariant #3). It is unit-testable with
  an injected clock and filesystem.

## Capabilities

### New Capabilities

- `session-reminder`: the deterministic `SessionStart` reminder — hook wiring,
  the zero-token staleness banner via `systemMessage`, the once-per-day snooze
  gated by `last_reminded`, and the invariant that the hook only reminds (no
  scan, no classification, no mutation beyond `last_reminded`). The build-spike's
  hook-wiring is proven under this capability.
- `doc-scanner`: the deterministic scan — walking scoped files, computing
  per-path objective signals, applying the exclusion pipeline (frozen frontmatter,
  ignore patterns, append-only detection, self-exclusion, excluded dirs), and
  emitting the intermediate shortlist + signals artifact. No model.
- `state-store`: the in-project state and report store — project-root resolution,
  lifecycle timestamps, atomic writes, report rollover, write confinement to
  `.dochygiene/`, no global index, and no silent `.gitignore` edits.

One capability per independently-testable deterministic seam: each maps to a
distinct runtime component with its own test seam (injected-clock snooze;
exclusion pipeline; root-resolution + rollover + atomic-write), so splitting keeps
each requirement set focused and the golden/unit tests cohesive rather than one
monolithic capability spanning three unrelated concerns.

### Modified Capabilities

None. The `report-schema` capability is **consumed, not modified** — this change
produces the `shortlist` and per-path `signals` that the frozen schema already
specifies, and changes none of its requirements.

## Impact

- **Affected specs:** creates three new capabilities — `session-reminder`,
  `doc-scanner`, `state-store`. Consumes the existing frozen `report-schema`
  capability without modifying it.
- **Affected code:** introduces `hooks/hooks.json` (first CC hook in the
  collection) and deterministic Python scripts under `scripts/` (reminder,
  scanner, state store) plus their unit tests. No skill or AI pass is added here.
- **Out of scope (named owners, so nothing is left ownerless):**
  - The **AI classify pass / `check` skill** (produces `entries`, reads the
    scanner's intermediate artifact, folds shortlist + signals into the machine
    report). Owner: the upcoming `add-check` change.
  - **`raw_tokens` / `token_estimate` population.** `token_estimate` is a field on
    `entries`, and `raw_tokens` counts "the removed or reduced span" — a span that
    only exists once the AI selects an `op`. Although the *count* is deterministic
    (invariant #6), its *input* (the chosen op) does not exist until
    classification, so the scanner has nothing to count. `raw_tokens` (v1-required)
    is therefore populated by the **`check`** change, which owns `entries`. The
    token *estimator's* full build (injection-frequency weighting + bottom-up
    rollup) remains the **v2 bonus** per PRD phase 5. "Deterministic" does not
    imply "belongs in deterministic-core."
  - The **`.gitignore` ignore-entry offer** (the one-line "add `.dochygiene/` to
    `.gitignore`?" surfaced on first check). This state store only guarantees it
    never *silently* edits `.gitignore`; the user-facing offer UX is a
    **`check`-change** concern. Note: creating the `.dochygiene/` directory
    untracked-but-not-yet-ignored is allowed — invariant #3 forbids *editing*
    `.gitignore`, not creating the dir.
  - The **`clean` skill + patch-applier**, the **`sweep`** orchestration, and the
    **mtime/content guard** application — all later changes.
- **Dependencies:** depends on the frozen `report-schema` (the `shortlist` and
  `signals` shapes). Unblocks the `check` change.
