95 lines
5.9 KiB
Markdown
95 lines
5.9 KiB
Markdown
|
|
# Tasks: Add Deterministic Core
|
||
|
|
|
||
|
|
> **Execution shape:** Group 1 (build-spike) is a **sequential gate** — it MUST
|
||
|
|
> complete and be visually confirmed in a real session before any other group
|
||
|
|
> starts. Once it lands, Groups 2 (scanner) and 3 (state store) are **independent
|
||
|
|
> and run in parallel** (no shared state). Group 4 (reminder hook) is **sequential
|
||
|
|
> after Group 3** — it reads and writes `last_reminded` through the state store.
|
||
|
|
> Group 5 (cross-seam verification) runs last. Model routing: all scripts are
|
||
|
|
> deterministic (no model); per CLAUDE.md, write them with **Haiku**.
|
||
|
|
|
||
|
|
## 1. Build-Spike — SessionStart banner (SEQUENTIAL GATE, do FIRST)
|
||
|
|
|
||
|
|
- [x] 1.1 Author a trivial `hooks/hooks.json` at the plugin root wiring a
|
||
|
|
`SessionStart` hook (`matcher: startup|resume`) that emits a fixed
|
||
|
|
`systemMessage` banner, with `timeout` ≤ 5s and exit 0.
|
||
|
|
- [x] 1.2 **Acceptance criterion (implementation-time):** start a real Claude Code
|
||
|
|
session with the plugin enabled and **visually confirm the banner renders**
|
||
|
|
to the user. The rest of this change does not start until this is confirmed.
|
||
|
|
- [x] 1.3 Record the confirmed hook entry format (plugin-root `hooks/hooks.json`,
|
||
|
|
nested hooks array, `systemMessage` field) so Group 4 reuses it. This is the
|
||
|
|
first CC hook in the collection — capture the working shape.
|
||
|
|
|
||
|
|
## 2. Scanner (`doc-scanner`) — parallel with Group 3 after the gate
|
||
|
|
|
||
|
|
- [x] 2.1 Implement a `Scanner` class with injected filesystem/root and scope
|
||
|
|
globs; walk scoped files under the resolved project root. No model.
|
||
|
|
- [x] 2.2 Implement the exclusion pipeline, applied **before** signal computation
|
||
|
|
and short-circuiting: (a) dir-prune `scan.excluded_dirs` (`build`, `vendor`,
|
||
|
|
`archive`, `graphify-out`, `.dochygiene`) at the directory level, including
|
||
|
|
self-exclusion of `.dochygiene/`; (b) `.dochygiene-ignore` pattern match;
|
||
|
|
(c) `hygiene: frozen` frontmatter; (d) append-only-log detection.
|
||
|
|
- [x] 2.3 Implement deterministic per-path **signal** computation (objective facts
|
||
|
|
only: broken references, version skew, edit-recency vs git churn, location,
|
||
|
|
append-only growth, archive-to-live ratio, frontmatter markers).
|
||
|
|
- [x] 2.4 Emit the **intermediate artifact** (`shortlist` + per-path `signals`) as
|
||
|
|
structured JSON with a correct exit code. Do NOT emit `entries`, `category`,
|
||
|
|
`op`, `token_estimate`, or a machine report.
|
||
|
|
- [x] 2.5 **Tests — exclusions (invariant #9):** fixtures proving a `hygiene:
|
||
|
|
frozen` file, a `.dochygiene-ignore`-matched file, and an append-only log
|
||
|
|
(positive + negative fixture) are each present in the tree and absent from
|
||
|
|
the shortlist; and that `.dochygiene/` and excluded dirs are never scanned.
|
||
|
|
- [x] 2.6 **Tests — signals:** one fixture per signal type asserts the expected
|
||
|
|
signal appears on the right path with no classification attached.
|
||
|
|
|
||
|
|
## 3. State store (`state-store`) — parallel with Group 2 after the gate
|
||
|
|
|
||
|
|
- [x] 3.1 Implement `resolve_project_root(start_dir)` — nearest ancestor `.git`,
|
||
|
|
fallback cwd — as a pure function over injected filesystem.
|
||
|
|
- [x] 3.2 Implement a `StateStore` class confining all writes to
|
||
|
|
`<project_root>/.dochygiene/`; create the dir as needed; maintain no global
|
||
|
|
index; never open or edit `.gitignore`.
|
||
|
|
- [x] 3.3 Implement lifecycle timestamp read/write (`last_check`, `last_clean`,
|
||
|
|
`last_reminded`) in `state.json`; absent timestamps read as unset.
|
||
|
|
- [x] 3.4 Implement atomic writes (write-temp → fsync → `os.replace`) for
|
||
|
|
`state.json` and report files.
|
||
|
|
- [x] 3.5 Implement report rollover: delete any prior `.md` + `.json` report pair
|
||
|
|
before writing the new pair; exactly one pair retained (invariant #4).
|
||
|
|
- [x] 3.6 **Tests — root resolution (invariant #3):** git-root case, cwd-fallback
|
||
|
|
case, and that no path outside the resolved root is written; assert
|
||
|
|
`.gitignore` is never edited.
|
||
|
|
- [x] 3.7 **Tests — atomic write & rollover (invariants #3, #4):** torn-write
|
||
|
|
safety (reader sees old-or-new, never partial); two sequential report writes
|
||
|
|
leave exactly one latest pair on disk.
|
||
|
|
- [x] 3.8 **Tests — timestamps:** round-trip read/write with an injected clock;
|
||
|
|
absent timestamp reads as unset.
|
||
|
|
|
||
|
|
## 4. Reminder hook (`session-reminder`) — SEQUENTIAL after Group 3
|
||
|
|
|
||
|
|
- [x] 4.1 Replace the spike's fixed banner: implement a `Reminder` decision pure
|
||
|
|
over `(last_check, last_reminded, now, threshold)` per design D5 — silent
|
||
|
|
when fresh; banner when stale and not snoozed; snooze keyed on **calendar
|
||
|
|
day** via `last_reminded`.
|
||
|
|
- [x] 4.2 Implement the reminder script: read state via the Group 3 store with an
|
||
|
|
injected clock; on banner, emit `systemMessage` (naming the slash command)
|
||
|
|
and write `last_reminded = now`; on silent, write nothing; always exit 0;
|
||
|
|
treat missing/unreadable state as never-checked (stale). No scan, no model
|
||
|
|
(invariant #1).
|
||
|
|
- [x] 4.3 Update `hooks/hooks.json` to invoke the reminder script (reusing the
|
||
|
|
confirmed entry format from 1.3), keeping `timeout` ≤ 5s.
|
||
|
|
- [x] 4.4 **Tests — injected clock (invariants #1, #2):** fresh → silent; stale +
|
||
|
|
never-reminded → banner; stale + reminded same calendar day → silent (covers
|
||
|
|
repeated startup/resume within a day); stale + reminded prior day → banner; assert no
|
||
|
|
file is mutated except `last_reminded`, and only on banner.
|
||
|
|
|
||
|
|
## 5. Cross-seam verification & wiring
|
||
|
|
|
||
|
|
- [x] 5.1 Confirm no seam imports or invokes a model and the scanner makes no
|
||
|
|
network/API call (invariant #6 review).
|
||
|
|
- [x] 5.2 Confirm the scanner output is the intermediate artifact (no `entries`,
|
||
|
|
no report) and references the frozen `report-schema` shapes only as consumed.
|
||
|
|
- [x] 5.3 Generate a `CONTEXT.md` for `scripts/` (progressive disclosure) indexing
|
||
|
|
the reminder, scanner, and state-store modules.
|
||
|
|
- [x] 5.4 Run `openspec validate add-deterministic-core --strict` and confirm it
|
||
|
|
passes.
|