cc-os/plugins/os-doc-hygiene/openspec/changes/add-deterministic-core/proposal.md

113 lines
6.7 KiB
Markdown
Raw Normal View History

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