import { type ConfigRootEnvironment, type ConfigRootPlatform } from "./config-store.js"; import { type RandomBytesSource } from "./artifacts.js"; /** Registry filename under the watch root. */ export declare const WATCH_REGISTRY_FILENAME = "targets.json"; /** Change-log filename under a target dir. */ export declare const WATCH_CHANGELOG_FILENAME = "change-log.jsonl"; /** Snapshot files live in this per-target subdir as `gen-.snapshot`. */ export declare const WATCH_SNAPSHOTS_DIRNAME = "snapshots"; /** Default ring size (`--keep` overrides per target; plan ruling #6). */ export declare const WATCH_DEFAULT_KEEP = 5; /** Smallest legal ring size. */ export declare const WATCH_MIN_KEEP = 1; /** Largest legal ring size. */ export declare const WATCH_MAX_KEEP = 100; /** v1 target type discriminator (plan ruling #4: page only, additive kinds later). */ export type WatchTargetType = "page"; /** One registry row in `targets.json`. */ export interface WatchTarget { readonly id: string; readonly name: string; readonly type: WatchTargetType; readonly url: string; readonly keep: number; readonly createdAt: string; } /** Full registry file shape. */ export interface WatchRegistry { readonly targets: readonly WatchTarget[]; /** Retired ids (default removal leaves the dir): ids are never reused. */ readonly retiredIds?: readonly string[]; } /** Environment keys {@link resolveWatchDir} reads. */ export interface WatchDirEnvironment extends ConfigRootEnvironment { readonly SCOUTLINE_WATCH_DIR?: string; } /** * Watch root: `SCOUTLINE_WATCH_DIR` wins; otherwise the config root's * `watch/` sibling. Pure — the caller supplies env and platform; the * thin `process.env`-reading wrapper is left to the command layer (the * {@link newRequestId}/{@link resolveArtifactsDir} pattern) so tests * never touch process.env. */ export declare function resolveWatchDir(env: WatchDirEnvironment, platform?: ConfigRootPlatform): string; /** Injectable knobs for store calls (tests inject small lock timings). */ export interface WatchStoreOptions { /** Lock-timing overrides (tests use small values). */ readonly lock?: { readonly timeoutMs?: number; readonly staleMs?: number; readonly setTimeout?: typeof setTimeout; }; } /** Options for {@link addTarget}. */ export interface AddTargetOptions extends WatchStoreOptions { /** The page URL to watch (http(s) only, v1). */ readonly url: string; /** Caller-supplied `--name`; omitted → minted from the URL host+path. */ readonly name?: string; /** Ring size; omitted → {@link WATCH_DEFAULT_KEEP}. */ readonly keep?: number; /** Injection seams — never Date.now()/crypto inside the store. */ readonly now?: Date | number; readonly randomBytes?: RandomBytesSource; } /** Options for {@link removeTarget}. */ export interface RemoveTargetOptions extends WatchStoreOptions { /** true → also delete the per-target dir (default keeps the evidence). */ readonly purge?: boolean; } /** Stored snapshot metadata (JSON sidecar, `gen-.snapshot`). */ export interface SnapshotMetadata { readonly gen: number; readonly capturedAt: string; readonly byteLength: number; readonly contentType?: string; readonly finalUrl?: string; } /** Options for {@link appendSnapshot}. */ export interface AppendSnapshotOptions extends WatchStoreOptions { readonly body: Uint8Array; /** Timestamp from the CALLER's injected instant; never Date.now(). */ readonly now: Date | number; /** Final content type after redirects, e.g. `text/html; charset=gbk`. */ readonly contentType?: string; /** Post-redirect URL (plan ruling #7: `finalUrl` recorded). */ readonly finalUrl?: string; } /** {@link readSnapshot} result: raw captured bytes plus their metadata. */ export interface WatchSnapshot extends SnapshotMetadata { readonly body: Uint8Array; } /** v1 change-log kinds (binding set — future kinds extend this union). */ export type ChangeLogKind = "baseline" | "change" | "moved" | "error" | "no-change"; /** One appended change-log line. */ export interface ChangeLogEntry { readonly at: Date | number | string; readonly kind: ChangeLogKind; readonly exit: 0 | 1 | 2; /** Generation the entry refers to; null for `error` (ring did not advance). */ readonly gen: number | null; readonly added?: readonly string[]; readonly removed?: readonly string[]; readonly changed?: readonly string[]; readonly hashOnly?: boolean; readonly finalUrl?: string; } /** Parsed change-log line (same shape, `at` normalized to ISO). */ export interface ParsedChangeLogEntry { readonly at: string; readonly kind: ChangeLogKind; readonly exit: number; readonly gen: number | null; readonly added?: readonly string[]; readonly removed?: readonly string[]; readonly changed?: readonly string[]; readonly hashOnly?: boolean; readonly finalUrl?: string; } /** * Register a watch target. Name defaults to the URL host+path slug; * names are unique case-sensitively; ids are minted `newRequestId`-style * and checked against both live and retired ids (never reused). The * read-check-write runs under the registry lock, so concurrent adds * serialize and the loser's duplicate name is caught. */ export declare function addTarget(root: string, options: AddTargetOptions): Promise; /** Registry listing, ascending by id (== chronological, ids sort). */ export declare function listTargets(root: string): Promise; /** * Exact-id lookup with NO name fallback (review: retired-id-as-name * collision). `getTarget` may ref-match a name, which is right for * user-supplied refs — but the tick/append membership re-checks hold a * captured target object and must confirm THAT row still exists; a * retired id later reused as a NEW target's name must not satisfy the * check for the removed target. Returns null when the id has no live row. */ export declare function findTargetById(root: string, id: string): Promise; /** Resolve one target by exact id, else by exact name. */ export declare function getTarget(root: string, ref: string): Promise; /** * Identity-guarded removal. Default removal deletes ONLY the registry * entry — the `//` change log is evidence and is never pruned * implicitly; the id is retired so it is never reused. `purge: true` * also deletes the per-target directory. * * Lock ordering (review: purge raced ticks): the registry lock is held * for the whole removal, and the PURGE `fs.rm` additionally runs under * the target's own tick lock (`watch-tick-`) and append lock * (`watch-target-`), taken at the SAME stateDir the tick and * appends use (`//`) so they guard the same lock files. An * in-flight tick's appends either complete before the registry rewrite * (purge then removes them) or start after it and are rejected as * unknown-target (the registry no longer has the row) — evidence can * never be recreated under a purged id. The `fs.rm` unlinks the lock * files purge itself holds; that is safe because the lock cleanup * tolerates ENOENT and the registry-membership re-check (not the lock) * is what bars late writers. */ export declare function removeTarget(root: string, ref: string, options?: RemoveTargetOptions): Promise; /** * List a target's surviving generations with their metadata, ascending. * Reads only `gen-.snapshot` files (the metadata sidecar IS the file — * bytes live in a sibling `.bytes` file so metadata stays plain JSON). */ export declare function listSnapshots(root: string, id: string): Promise; /** * Append one generation to a target's snapshot ring and return its * generation number. The ONLY gen-advancing call in the store. Ordering: * mkdir → write gen file → unlink old generations. Atomic-enough first: * the new generation is complete on disk before any old generation is * unlinked — a crash mid-prune leaves extra old generations, never a * hole at the head; the next append re-prunes (keep is recomputed from * the full on-disk listing, not from an assumed ring shape). */ export declare function appendSnapshot(root: string, id: string, options: AppendSnapshotOptions): Promise; /** * Read one generation: raw captured bytes plus metadata. A gen whose * metadata or bytes are missing (crashed append) is a ValidationError — * the ring never advances past a hole, so a listed gen must read. */ export declare function readSnapshot(root: string, id: string, gen: number): Promise; /** * Structural guard for one parsed log line. Unknown kinds are a * ValidationError — the reader fails closed loudly rather than skipping * (audit evidence must never thin itself; contrast the artifacts log's * fail-open reads). */ export declare function parseChangeLogLine(file: string, line: string, index: number): ParsedChangeLogEntry; /** * Read a target's full change log. Missing/empty file → []. Malformed * JSON or unknown kinds throw ValidationError — fail-closed, loudly, * never a silent skip. */ export declare function readChangeLog(root: string, id: string): Promise; /** * Append one change-log entry as a single `\n`-terminated JSON line, * under the per-target lock so concurrent double-fires serialize (the * JSONL analogue of {@link appendLogEntry} — no lost update, no torn * line). Validation happens BEFORE the file is opened: an unknown kind * never reaches disk. */ export declare function appendChangeLog(root: string, id: string, entry: ChangeLogEntry, options?: WatchStoreOptions): Promise; //# sourceMappingURL=watch-store.d.ts.map