diff --git a/scripts/monitor-sqlite-migration-import.ts b/scripts/monitor-sqlite-migration-import.ts index 48b5d8c5..aad27382 100644 --- a/scripts/monitor-sqlite-migration-import.ts +++ b/scripts/monitor-sqlite-migration-import.ts @@ -17,11 +17,12 @@ function positive(name: string): number { return value; } +let store: ReturnType | null = null; try { const snapshotPath = process.argv[2]; if (!snapshotPath) throw Object.assign(new Error("snapshot path is required"), { code: "monitor_migration_snapshot_missing" }); const snapshot = JSON.parse(readFileSync(snapshotPath, "utf8")) as MonitorSqliteSnapshot; - const store = createPostgresMonitorCentralStore(required("DATABASE_URL"), { poolMax: positive("MONITOR_CENTRAL_PG_POOL_MAX"), idleTimeoutSeconds: positive("MONITOR_CENTRAL_PG_IDLE_TIMEOUT_SECONDS") }); + store = createPostgresMonitorCentralStore(required("DATABASE_URL"), { poolMax: positive("MONITOR_CENTRAL_PG_POOL_MAX"), idleTimeoutSeconds: positive("MONITOR_CENTRAL_PG_IDLE_TIMEOUT_SECONDS") }); await store.ping(); if (await store.schemaVersion() !== MONITOR_CENTRAL_SCHEMA_VERSION) throw Object.assign(new Error("central monitor schema is not ready"), { code: "monitor_migration_schema_unready" }); if (!(await store.capability()).ingest) throw Object.assign(new Error("central monitor ingest capability is unavailable"), { code: "monitor_migration_capability_unavailable" }); @@ -29,4 +30,6 @@ try { } catch (error) { console.log(JSON.stringify({ ok: false, error: structuredMonitorCentralError(error), valuesRedacted: true })); process.exitCode = 2; +} finally { + await store?.close(); } diff --git a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts index c1724f8d..d096393e 100644 --- a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts +++ b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts @@ -1033,7 +1033,7 @@ function submitCentralTerminalIngest(state: SentinelCicdState, payload: Record | null { +export function analysisSummaryFromAnalyzeResult(analysis: ChildCliResult, fallbackStateDir: string | null): Record | null { const payload = cliDataPayload(analysis.parsed); const source = record(payload.analysis); - const reportJsonSha256 = stringAtNullable(source, "reportJsonSha256"); + const analyzer = record(source.analyzer); + const reportJsonSha256 = stringAtNullable(source, "reportJsonSha256") ?? stringAtNullable(analyzer, "reportJsonSha256"); if (reportJsonSha256 === null) return null; const counts = record(source.counts); const archiveSummary = record(source.archiveSummary); @@ -1428,13 +1429,13 @@ function analysisSummaryFromAnalyzeResult(analysis: ChildCliResult, fallbackStat source: "observe-analyze-raw", reason: null, reportOk: source.ok === true, - stateDir: stringAtNullable(source, "stateDir") ?? fallbackStateDir, - reportJsonPath: stringAtNullable(source, "reportJsonPath"), + stateDir: stringAtNullable(source, "stateDir") ?? stringAtNullable(analyzer, "stateDir") ?? fallbackStateDir, + reportJsonPath: stringAtNullable(source, "reportJsonPath") ?? stringAtNullable(analyzer, "reportJsonPath"), reportJsonSha256, - reportJsonBytes: numberAtNullable(source, "reportJsonBytes"), - reportUpdatedAt: stringAtNullable(source, "reportUpdatedAt"), - reportMdPath: stringAtNullable(source, "reportMdPath"), - reportMdSha256: stringAtNullable(source, "reportMdSha256"), + reportJsonBytes: numberAtNullable(source, "reportJsonBytes") ?? numberAtNullable(analyzer, "reportJsonBytes"), + reportUpdatedAt: stringAtNullable(source, "reportUpdatedAt") ?? stringAtNullable(analyzer, "reportUpdatedAt"), + reportMdPath: stringAtNullable(source, "reportMdPath") ?? stringAtNullable(analyzer, "reportMdPath"), + reportMdSha256: stringAtNullable(source, "reportMdSha256") ?? stringAtNullable(analyzer, "reportMdSha256"), findingCount, artifactCount: numberAtNullable(source, "artifactCount") ?? numberAtNullable(counts, "artifacts") ?? 0, findings, diff --git a/scripts/src/monitor-central-persistence.test.ts b/scripts/src/monitor-central-persistence.test.ts index 30557664..c0b1f702 100644 --- a/scripts/src/monitor-central-persistence.test.ts +++ b/scripts/src/monitor-central-persistence.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { test } from "bun:test"; -import { createInMemoryMonitorCentralStore, createMonitorCentralService, mapPostgresFindingRow, MONITOR_CENTRAL_MIGRATIONS, MonitorCentralConfigurationError, structuredMonitorCentralError } from "./monitor-central-persistence"; +import { createInMemoryMonitorCentralStore, createMonitorCentralService, mapPostgresFindingRow, MONITOR_CENTRAL_MIGRATIONS, MonitorCentralConfigurationError, monitorCentralStableJson, normalizeMonitorCentralIngest, structuredMonitorCentralError } from "./monitor-central-persistence"; function service() { return createMonitorCentralService({ store: createInMemoryMonitorCentralStore(), pageSize: 50 }); } function locator(runId: string) { return { ownerNode: "NC01", ownerLane: "v03", ownerPvc: "sentinel-pvc", stateDir: `/state/${runId}`, relativePath: "analysis/report.json", sha256: "sha256:abc", sizeBytes: 12, kind: "report", reachable: true }; } @@ -44,6 +44,14 @@ test("full terminal envelope controls hash and concurrent same identity converge assert.equal(detail.run.artifactLocators[0].reachable, false); }); +test("stable JSON accepts optional fields and in-memory view matches the PG contract", async () => { + assert.equal(monitorCentralStableJson({ optional: undefined, value: "ok", list: [undefined] }), '{"list":[null],"value":"ok"}'); + const store = createInMemoryMonitorCentralStore(); + const normalized = normalizeMonitorCentralIngest({ ...input("top-level-view"), payload: { views: { summary: { state: "ready" } }, optional: undefined } }); + await store.ingest(normalized); + assert.deepEqual(await store.view({ sentinelId: "sentinel-a", node: "NC01", lane: "v03" }, "top-level-view", "summary"), { run: { sentinelId: "sentinel-a", node: "NC01", lane: "v03", runId: "top-level-view", updatedAt: normalized.updatedAt, status: normalized.status, payloadHash: normalized.payloadHash }, view: "summary", value: { state: "ready" } }); +}); + test("global and scoped latest use updatedAt then runId descending", async () => { const target = service(); await ingest(target, "run-a", "2026-07-12T10:00:00.000Z"); diff --git a/scripts/src/monitor-central-persistence.ts b/scripts/src/monitor-central-persistence.ts index 6bf00b2a..be289634 100644 --- a/scripts/src/monitor-central-persistence.ts +++ b/scripts/src/monitor-central-persistence.ts @@ -46,6 +46,7 @@ export interface MonitorRun extends Required { } export interface MonitorCentralStore { + close(): Promise; migrate(): Promise; ping(): Promise; schemaVersion(): Promise; @@ -176,6 +177,7 @@ export function createMonitorCentralService(options: MonitorCentralServiceOption export function createPostgresMonitorCentralStore(databaseUrl: string, options: PostgresMonitorCentralStoreOptions): MonitorCentralStore { const sql = postgres(databaseUrl, { max: requirePositiveInteger(options.poolMax, "monitor_central_pg_pool_max_missing"), idle_timeout: requirePositiveInteger(options.idleTimeoutSeconds, "monitor_central_pg_idle_timeout_missing") }); return { + async close() { await sql.end({ timeout: 0 }); }, async migrate() { for (const statement of MONITOR_CENTRAL_MIGRATIONS) await sql.unsafe(statement); await sql`insert into monitor.schema_migrations (version, applied_at) values (${MONITOR_CENTRAL_SCHEMA_VERSION}, now()) on conflict (version) do nothing`; @@ -197,9 +199,13 @@ export function createPostgresMonitorCentralStore(databaseUrl: string, options: }, async importSource(manifest, inputs) { return sql.begin(async (transaction) => { - const existing = await transaction<{ source_fingerprint: string; row_count: string | number }[]>` - select source_fingerprint, row_count from monitor.import_manifests where manifest_id = ${manifest.manifestId} for update`; - if (existing[0]) { + const claimed = await transaction<{ manifest_id: string }[]>` + insert into monitor.import_manifests (manifest_id, source_fingerprint, row_count, imported_at) + values (${manifest.manifestId}, ${manifest.sourceFingerprint}, ${manifest.rowCount}, now()) + on conflict (manifest_id) do nothing returning manifest_id`; + if (!claimed[0]) { + const existing = await transaction<{ source_fingerprint: string; row_count: string | number }[]>` + select source_fingerprint, row_count from monitor.import_manifests where manifest_id = ${manifest.manifestId}`; if (existing[0].source_fingerprint !== manifest.sourceFingerprint || Number(existing[0].row_count) !== manifest.rowCount) { throw Object.assign(new Error("monitor import manifest conflict"), { code: "monitor_import_manifest_conflict" }); } @@ -207,9 +213,6 @@ export function createPostgresMonitorCentralStore(databaseUrl: string, options: } const runs = [] as { disposition: "inserted" | "idempotent" }[]; for (const input of inputs) runs.push(await writePostgresIngest(transaction, input)); - await transaction` - insert into monitor.import_manifests (manifest_id, source_fingerprint, row_count, imported_at) - values (${manifest.manifestId}, ${manifest.sourceFingerprint}, ${manifest.rowCount}, now())`; return { disposition: "inserted" as const, runs }; }); }, @@ -307,6 +310,7 @@ export function createInMemoryMonitorCentralStore(): MonitorCentralStore { const records = new Map(); const manifests = new Map(); return { + async close() {}, async migrate() {}, async ping() {}, async schemaVersion() { return MONITOR_CENTRAL_SCHEMA_VERSION; }, @@ -361,7 +365,7 @@ export function createInMemoryMonitorCentralStore(): MonitorCentralStore { const detail = await this.run(scope, runId); const views = detail?.payload && typeof detail.payload === "object" ? (detail.payload as Record).views : null; const value = views && typeof views === "object" ? (views as Record)[view] : undefined; - return value === undefined ? null : { runId, name: view, value }; + return value === undefined || !detail ? null : { run: runFrom(detail as MonitorRun), view, value }; }, async findings(scope, options) { return sortRuns([...records.values()].filter((item) => matches(scope, item)).map(runFrom)).flatMap((run) => { @@ -416,7 +420,7 @@ function sortRuns(runs: readonly MonitorRun[]): MonitorRun[] { return [...runs]. function afterRunCursor(run: MonitorRun, cursorValue: RunCursor | null): boolean { return !cursorValue || run.updatedAt < cursorValue.updatedAt || (run.updatedAt === cursorValue.updatedAt && run.runId < cursorValue.runId); } function compareFindings(left: Record, right: Record): number { return String(right.updatedAt).localeCompare(String(left.updatedAt)) || String(right.runId).localeCompare(String(left.runId)) || Number(right.findingIndex) - Number(left.findingIndex); } function afterFindingCursor(finding: Record, cursorValue: FindingCursor | null): boolean { if (!cursorValue) return true; return String(finding.updatedAt) < cursorValue.updatedAt || (finding.updatedAt === cursorValue.updatedAt && (String(finding.runId) < cursorValue.runId || (finding.runId === cursorValue.runId && Number(finding.findingIndex) < cursorValue.findingIndex))); } -export function monitorCentralStableJson(value: unknown): string { if (Array.isArray(value)) return `[${value.map(monitorCentralStableJson).join(",")}]`; if (value && typeof value === "object") return `{${Object.entries(value as Record).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${monitorCentralStableJson(item)}`).join(",")}}`; return JSON.stringify(value); } +export function monitorCentralStableJson(value: unknown): string { if (value === undefined) return "null"; if (Array.isArray(value)) return `[${value.map(monitorCentralStableJson).join(",")}]`; if (value && typeof value === "object") return `{${Object.entries(value as Record).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${monitorCentralStableJson(item)}`).join(",")}}`; const json = JSON.stringify(value); return json === undefined ? "null" : json; } function scopeWhere(scope: MonitorScope): { readonly text: string; readonly values: readonly string[] } { const entries = Object.entries({ sentinel_id: scope.sentinelId, node: scope.node, lane: scope.lane }).filter(([, value]) => value); return { text: entries.length ? entries.map(([key], index) => `r.${key} = $${index + 1}`).join(" and ") : "true", values: entries.map(([, value]) => String(value)) }; } function identityWhere(scope: Required, runId: string): { readonly text: string; readonly values: readonly string[] } { return { text: "r.sentinel_id = $1 and r.node = $2 and r.lane = $3 and r.run_id = $4", values: [scope.sentinelId, scope.node, scope.lane, runId] }; } async function queryRuns(sql: postgres.Sql, scope: MonitorScope, options: RunQueryOptions): Promise { const where = scopeWhere(scope); const values: unknown[] = [...where.values]; let cursorClause = ""; if (options.cursor) { values.push(options.cursor.updatedAt, options.cursor.runId); cursorClause = `and (r.updated_at, r.run_id) < ($${values.length - 1}, $${values.length})`; } values.push(options.limit); const rows = await sql.unsafe(`select r.sentinel_id, r.node, r.lane, r.run_id, r.updated_at, r.status, r.payload_hash from monitor.runs r where ${where.text} ${cursorClause} order by r.updated_at desc, r.run_id desc limit $${values.length}`, values); return rows.map(runRow); } diff --git a/scripts/src/monitor-sqlite-migration.ts b/scripts/src/monitor-sqlite-migration.ts index 697f7e91..fb2c6cba 100644 --- a/scripts/src/monitor-sqlite-migration.ts +++ b/scripts/src/monitor-sqlite-migration.ts @@ -27,15 +27,20 @@ export function exportMonitorSqliteSnapshot(source: MonitorSqliteSource, outputP const path = assertReadOnlySqliteSource(source.path); const sourceBytes = readFileSync(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 unsigned = { schema: MONITOR_SQLITE_MIGRATION_SCHEMA, createdAt: new Date().toISOString(), source: { ...source, path }, fingerprint: `sha256:${sha256(sourceBytes)}`, integrity, byteLength: sourceBytes.byteLength, tables, artifactLocator: { path, sha256: `sha256:${sha256(sourceBytes)}`, sizeBytes: sourceBytes.byteLength, kind: "legacy-sqlite" } }; const snapshot: MonitorSqliteSnapshot = { ...unsigned, manifestContentHash: monitorSnapshotContentHash(unsigned) }; writeSnapshot(outputPath, `${monitorCentralStableJson(snapshot)}\n`); + database.exec("COMMIT"); + transactionOpen = false; return snapshot; - } finally { database.close(); } + } finally { if (transactionOpen) database.exec("ROLLBACK"); database.close(); } } export function verifyMonitorSqliteSnapshot(snapshot: MonitorSqliteSnapshot): Record { @@ -76,9 +81,11 @@ function canonicalIngests(snapshot: MonitorSqliteSnapshot): readonly ReturnType< const report = metadataValue(metadata, `run.report.${runId}`); const rowLocators = locators.filter((item) => stringField(item, ["run_id"]) === runId).map((item) => locatorFromRow(item, snapshot)); const reportLocator = reportLocatorFromRun(row, snapshot); - const payload = jsonField(payloads.find((item) => stringField(item, ["run_id"]) === runId), ["payload", "value_json"]) ?? { ...(report ?? {}), legacy: { run: row, sourceFingerprint: snapshot.fingerprint } }; + 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) : []; - 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, findings: rowFindings.length ? rowFindings : reportFindings, artifactLocators: reportLocator ? [...rowLocators, reportLocator] : rowLocators, timeline: [] }); + const canonicalFindings = reportFindings.length ? [...reportFindings, ...rowFindings.filter((item) => !reportFindings.some((reportFinding) => reportFinding.id === item.id))] : rowFindings; + 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, findings: canonicalFindings, artifactLocators: reportLocator ? [...rowLocators, reportLocator] : rowLocators, timeline: [] }); }); } function locatorFromRow(row: Record, source: MonitorSqliteSnapshot): MonitorArtifactLocator { const ownerPvc = stringField(row, ["owner_pvc", "ownerPvc"]); const relativePath = stringField(row, ["relative_path", "relativePath"]); const hash = stringField(row, ["sha256"]); const sizeBytes = numberField(row, ["size_bytes", "sizeBytes"]); if (!ownerPvc || !relativePath || !hash || sizeBytes === null) throw Object.assign(new Error("legacy artifact locator is incomplete"), { code: "monitor-migration-locator-invalid" }); return { ownerNode: stringField(row, ["owner_node", "ownerNode"]) || source.source.node, ownerLane: stringField(row, ["owner_lane", "ownerLane"]) || source.source.lane, ownerPvc, stateDir: stringField(row, ["state_dir", "stateDir"]), relativePath, sha256: hash, sizeBytes, kind: stringField(row, ["kind"]) || "artifact", reachable: row.reachable !== false }; } diff --git a/scripts/src/monitor-terminal-ingest-migration.test.ts b/scripts/src/monitor-terminal-ingest-migration.test.ts index 00f784b1..da3e79c4 100644 --- a/scripts/src/monitor-terminal-ingest-migration.test.ts +++ b/scripts/src/monitor-terminal-ingest-migration.test.ts @@ -9,6 +9,7 @@ import { Database } from "bun:sqlite"; import { createInMemoryMonitorCentralStore } from "./monitor-central-persistence"; import { exportMonitorSqliteSnapshot, importMonitorSqliteSnapshot, planMonitorSqliteSources, verifyMonitorSqliteSnapshot } from "./monitor-sqlite-migration"; import { buildMonitorTerminalIngest, MonitorIngestFailedError, resolveMonitorTerminalWriter, submitMonitorTerminalIngest } from "./monitor-terminal-ingest"; +import { analysisSummaryFromAnalyzeResult } from "./hwlab-node-web-sentinel-p5-observe"; test("terminal ingest payload is immutable and retries bounded failures", async () => { const report = { ok: false, findings: [{ id: "blocked" }], updatedAt: "2026-07-12T00:00:00.000Z" }; @@ -35,6 +36,11 @@ test("terminal writer requires explicit mutually exclusive authority", () => { assert.throws(() => resolveMonitorTerminalWriter({ sqlite: { path: "/state/legacy.sqlite" }, monitor: { terminalWriter: { mode: "central", ingest: { endpoint: "https://monitor.example.test/api/ingest", timeoutMs: 1_000, retry: { maxAttempts: 2, delayMs: 0 } } } } })); }); +test("runner-shaped analyze result projects nested report artifact facts", () => { + const summary = analysisSummaryFromAnalyzeResult({ ok: true, parsed: { data: { analysis: { analyzer: { stateDir: "run-1", reportJsonPath: "/state/run-1/analysis/report.json", reportJsonSha256: `sha256:${"c".repeat(64)}`, reportJsonBytes: 12, reportUpdatedAt: "2026-07-12T00:00:00.000Z" } } } }, result: {} } as never, null); + assert.deepEqual(summary && { stateDir: summary.stateDir, reportJsonSha256: summary.reportJsonSha256, reportJsonBytes: summary.reportJsonBytes, reportUpdatedAt: summary.reportUpdatedAt }, { stateDir: "run-1", reportJsonSha256: `sha256:${"c".repeat(64)}`, reportJsonBytes: 12, reportUpdatedAt: "2026-07-12T00:00:00.000Z" }); +}); + test("SQLite snapshot reconciles the 228/242/234 fixture shape without production assumptions", async () => { const directory = mkdtempSync(join(tmpdir(), "monitor-migration-")); try { @@ -47,11 +53,11 @@ test("SQLite snapshot reconciles the 228/242/234 fixture shape without productio writeFileSync(reportPath, JSON.stringify({ updatedAt: "2026-07-12T00:00:00.000Z", views: { summary: { title: "legacy summary" } }, findings: [{ id: "report-finding" }] })); const reportHash = `sha256:${Bun.CryptoHasher.hash("sha256", readFileSync(reportPath), "hex")}`; database.run("CREATE TABLE runs (id TEXT, state_dir TEXT, report_json_sha256 TEXT, updated_at TEXT)"); - database.run("CREATE TABLE findings (id TEXT)"); + database.run("CREATE TABLE findings (run_id TEXT, finding TEXT)"); database.run("CREATE TABLE metadata (id TEXT, key TEXT, value_json TEXT)"); for (let index = 0; index < 228; index += 1) database.run("INSERT INTO runs VALUES (?, ?, ?, ?)", [`run-${index}`, index === 0 ? "run-0" : null, index === 0 ? reportHash : null, "2026-07-12T00:00:00.000Z"]); - for (let index = 0; index < 242; index += 1) database.run("INSERT INTO findings VALUES (?)", [`finding-${index}`]); - for (let index = 0; index < 234; index += 1) database.run("INSERT INTO metadata VALUES (?, ?, ?)", [`metadata-${index}`, index === 0 ? "run.report.run-0" : `metadata-${index}`, index === 0 ? JSON.stringify({ views: { summary: { title: "legacy summary" } }, findings: [{ id: "report-finding" }] }) : "{}"]); + for (let index = 0; index < 242; index += 1) database.run("INSERT INTO findings VALUES (?, ?)", [index === 0 ? "run-0" : `run-${index % 228}`, JSON.stringify({ id: index === 0 ? "report-finding" : `finding-${index}`, summary: "indexed" })]); + for (let index = 0; index < 234; index += 1) database.run("INSERT INTO metadata VALUES (?, ?, ?)", [`metadata-${index}`, index === 0 ? "run.report.run-0" : `metadata-${index}`, index === 0 ? JSON.stringify({ views: { summary: { title: "legacy summary" } }, findings: [{ id: "report-finding", rootCause: "complete metadata detail" }] }) : "{}"]); database.close(); const source = { sentinelId: "fixture", node: "NC01", lane: "v03", path: sqlitePath, runtimeConfigRef: "fixture#runtime", stateRoot, ownerPvc: "fixture-pvc" }; const plan = planMonitorSqliteSources([source]); diff --git a/scripts/src/monitor-terminal-ingest.ts b/scripts/src/monitor-terminal-ingest.ts index 24975958..39104796 100644 --- a/scripts/src/monitor-terminal-ingest.ts +++ b/scripts/src/monitor-terminal-ingest.ts @@ -23,7 +23,7 @@ export function buildMonitorTerminalIngest(identity: MonitorTerminalIdentity, re ...identity, updatedAt, status: stringValue(immutableReport.status) ?? (immutableReport.ok === true ? "ok" : "blocked"), - payload: { report: immutableReport, artifacts: artifacts.map((artifact) => ({ ...artifact })) }, + payload: { ...immutableReport, report: immutableReport, artifacts: artifacts.map((artifact) => ({ ...artifact })) }, findings: recordArray(immutableReport.findings), timeline: recordArray(immutableReport.timeline), artifactLocators: artifacts.map((artifact) => ({ ...locatorOwner, relativePath: artifact.relativePath, kind: artifact.kind, sha256: artifact.sha256, sizeBytes: artifact.sizeBytes, reachable: true })),