/** * Snapshot store — versioned JSONL snapshots + SQLite index. * * data/snapshots/{industry}/{brand}/{YYYYMMDD-HHmmss}Z.jsonl * data/snapshots/index.db * * Each snapshot file is JSONL: * line 1 — header record: { "_header": true, brand, industry, runAt, recordCount, scorecard, gitSha, extractor } * lines 2+ — one extracted record per line (whatever schema; we don't enforce here) * * The index lets us answer cheaply: * - "what's the latest snapshot for brand X?" * - "list all snapshots for brand X between A and B" * - "all brands whose latest scorecard dropped a band" * * Design rules: * - JSONL is source of truth. SQLite is a derived index — rebuildable by re-reading headers. * - Header is the first JSONL line so we never need to load all records to get summary data. * - No mutation. Each run writes a new file. */ import { Database } from "bun:sqlite"; import type { ScrapeScoreCard } from "../recon/scrapeScoreCard.js"; export interface SnapshotHeader { readonly _header: true; readonly brand: string; readonly industry: string; /** ISO-8601 UTC timestamp. */ readonly runAt: string; readonly recordCount: number; readonly scorecard: ScrapeScoreCard | null; readonly gitSha: string | null; /** e.g. "scrapers/retail/westside.ts" */ readonly extractor: string | null; } export interface SnapshotRow { readonly id: number; readonly industry: string; readonly brand: string; readonly runAt: string; readonly recordCount: number; readonly scoreOverall: number | null; readonly scoreBand: "high" | "medium" | "low" | null; readonly completeness: number | null; readonly dataQuality: number | null; readonly sourceAuthenticity: number | null; readonly freshness: number | null; readonly snapshotPath: string; readonly gitSha: string | null; } export interface WriteSnapshotInput { readonly brand: string; readonly industry: string; readonly records: readonly object[]; readonly scorecard?: ScrapeScoreCard | null; readonly gitSha?: string | null; readonly extractor?: string | null; /** Override now() — used in tests. */ readonly clock?: () => Date; /** Override the snapshots root — used in tests. */ readonly root?: string; } export interface WriteSnapshotResult { readonly path: string; readonly header: SnapshotHeader; readonly indexRowId: number; } /** Compact ISO timestamp safe for filenames: 20260520T140258Z */ export declare function snapshotStamp(d: Date): string; /** * Slugify a brand name for filesystem use. Keep it boring — lowercase, * spaces to hyphens, strip everything else. */ export declare function brandSlug(brand: string): string; /** * Open or create the snapshots SQLite index at `{root}/index.db`. * Idempotent — safe to call repeatedly. */ export declare function openSnapshotIndex(root?: string): Database; /** * Write a snapshot to disk and index it. * * If a snapshot with the same path already exists, throws — snapshots are * append-only at the day-second granularity, and re-runs in the same second * are a caller bug. */ export declare function writeSnapshot(input: WriteSnapshotInput): WriteSnapshotResult; /** * Read a snapshot file — returns header + records. */ export declare function readSnapshot(path: string): { header: SnapshotHeader; records: readonly object[]; }; /** * Read just the header — much cheaper for snapshot summaries. */ export declare function readSnapshotHeader(path: string): SnapshotHeader; /** * List snapshots for a brand, newest first. */ export declare function listSnapshots(brand: string, root?: string): readonly SnapshotRow[]; /** * Get the most recent snapshot for a brand, or null. */ export declare function latestSnapshot(brand: string, root?: string): SnapshotRow | null; /** * Get the snapshot immediately before `runAt` for a brand. Used for diffs. */ export declare function previousSnapshot(brand: string, runAt: string, root?: string): SnapshotRow | null; /** * Rebuild the SQLite index by scanning {root} headers. Useful if the index * is lost or out of sync. */ export declare function rebuildIndex(root?: string): number;