113 lines
6.7 KiB
Markdown
113 lines
6.7 KiB
Markdown
|
|
# 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.
|