import { type Duration, type StateEntry, type StateFact, type StateWrite } from "./core/hook-state.js"; /** * The directory a hook's recorded facts live in — the SCOPE of `state()`/`record()`. * * Derived from the hook's own location and never from anything the hook said, so * a key cannot address another owner's store: hooks shipped in the same directory * share their facts (the requirement — one hook records, another reads), a * vendored plugin's hooks get their own. The layout MIRRORS the hook's directory * rather than slugging it, which keeps it injective and lets a human debugging a * hook find the fact by walking the path they already know: * * .claude/hooks/calendar-sync-record.hook.ts * → .vigiles/state/.claude/hooks/calendar.synced.json * * A hook outside the project (an absolute path elsewhere) falls back to a hash of * its directory: still stable and still isolated, just not readable — which is the * right trade for a case that should not happen in a project's own harness. */ export declare function hookStateDir(file: string, cwd?: string): string; /** Read one recorded fact for a hook, or `null` if it was never recorded. */ export declare function readHookState(file: string, key: string, cwd?: string): StateEntry | null; /** Options for {@link writeHookState}. */ export interface WriteHookStateOptions { /** The project root the store hangs off. Default `process.cwd()`. */ readonly cwd?: string; /** * The instant to stamp. Default: now. * * The runtime never passes this — a fact it records happened just now, by * definition. It exists for {@link experimental_hookState}'s `seed`, which * must be able to write "four days ago" for a throttle test to have anything * to test. Threading it through the REAL writer rather than letting a test * build its own entry is what keeps the two in step. */ readonly at?: Date; } /** * Record one fact. Atomic: written to a temp file in the same directory and * `rename()`d over, so a concurrent reader sees the whole old entry or the whole * new one — never one write's value with another's timestamp. Distinct keys are * distinct files and never interact at all. */ export declare function writeHookState(file: string, w: StateWrite, opts?: WriteHookStateOptions): void; /** * Options for {@link HookStateHandle.seed}. * * Spelled out as a three-way union rather than `{value} & SeedWhen` so the whole * type is IN this declaration: a public signature that names a type a consumer * cannot import is a surface you can read but not write against. The union is * also what makes `{ ago, at }` — two disagreeing answers to "when" — a tsc * error; `seedInstant` throws on it as well, because harness tests are `.mjs` * and a type does not run there. */ export type SeedStateOptions = /** Recorded that long ago — the reason this exists (a throttle needs an OLD fact). */ { readonly value?: string; readonly ago: Duration; readonly at?: never; } /** Recorded at exactly this instant. */ | { readonly value?: string; readonly at: Date; readonly ago?: never; } /** Recorded just now. */ | { readonly value?: string; readonly ago?: never; readonly at?: never; }; /** * A handle on ONE hook's recorded facts — what {@link experimental_hookState} * returns. Every method goes through the runtime's own store functions, so a * seeded fact is indistinguishable from one the hook recorded itself. */ export interface HookStateHandle { /** * Where this hook's facts live. Exposed for a failure message ("no fact under * …"), NOT as a path to build on — build on it and you are back to the * hard-coded private path this handle exists to retire. */ readonly dir: string; /** * Write a fact as if the hook had recorded it, then read it back through the * real reader — so what you get is exactly what the hook will see, and a write * that did not land cannot be mistaken for one that did. * * `ago` is the reason this exists: a throttle is only interesting against an * OLD fact, and "old" is not something a test can produce by waiting. * * ```js * const st = experimental_hookState(".vigiles/hooks/nag.hook.mjs", { cwd }); * st.seed("retro.nagged", { ago: "4d" }); // → the hook speaks * st.seed("retro.nagged", { ago: "10m" }); // → the hook stays quiet * ``` */ seed(key: string, opts?: SeedStateOptions): StateFact; /** * Read a fact exactly as the hook's `e.ctx[key]` would — same `recorded` / * `ageSeconds` / `fresherThan`. Never-recorded reads back as the total * `Infinity` fact rather than throwing, because that is what the hook sees. * * Also the cheap way to drive the IN-PROCESS tier: the value it returns is a * `StateFact`, which is what `runHookProgram(hook, event, ctx)` wants in `ctx`. */ read(key: string): StateFact; /** * Forget the facts THIS hook recorded. A no-op when it recorded none. * * Scoped to this hook's own writes, because the store is shared per DIRECTORY * — that sharing is what lets one hook read a fact another recorded — so a * co-located hook's facts (and any entry with no recorded owner) are left * untouched. When a test wants the whole store empty rather than this hook's * share of it, give it a throwaway `cwd`: the store hangs off that root. */ clear(): void; } /** * Seed and read a compiled hook's named state from a test. * * @experimental — the store LAYOUT is not a stable contract (this handle exists * so you never depend on it), and the hook vocabulary it serves is itself * `experimental_`. See `docs/compiled-hooks.md` § Status / pending. * * Pass the hook file exactly as the wiring names it, and the same `cwd` the hook * will run under — the store's location is derived from both, so a handle built * with a different root points at a different (empty) store. * * ```js * import { experimental_hookState, runHook } from "vigiles"; * * const st = experimental_hookState(hookFile, { cwd }); * st.clear(); * st.seed("retro.nagged", { ago: "4d" }); * const r = runHook(`npx vigiles hook-runtime run-program ${hookFile}`, event, { cwd }); * ``` */ export declare function experimental_hookState(hookFile: string, opts?: { readonly cwd?: string; }): HookStateHandle; //# sourceMappingURL=hook-state-store.d.ts.map