# 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.