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

6.7 KiB

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.