// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-12-p17-monitor-central-persistence.
// Responsibility: Registry-resolved read-only legacy SQLite snapshots and one-shot canonical central-store import.
import { createHash } from "node:crypto";
import { closeSync, existsSync, openSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { Database } from "bun:sqlite";

import type { MonitorArtifactLocator, MonitorCentralIngest, MonitorCentralStore, MonitorRuntimeMetadata } from "./monitor-central-persistence";
import { monitorCentralStableJson, normalizeMonitorCentralIngest } from "./monitor-central-persistence";

export const MONITOR_SQLITE_MIGRATION_SCHEMA = "2026-07-12-p17-3";
export interface MonitorArtifactSource { readonly ownerPvc: string; readonly mountPath: string; readonly hostPath?: string; }
export interface MonitorLegacyArtifactGap { readonly runId: string; readonly artifactCount: number; readonly stateDir: string; readonly reportSha256: string; }
export interface MonitorArtifactManifest { readonly runId: string; readonly artifactCount: number; readonly stateDir: string; readonly reportSha256: string; readonly ownerPvc: string | null; readonly locators: readonly MonitorArtifactLocator[]; readonly missingBeforeMigration?: MonitorLegacyArtifactGap; }
function freezeArtifactManifests(snapshot: Pick<MonitorSqliteSnapshot, "source" | "tables">): readonly MonitorArtifactManifest[] { const rows = tableRows(snapshot.tables, ["runs", "run"]); const locatorRows = tableRows(snapshot.tables, ["artifact_locators", "locators"]); const declared = snapshot.source.legacyArtifactGaps ?? []; const used = new Set<string>(); const manifests = rows.map((row) => { const runId = stringField(row, ["run_id", "id"]); const gap = declared.find((item) => item.runId === runId); const rowCount = numberField(row, ["artifact_count", "artifactCount"]); const stateDir = stringField(row, ["state_dir", "stateDir"]); const reportSha256 = stringField(row, ["report_json_sha256", "reportJsonSha256"]); const rowLocators = locatorRows.filter((item) => stringField(item, ["run_id"]) === runId).map((item) => locatorFromRow(item, snapshot)); if (gap !== undefined) { if (gap.artifactCount !== rowCount || gap.stateDir !== stateDir || gap.reportSha256 !== reportSha256) throw artifactGapIdentityInvalid(runId, gap, { artifactCount: rowCount, stateDir, reportSha256 }); if (rowLocators.length !== 0) throw Object.assign(new Error(`declared missing artifact gap unexpectedly has locators for ${runId}`), { code: "monitor-migration-artifact-gap-locator-unexpected" }); used.add(runId); return { runId, artifactCount: gap.artifactCount, stateDir, reportSha256, ownerPvc: null, locators: [], missingBeforeMigration: gap }; } const locators = rowLocators.length > 0 ? rowLocators : artifactLocatorsFromRun(row, snapshot); const artifactCount = rowCount ?? locators.length; if (artifactCount !== locators.length) throw artifactCountMismatch(runId, stateDir, artifactCount, locators.length); verifyReportLocator(row, locators); const ownerPvc = locators[0]?.ownerPvc ?? (stateDir ? artifactSourceForRunStateDir(snapshot.source, stateDir).ownerPvc : null); return { runId, artifactCount, stateDir, reportSha256, ownerPvc, locators }; }); if (used.size !== declared.length) throw Object.assign(new Error("declared legacy artifact gap was not found in the SQLite snapshot"), { code: "monitor-migration-artifact-gap-unmatched" }); return manifests; }
export interface MonitorSqliteSource { readonly sentinelId: string; readonly node: string; readonly lane: string; readonly path: string; readonly runtimeConfigRef: string; readonly stateRoot: string; readonly ownerPvc: string; readonly artifactSources: readonly MonitorArtifactSource[]; readonly legacyArtifactGaps?: readonly MonitorLegacyArtifactGap[]; }
export interface MonitorSqliteTable { readonly name: string; readonly sql: string; readonly rowCount: number; readonly rows: readonly Record<string, unknown>[]; }
export interface MonitorSqliteSnapshot {
  readonly schema: string; readonly createdAt: string; readonly source: MonitorSqliteSource; readonly fingerprint: string; readonly integrity: string; readonly byteLength: number;
  readonly tables: readonly MonitorSqliteTable[];
  readonly artifactManifests: readonly MonitorArtifactManifest[];
  readonly artifactLocator: { readonly path: string; readonly sha256: string; readonly sizeBytes: number; readonly kind: "legacy-sqlite" };
  readonly manifestContentHash: string;
}
export interface MonitorMigrationReconciliation { readonly runs: number; readonly findings: number; readonly metadata: number; readonly payloads: number; readonly locators: number; readonly latest: number; }

export async function verifyMonitorSqliteSnapshotAgainstStore(snapshot: MonitorSqliteSnapshot, store: MonitorCentralStore): Promise<Record<string, unknown>> {
  const snapshotVerification = verifyMonitorSqliteSnapshot(snapshot);
  verifySnapshotReferentialIntegrity(snapshot);
  verifyFrozenArtifactManifests(snapshot);
  const expected = canonicalIngests(snapshot);
  const expectedMetadata = canonicalRuntimeMetadata(snapshot);
  const checks: Record<string, unknown>[] = [];
  for (const input of expected) {
    const scope = { sentinelId: input.sentinelId, node: input.node, lane: input.lane };
    const actual = await store.run(scope, input.runId);
    checks.push({ name: "run", runId: input.runId, ok: actual?.payloadHash === input.payloadHash && actual?.status === input.status && actual?.updatedAt === input.updatedAt && monitorCentralStableJson(actual?.payload) === monitorCentralStableJson(input.payload), valuesRedacted: true });
    checks.push({ name: "finding", runId: input.runId, ok: monitorCentralStableJson(actual?.findings) === monitorCentralStableJson(input.findings), valuesRedacted: true });
    checks.push({ name: "artifactLocator", runId: input.runId, ok: monitorCentralStableJson(actual?.artifactLocators) === monitorCentralStableJson(input.artifactLocators), valuesRedacted: true });
    checks.push({ name: "timeline", runId: input.runId, ok: Array.isArray(actual?.timeline) && actual.timeline.length === input.timeline.length && monitorCentralStableJson(actual.timeline) === monitorCentralStableJson(input.timeline), valuesRedacted: true });
    const views = isRecord(input.payload.views) ? input.payload.views : {};
    for (const [name, value] of Object.entries(views)) {
      const actualView = await store.view(scope, input.runId, name);
      checks.push({ name: "view", runId: input.runId, view: name, ok: monitorCentralStableJson(actualView?.value) === monitorCentralStableJson(value), valuesRedacted: true });
    }
  }
  const latestFor = (items: readonly typeof expected) => [...items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt) || right.runId.localeCompare(left.runId))[0] ?? null;
  const globalLatest = latestFor(expected);
  const global = await store.overview({});
  checks.push({ name: "globalOverview", ok: global.runCount === expected.length && (globalLatest === null ? global.latest === null : sameRun(global.latest, globalLatest)), valuesRedacted: true });
  const scopes = [...new Map([{ sentinelId: snapshot.source.sentinelId, node: snapshot.source.node, lane: snapshot.source.lane }, ...expected.map((item) => ({ sentinelId: item.sentinelId, node: item.node, lane: item.lane })), ...expectedMetadata.map((item) => ({ sentinelId: item.sentinelId, node: item.node, lane: item.lane }))].map((scope) => [`${scope.sentinelId}/${scope.node}/${scope.lane}`, scope])).values()];
  for (const scope of scopes) {
    const latest = latestFor(expected.filter((item) => item.sentinelId === scope.sentinelId && item.node === scope.node && item.lane === scope.lane));
    const overview = await store.overview(scope);
    const expectedCount = expected.filter((item) => item.sentinelId === scope.sentinelId && item.node === scope.node && item.lane === scope.lane).length;
    checks.push({ name: "scopedOverview", scope, ok: overview.runCount === expectedCount && (latest === null ? overview.latest === null : sameRun(overview.latest, latest)), valuesRedacted: true });
    const actualMetadata = await store.runtimeMetadata(scope);
    const scopedMetadata = expectedMetadata.filter((item) => item.sentinelId === scope.sentinelId && item.node === scope.node && item.lane === scope.lane);
    checks.push({ name: "runtimeMetadata", scope, ok: monitorCentralStableJson(actualMetadata) === monitorCentralStableJson(scopedMetadata), sourceCount: scopedMetadata.length, targetCount: actualMetadata.length, valuesRedacted: true });
  }
  return { ok: snapshotVerification.ok === true && checks.every((item) => item.ok === true), command: "web-probe sentinel migration verify", source: "resolved-active-registry", snapshot: snapshotVerification, reconciliation: { source: reconcileTables(snapshot.tables), target: targetReconciliation(expected, expectedMetadata) }, checks, valuesRedacted: true };
}

export function planMonitorSqliteSources(sources: readonly MonitorSqliteSource[]): Record<string, unknown> {
  return { ok: true, command: "web-probe sentinel migration plan", sources: sources.map(sourcePlan), sourceCount: sources.length, valuesRedacted: true };
}

export function exportMonitorSqliteSnapshot(source: MonitorSqliteSource, outputPath: string): MonitorSqliteSnapshot {
  const path = assertReadOnlySqliteSource(source.path);
  const database = new Database(path, { readonly: true });
  let transactionOpen = false;
  try {
    database.exec("BEGIN");
    transactionOpen = true;
    const integrity = String(database.query("PRAGMA integrity_check").get()?.integrity_check ?? "unknown");
    if (integrity !== "ok") throw new Error(`legacy SQLite integrity_check failed: ${integrity}`);
    const tables = (database.query("SELECT name, sql FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all() as { name: string; sql: string }[]).map((table) => ({ name: table.name, sql: table.sql, rowCount: Number(database.query(`SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}`).get()?.count ?? 0), rows: database.query(`SELECT * FROM ${quoteIdentifier(table.name)}`).all().map(normalizeRow) }));
    const immutablePath = sqliteSnapshotPath(outputPath);
    const immutableBytes = (database as unknown as { serialize(): Uint8Array }).serialize();
    writeFileSync(immutablePath, immutableBytes);
    const artifactManifests = freezeArtifactManifests({ source: { ...source, path }, tables });
    const unsigned = { schema: MONITOR_SQLITE_MIGRATION_SCHEMA, createdAt: new Date().toISOString(), source: { ...source, path }, fingerprint: `sha256:${sha256(immutableBytes)}`, integrity, byteLength: immutableBytes.byteLength, tables, artifactManifests, artifactLocator: { path: immutablePath, sha256: `sha256:${sha256(immutableBytes)}`, sizeBytes: immutableBytes.byteLength, kind: "legacy-sqlite" } };
    const snapshot: MonitorSqliteSnapshot = { ...unsigned, manifestContentHash: monitorSnapshotContentHash(unsigned) };
    writeSnapshot(outputPath, `${monitorCentralStableJson(snapshot)}\n`);
    database.exec("COMMIT");
    transactionOpen = false;
    return snapshot;
  } finally { if (transactionOpen) database.exec("ROLLBACK"); database.close(); }
}

export function verifyMonitorSqliteSnapshot(snapshot: MonitorSqliteSnapshot): Record<string, unknown> {
  const sourceBytes = readFileSync(assertReadOnlySqliteSource(snapshot.artifactLocator.path));
  const locatorValid = snapshot.artifactLocator.sha256 === `sha256:${sha256(sourceBytes)}` && snapshot.artifactLocator.sizeBytes === sourceBytes.byteLength;
  const fingerprintValid = snapshot.fingerprint === snapshot.artifactLocator.sha256 && snapshot.byteLength === snapshot.artifactLocator.sizeBytes;
  const reconciliation = reconcileTables(snapshot.tables);
  const manifestContentHashValid = snapshot.manifestContentHash === monitorSnapshotContentHash(snapshot);
  const rowsValid = snapshot.tables.every((table) => table.rowCount === table.rows.length);
  let referentialValid = true;
  try { verifySnapshotReferentialIntegrity(snapshot); } catch { referentialValid = false; }
  return { ok: snapshot.schema === MONITOR_SQLITE_MIGRATION_SCHEMA && snapshot.integrity === "ok" && locatorValid && fingerprintValid && manifestContentHashValid && rowsValid && referentialValid, command: "web-probe sentinel migration verify", fingerprint: snapshot.fingerprint, integrity: snapshot.integrity, reconciliation, artifactLocator: { ...snapshot.artifactLocator, valid: locatorValid }, schemaValid: snapshot.schema === MONITOR_SQLITE_MIGRATION_SCHEMA, manifestContentHashValid, rowsValid, referentialValid, valuesRedacted: true };
}

export async function importMonitorSqliteSnapshot(snapshot: MonitorSqliteSnapshot, store: MonitorCentralStore): Promise<Record<string, unknown>> {
  const verified = verifyMonitorSqliteSnapshot(snapshot);
  if (verified.ok !== true) throw Object.assign(new Error("snapshot verification failed"), { code: "monitor-migration-snapshot-invalid", verification: verified });
  verifySnapshotReferentialIntegrity(snapshot);
  verifyFrozenArtifactManifests(snapshot);
  const ingests = canonicalIngests(snapshot);
  const runtimeMetadata = canonicalRuntimeMetadata(snapshot);
  const manifest = { manifestId: snapshot.manifestContentHash, sourceFingerprint: snapshot.fingerprint, rowCount: snapshot.tables.reduce((total, table) => total + table.rowCount, 0) };
  const batch = await store.importSource(manifest, ingests, runtimeMetadata);
  const dispositions = batch.runs;
  const reconciliation = reconcileTables(snapshot.tables);
  return { ok: true, command: "web-probe sentinel migration import", source: snapshot.source, manifest, disposition: batch.disposition, imported: { inserted: dispositions.filter((item) => item.disposition === "inserted").length, idempotent: dispositions.filter((item) => item.disposition === "idempotent").length }, reconciliation: { source: reconciliation, target: targetReconciliation(ingests, runtimeMetadata) }, payloadHashCount: new Set(ingests.map((item) => item.payloadHash)).size, valuesRedacted: true };
}

export function reconcileTables(tables: readonly MonitorSqliteTable[]): MonitorMigrationReconciliation {
  const count = (names: readonly string[]) => tables.filter((table) => names.includes(table.name)).reduce((total, table) => total + table.rowCount, 0);
  return { runs: count(["runs", "run"]), findings: count(["findings", "finding"]), metadata: count(["metadata", "run_metadata"]), payloads: count(["payloads", "run_payloads"]), locators: count(["artifact_locators", "locators"]), latest: count(["latest", "latest_runs"]) };
}

function canonicalIngests(snapshot: MonitorSqliteSnapshot): readonly ReturnType<typeof normalizeMonitorCentralIngest>[] {
  const rows = tableRows(snapshot.tables, ["runs", "run"]);
  const findings = tableRows(snapshot.tables, ["findings", "finding"]);
  const metadata = tableRows(snapshot.tables, ["metadata", "run_metadata"]);
  const payloads = tableRows(snapshot.tables, ["payloads", "run_payloads"]);
  return rows.map((row) => {
    const runId = stringField(row, ["run_id", "id"]);
    const rowFindings = findings.filter((item) => stringField(item, ["run_id"]) === runId).map(normalizeLegacyFinding);
    const report = metadataValue(metadata, `run.report.${runId}`);
    const storedPayload = jsonField(payloads.find((item) => stringField(item, ["run_id"]) === runId), ["payload", "value_json"]);
    const payload = { ...(storedPayload ?? {}), ...(report ?? {}), legacy: { run: row, sourceFingerprint: snapshot.fingerprint } };
    const reportFindings = Array.isArray(report?.findings) ? report.findings.filter(isRecord) : [];
    const canonicalFindings = dedupeFindings(reportFindings, rowFindings);
    const artifactManifest = snapshot.artifactManifests.find((item) => item.runId === runId);
    if (artifactManifest === undefined) throw Object.assign(new Error(`snapshot artifact manifest is missing ${runId}`), { code: "monitor-migration-artifact-manifest-missing" });
    const missing = artifactManifest.missingBeforeMigration;
    const missingFact = missing === undefined ? null : { code: "legacy-artifact-bytes-missing-before-migration", runId, artifactCount: missing.artifactCount, stateDir: missing.stateDir, reportSha256: missing.reportSha256, locator: null, bytesVerified: false, valuesRedacted: true };
    return normalizeMonitorCentralIngest({ sentinelId: stringField(row, ["sentinel_id", "sentinelId"]) || snapshot.source.sentinelId, node: stringField(row, ["node"]) || snapshot.source.node, lane: stringField(row, ["lane"]) || snapshot.source.lane, runId, updatedAt: stringField(row, ["updated_at", "updatedAt", "created_at", "createdAt"]) || snapshot.createdAt, status: stringField(row, ["status"]) || "legacy", payload: missingFact === null ? payload : { ...payload, migration: { legacyArtifactBytesMissingBeforeMigration: missingFact } }, findings: missingFact === null ? canonicalFindings : [...canonicalFindings, missingFact], artifactLocators: artifactManifest.locators, timeline: missingFact === null ? [] : [missingFact] });
  });
}
function targetReconciliation(ingests: readonly ReturnType<typeof normalizeMonitorCentralIngest>[], runtimeMetadata: readonly MonitorRuntimeMetadata[]): Record<string, number> { return { runs: ingests.length, findings: ingests.reduce((total, item) => total + item.findings.length, 0), payloads: ingests.length, artifactLocators: ingests.reduce((total, item) => total + item.artifactLocators.length, 0), views: ingests.reduce((total, item) => total + (isRecord(item.payload.views) ? Object.keys(item.payload.views).length : 0), 0), runtimeMetadata: runtimeMetadata.length }; }
function canonicalRuntimeMetadata(snapshot: MonitorSqliteSnapshot): readonly MonitorRuntimeMetadata[] { return tableRows(snapshot.tables, ["metadata", "run_metadata"]).map((row) => ({ sentinelId: snapshot.source.sentinelId, node: snapshot.source.node, lane: snapshot.source.lane, key: stringField(row, ["key"]), value: jsonValue(row, ["value_json", "value"]), updatedAt: stringField(row, ["updated_at", "updatedAt", "created_at", "createdAt"]) || snapshot.createdAt })).filter((item) => item.key.length > 0).sort((left, right) => left.key.localeCompare(right.key)); }
function sameRun(value: unknown, expected: { readonly sentinelId: string; readonly node: string; readonly lane: string; readonly runId: string; readonly updatedAt: string }): boolean { return isRecord(value) && value.sentinelId === expected.sentinelId && value.node === expected.node && value.lane === expected.lane && value.runId === expected.runId && value.updatedAt === expected.updatedAt; }
function locatorFromRow(row: Record<string, unknown>, snapshot: Pick<MonitorSqliteSnapshot, "source">): MonitorArtifactLocator { const ownerPvc = stringField(row, ["owner_pvc", "ownerPvc"]); const relativePath = stringField(row, ["relative_path", "relativePath"]); const expectedHash = stringField(row, ["sha256"]); const expectedSize = numberField(row, ["size_bytes", "sizeBytes"]); const stateDir = stringField(row, ["state_dir", "stateDir"]); if (!ownerPvc || !stateDir || !relativePath || !expectedHash || expectedSize === null) throw Object.assign(new Error("legacy artifact locator is incomplete"), { code: "monitor-migration-locator-invalid" }); const source = artifactSourceFor(snapshot.source, ownerPvc); const path = artifactPath(source.mountPath, stateDir, relativePath); const bytes = readFileSync(path); const actualHash = `sha256:${sha256(bytes)}`; if (actualHash !== expectedHash || bytes.byteLength !== expectedSize) throw Object.assign(new Error("legacy artifact locator does not match source artifact"), { code: "monitor-migration-locator-invalid" }); return { ownerNode: stringField(row, ["owner_node", "ownerNode"]) || snapshot.source.node, ownerLane: stringField(row, ["owner_lane", "ownerLane"]) || snapshot.source.lane, ownerPvc, stateDir, relativePath, sha256: actualHash, sizeBytes: bytes.byteLength, kind: stringField(row, ["kind"]) || "artifact", reachable: row.reachable !== false }; }
function artifactLocatorsFromRun(row: Record<string, unknown>, snapshot: MonitorSqliteSnapshot): readonly MonitorArtifactLocator[] { const stateDir = stringField(row, ["state_dir", "stateDir"]); if (!stateDir) return []; const source = artifactSourceForStateDir(snapshot.source, stateDir); const directory = artifactPath(source.mountPath, stateDir, ""); if (!existsSync(directory) || !statSync(directory).isDirectory()) throw Object.assign(new Error("legacy artifact stateDir is unavailable"), { code: "monitor-migration-artifact-path-invalid" }); return artifactFiles(directory).map((path) => { const bytes = readFileSync(path); const relativePath = path.slice(directory.length + 1).replaceAll("\\", "/"); return { ownerNode: snapshot.source.node, ownerLane: snapshot.source.lane, ownerPvc: source.ownerPvc, stateDir, relativePath, sha256: `sha256:${sha256(bytes)}`, sizeBytes: bytes.byteLength, kind: relativePath === "analysis/report.json" ? "report-json" : "artifact", reachable: true }; }); }
function verifyReportLocator(row: Record<string, unknown>, locators: readonly MonitorArtifactLocator[]): void { const expectedHash = stringField(row, ["report_json_sha256", "reportJsonSha256"]); if (!expectedHash) return; const report = locators.find((locator) => locator.relativePath === "analysis/report.json"); if (!report || report.sha256 !== expectedHash) throw Object.assign(new Error("legacy report hash does not match artifact"), { code: "monitor-migration-report-locator-invalid" }); }
function metadataValue(rows: readonly Record<string, unknown>[], key: string): Record<string, unknown> | null { return jsonField(rows.find((row) => stringField(row, ["key"]) === key), ["value_json", "value"]); }
function normalizeLegacyFinding(row: Record<string, unknown>): Record<string, unknown> { return jsonField(row, ["finding", "value_json"]) ?? { id: stringField(row, ["finding_id", "id"]), severity: stringField(row, ["severity"]), count: numberField(row, ["count"]) ?? 1, summary: stringField(row, ["summary"]), reportJsonSha256: stringField(row, ["report_json_sha256", "reportJsonSha256"]), createdAt: stringField(row, ["created_at", "createdAt"]) }; }
function dedupeFindings(primary: readonly Record<string, unknown>[], secondary: readonly Record<string, unknown>[]): readonly Record<string, unknown>[] { const seen = new Set<string>(); return [...primary, ...secondary].filter((finding) => { const id = stringField(finding, ["id"]); const key = id || monitorCentralStableJson(finding); if (seen.has(key)) return false; seen.add(key); return true; }); }
function tableRows(tables: readonly MonitorSqliteTable[], names: readonly string[]): readonly Record<string, unknown>[] { return tables.filter((table) => names.includes(table.name)).flatMap((table) => table.rows); }
function sourcePlan(source: MonitorSqliteSource): Record<string, unknown> { const path = resolve(source.path); const state = existsSync(path) ? statSync(path) : null; return { sentinelId: source.sentinelId, node: source.node, lane: source.lane, path, runtimeConfigRef: source.runtimeConfigRef, exists: state !== null, isFile: state?.isFile() ?? false, byteLength: state?.size ?? null, mutation: false, valuesRedacted: true }; }
function assertReadOnlySqliteSource(value: string): string { const path = resolve(value); const state = statSync(path); if (!state.isFile()) throw new Error(`legacy SQLite source is not a file: ${path}`); const descriptor = openSync(path, "r"); closeSync(descriptor); return path; }
function writeSnapshot(path: string, content: string): void { const parent = dirname(resolve(path)); if (!existsSync(parent)) throw new Error(`snapshot output directory does not exist: ${parent}`); writeFileSync(path, content); }
function sqliteSnapshotPath(path: string): string { const resolved = resolve(path); return resolved.endsWith(".json") ? `${resolved.slice(0, -5)}.sqlite` : `${resolved}.sqlite`; }
function artifactSourceFor(source: MonitorSqliteSource, ownerPvc: string): MonitorArtifactSource { const artifactSource = source.artifactSources.find((item) => item.ownerPvc === ownerPvc); if (!artifactSource) throw Object.assign(new Error(`legacy artifact owner is not registry-mounted: ${ownerPvc}`), { code: "monitor-migration-artifact-source-missing" }); return artifactSource; }
function artifactSourceForStateDir(source: MonitorSqliteSource, stateDir: string): MonitorArtifactSource { if (!stateDir.startsWith("/") && source.artifactSources.length === 1) return source.artifactSources[0]!; const directory = resolve(stateDir); const matches = source.artifactSources.filter((item) => directory === resolve(item.mountPath) || directory.startsWith(`${resolve(item.mountPath)}/`)).sort((left, right) => resolve(right.mountPath).length - resolve(left.mountPath).length); if (matches.length === 0) throw Object.assign(new Error(`legacy artifact stateDir is not registry-mounted: ${stateDir}`), { code: "monitor-migration-artifact-source-missing" }); return matches[0]!; }
function artifactSourceForRunStateDir(source: MonitorSqliteSource, stateDir: string): MonitorArtifactSource { return artifactSourceForStateDir(source, stateDir); }
function artifactPath(mountPath: string, stateDir: string, relativePath: string): string { const root = resolve(mountPath); const directory = stateDir.startsWith("/") ? resolve(stateDir) : resolve(root, stateDir); if (directory !== root && !directory.startsWith(`${root}/`)) throw Object.assign(new Error("legacy artifact stateDir escapes registry mount"), { code: "monitor-migration-artifact-path-invalid" }); return join(directory, relativePath); }
function artifactFiles(directory: string): readonly string[] { return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { const path = join(directory, entry.name); if (entry.isDirectory()) return artifactFiles(path); return entry.isFile() ? [path] : []; }).sort(); }
function verifyFrozenArtifactManifests(snapshot: MonitorSqliteSnapshot): void {
  for (const manifest of snapshot.artifactManifests) {
    if (manifest.missingBeforeMigration !== undefined) {
      const gap = manifest.missingBeforeMigration;
      if (manifest.locators.length !== 0 || !matchesArtifactGap(gap, snapshot.source.legacyArtifactGaps ?? [])) throw Object.assign(new Error(`invalid frozen legacy artifact gap for ${manifest.runId}`), { code: "monitor-migration-artifact-gap-identity-invalid" });
      for (const source of snapshot.source.artifactSources) {
        const directory = artifactPath(source.mountPath, gap.stateDir, "");
        if (existsSync(directory) && (statSync(directory).isFile() || (statSync(directory).isDirectory() && artifactFiles(directory).length > 0))) throw Object.assign(new Error(`legacy artifact bytes recovered for ${manifest.runId}; re-export is required`), { code: "monitor-migration-artifact-gap-recovered", diagnostic: { runId: manifest.runId, stateDir: gap.stateDir, valuesRedacted: true } });
      }
      continue;
    }
    const expected = new Map(manifest.locators.map((locator) => [`${locator.ownerPvc}\u0000${locator.stateDir}\u0000${locator.relativePath}`, locator]));
    const observed = new Map<string, MonitorArtifactLocator>();
    for (const locator of manifest.locators) {
      const source = artifactSourceFor(snapshot.source, locator.ownerPvc);
      const path = artifactPath(source.mountPath, locator.stateDir, locator.relativePath);
      if (!existsSync(path) || !statSync(path).isFile()) throw frozenArtifactInvalid(manifest, null);
      const bytes = readFileSync(path);
      const actual = { ...locator, sha256: `sha256:${sha256(bytes)}`, sizeBytes: bytes.byteLength };
      if (actual.sha256 !== locator.sha256 || actual.sizeBytes !== locator.sizeBytes) throw frozenArtifactInvalid(manifest, manifest.locators.length);
      observed.set(`${locator.ownerPvc}\u0000${locator.stateDir}\u0000${locator.relativePath}`, actual);
    }
    if (!manifest.stateDir) { if (manifest.locators.length !== 0 || manifest.artifactCount !== 0) throw frozenArtifactInvalid(manifest, null); continue; }
    if (manifest.ownerPvc === null) throw frozenArtifactInvalid(manifest, null);
    const source = artifactSourceFor(snapshot.source, manifest.ownerPvc);
    const directory = artifactPath(source.mountPath, manifest.stateDir, "");
    if (!existsSync(directory) || !statSync(directory).isDirectory()) throw frozenArtifactInvalid(manifest, null);
    for (const path of artifactFiles(directory)) {
      const relativePath = path.slice(directory.length + 1).replaceAll("\\", "/");
      const key = `${manifest.ownerPvc}\u0000${manifest.stateDir}\u0000${relativePath}`;
      if (!expected.has(key)) throw frozenArtifactInvalid(manifest, manifest.locators.length + 1);
    }
    if (observed.size !== expected.size) throw frozenArtifactInvalid(manifest, observed.size);
  }
}

function frozenArtifactInvalid(manifest: MonitorArtifactManifest, observedCount: number | null): Error { return Object.assign(new Error(`frozen artifact set changed for ${manifest.runId}`), { code: "monitor-migration-frozen-artifact-invalid", diagnostic: { runId: manifest.runId, stateDir: manifest.stateDir, expectedCount: manifest.locators.length, observedCount, valuesRedacted: true } }); }
function verifySnapshotReferentialIntegrity(snapshot: MonitorSqliteSnapshot): void {
  const rows = tableRows(snapshot.tables, ["runs", "run"]);
  const rowByRunId = new Map<string, Record<string, unknown>>();
  for (const row of rows) { const runId = stringField(row, ["run_id", "id"]); if (!runId || rowByRunId.has(runId)) throw Object.assign(new Error("snapshot run identities are invalid"), { code: "monitor-migration-snapshot-referential-invalid" }); rowByRunId.set(runId, row); }
  const manifestByRunId = new Map<string, MonitorArtifactManifest>();
  for (const manifest of snapshot.artifactManifests) { if (!manifest.runId || manifestByRunId.has(manifest.runId) || !rowByRunId.has(manifest.runId)) throw Object.assign(new Error("snapshot artifact manifests are invalid"), { code: "monitor-migration-snapshot-referential-invalid" }); manifestByRunId.set(manifest.runId, manifest); }
  if (manifestByRunId.size !== rowByRunId.size) throw Object.assign(new Error("snapshot artifact manifests must match SQLite runs"), { code: "monitor-migration-snapshot-referential-invalid" });
  const declared = snapshot.source.legacyArtifactGaps ?? [];
  const declaredByRunId = new Map<string, MonitorLegacyArtifactGap>();
  for (const gap of declared) { if (!gap.runId || declaredByRunId.has(gap.runId)) throw Object.assign(new Error("snapshot legacy artifact gaps are invalid"), { code: "monitor-migration-snapshot-referential-invalid" }); declaredByRunId.set(gap.runId, gap); }
  for (const [runId, manifest] of manifestByRunId) {
    const row = rowByRunId.get(runId)!;
    const gap = manifest.missingBeforeMigration;
    if (gap === undefined) {
      const rowCount = numberField(row, ["artifact_count", "artifactCount"]);
      const stateDir = stringField(row, ["state_dir", "stateDir"]);
      const reportSha256 = stringField(row, ["report_json_sha256", "reportJsonSha256"]);
      const locatorRows = tableRows(snapshot.tables, ["artifact_locators", "locators"]);
      const expectedLocators = locatorRows.filter((locator) => stringField(locator, ["run_id"]) === runId).map((locator) => locatorBindingFromRow(locator, snapshot));
      if (declaredByRunId.has(runId) || manifest.artifactCount !== (rowCount ?? manifest.locators.length) || manifest.stateDir !== stateDir || manifest.reportSha256 !== reportSha256 || (stateDir.length > 0 && manifest.ownerPvc === null) || manifest.locators.some((locator) => locator.ownerPvc !== manifest.ownerPvc || locator.stateDir !== stateDir) || manifest.locators.length !== manifest.artifactCount || (expectedLocators.length > 0 && !sameLocatorBindings(manifest.locators, expectedLocators))) throw Object.assign(new Error("snapshot normal artifact manifest does not match run identity"), { code: "monitor-migration-snapshot-referential-invalid" });
      verifyReportLocator(row, manifest.locators);
      continue;
    }
    const declaredGap = declaredByRunId.get(runId);
    if (gap.runId !== runId || declaredGap === undefined || !sameArtifactGap(gap, declaredGap) || gap.artifactCount !== numberField(row, ["artifact_count", "artifactCount"]) || gap.stateDir !== stringField(row, ["state_dir", "stateDir"]) || gap.reportSha256 !== stringField(row, ["report_json_sha256", "reportJsonSha256"])) throw Object.assign(new Error("snapshot legacy artifact gap does not match run identity"), { code: "monitor-migration-snapshot-referential-invalid" });
  }
  if ([...declaredByRunId.keys()].some((runId) => manifestByRunId.get(runId)?.missingBeforeMigration === undefined)) throw Object.assign(new Error("snapshot declared gap has no matching frozen manifest"), { code: "monitor-migration-snapshot-referential-invalid" });
}
function locatorBindingFromRow(row: Record<string, unknown>, snapshot: Pick<MonitorSqliteSnapshot, "source">): MonitorArtifactLocator { return { ownerNode: stringField(row, ["owner_node", "ownerNode"]) || snapshot.source.node, ownerLane: stringField(row, ["owner_lane", "ownerLane"]) || snapshot.source.lane, ownerPvc: stringField(row, ["owner_pvc", "ownerPvc"]), stateDir: stringField(row, ["state_dir", "stateDir"]), relativePath: stringField(row, ["relative_path", "relativePath"]), sha256: stringField(row, ["sha256"]), sizeBytes: numberField(row, ["size_bytes", "sizeBytes"]) ?? -1, kind: stringField(row, ["kind"]) || "artifact", reachable: row.reachable !== false }; }
function sameLocatorBindings(left: readonly MonitorArtifactLocator[], right: readonly MonitorArtifactLocator[]): boolean { const key = (locator: MonitorArtifactLocator): string => [locator.ownerNode, locator.ownerLane, locator.ownerPvc, locator.stateDir, locator.relativePath, locator.sha256, locator.sizeBytes, locator.kind, locator.reachable].join("\u0000"); return left.length === right.length && left.map(key).sort().every((value, index) => value === right.map(key).sort()[index]); }
function artifactCountMismatch(runId: string, stateDir: string, expectedCount: number, observedCount: number): Error { return Object.assign(new Error(`legacy artifact locator count mismatch for ${runId}`), { code: "monitor-migration-artifact-count-mismatch", diagnostic: { runId, stateDir, expectedCount, observedCount, valuesRedacted: true } }); }
function artifactGapIdentityInvalid(runId: string, expected: MonitorLegacyArtifactGap, observed: Record<string, unknown>): Error { return Object.assign(new Error(`legacy artifact gap identity mismatch for ${runId}`), { code: "monitor-migration-artifact-gap-identity-invalid", diagnostic: { runId, expected, observed, valuesRedacted: true } }); }
function sameArtifactGap(left: MonitorLegacyArtifactGap, right: MonitorLegacyArtifactGap): boolean { return left.runId === right.runId && left.artifactCount === right.artifactCount && left.stateDir === right.stateDir && left.reportSha256 === right.reportSha256; }
function matchesArtifactGap(gap: MonitorLegacyArtifactGap, declared: readonly MonitorLegacyArtifactGap[]): boolean { return declared.some((item) => item.runId === gap.runId && item.artifactCount === gap.artifactCount && item.stateDir === gap.stateDir && item.reportSha256 === gap.reportSha256); }
function quoteIdentifier(value: string): string { return `"${value.replaceAll("\"", "\"\"")}"`; }
function normalizeRow(value: unknown): Record<string, unknown> { const row = value as Record<string, unknown>; return Object.fromEntries(Object.entries(row).map(([key, item]) => [key, item instanceof Uint8Array ? Buffer.from(item).toString("base64") : item])); }
function jsonField(row: Record<string, unknown> | undefined, fields: readonly string[]): Record<string, unknown> | null { if (!row) return null; const value = fields.map((field) => row[field]).find((item) => item !== undefined); if (typeof value === "string") try { const parsed = JSON.parse(value); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null; } catch { return null; } return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null; }
function jsonValue(row: Record<string, unknown> | undefined, fields: readonly string[]): unknown { if (!row) return null; const value = fields.map((field) => row[field]).find((item) => item !== undefined); if (typeof value !== "string") return value ?? null; try { return JSON.parse(value); } catch { return value; } }
function stringField(row: Record<string, unknown> | undefined, fields: readonly string[]): string { const value = row === undefined ? null : fields.map((field) => row[field]).find((item) => typeof item === "string" && item.length > 0); return typeof value === "string" ? value : ""; }
function numberField(row: Record<string, unknown>, fields: readonly string[]): number | null { const value = fields.map((field) => row[field]).find((item) => typeof item === "number"); return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null; }
function sha256(value: Uint8Array): string { return createHash("sha256").update(value).digest("hex"); }
export function monitorSnapshotContentHash(snapshot: Omit<MonitorSqliteSnapshot, "manifestContentHash"> | MonitorSqliteSnapshot): string { const { manifestContentHash: _ignored, ...unsigned } = snapshot as MonitorSqliteSnapshot; return `sha256:${sha256(Buffer.from(monitorCentralStableJson(unsigned)))}`; }
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
