From 977a610ea96c8e7a2073164c1b6658946c11c25c Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Sun, 17 May 2026 22:02:32 +0200 Subject: [PATCH] fix(ingest): gate provenance before isolated diff squash --- ...ingest-bundle.runner.isolated-diff.test.ts | 169 ++++++++ .../src/ingest/ingest-bundle.runner.test.ts | 6 +- .../src/ingest/ingest-bundle.runner.ts | 373 +++++++++++++----- .../context/src/ingest/report-snapshot.ts | 1 + packages/context/src/ingest/reports.ts | 1 + 5 files changed, 440 insertions(+), 110 deletions(-) diff --git a/packages/context/src/ingest/ingest-bundle.runner.isolated-diff.test.ts b/packages/context/src/ingest/ingest-bundle.runner.isolated-diff.test.ts index 382e2e5f..4a9119ff 100644 --- a/packages/context/src/ingest/ingest-bundle.runner.isolated-diff.test.ts +++ b/packages/context/src/ingest/ingest-bundle.runner.isolated-diff.test.ts @@ -609,6 +609,175 @@ describe('IngestBundleRunner isolated diff path', () => { } }); + it('rejects invalid provenance raw paths before squash reaches main', async () => { + const runtime = await makeRealGitRuntime(); + try { + const { deps, adapter } = makeDeps(runtime); + const createdReports: any[] = []; + deps.reports.create = vi.fn(async (args: any) => { + createdReports.push(args); + return { id: `report-${createdReports.length}` }; + }); + adapter.chunk.mockResolvedValue({ + workUnits: [ + { + unitKey: 'card-valid-artifacts', + rawFiles: ['cards/source.json'], + peerFileIndex: [], + dependencyPaths: [], + }, + ], + }); + + let currentSession: any = null; + deps.toolsetFactory.createIngestWuToolset = vi.fn((toolSession: any) => { + currentSession = toolSession; + return { toRuntimeTools: vi.fn(() => ({})) }; + }); + deps.agentRunner.runLoop = vi.fn(async () => { + const root = rootOfConfig(currentSession.configService, runtime.configDir); + await mkdir(join(root, 'semantic-layer/warehouse'), { recursive: true }); + await mkdir(join(root, 'wiki/global'), { recursive: true }); + await writeFile( + join(root, 'semantic-layer/warehouse/mart_account_segments.yaml'), + 'name: mart_account_segments\ngrain: [account_id]\ncolumns: [{name: account_id, type: string}]\njoins: []\nmeasures:\n - name: total_contract_arr\n expr: sum(contract_arr)\n', + ); + await writeFile( + join(root, 'wiki/global/account-segments.md'), + '---\nsummary: Account segments\nusage_mode: auto\nsl_refs:\n - mart_account_segments\n---\n\nARR is `mart_account_segments.total_contract_arr`.\n', + ); + addTouchedSlSource(currentSession.touchedSlSources, 'warehouse', 'mart_account_segments'); + currentSession.actions.push({ + target: 'sl', + type: 'created', + key: 'mart_account_segments', + detail: 'Valid source', + targetConnectionId: 'warehouse', + rawPaths: ['cards/source.json'], + }); + currentSession.actions.push({ + target: 'wiki', + type: 'created', + key: 'account-segments', + detail: 'Valid wiki with invalid provenance raw path', + rawPaths: ['cards/missing.json'], + }); + await currentSession.gitService.commitFiles( + ['semantic-layer/warehouse/mart_account_segments.yaml', 'wiki/global/account-segments.md'], + 'valid artifacts with invalid provenance', + 'KTX Test', + 'system@ktx.local', + ); + return { stopReason: 'natural' }; + }) as never; + + const runner = new IngestBundleRunner(deps); + await mockStageRawFiles(runner, runtime, [['cards/source.json', 'h1']]); + const preRunHead = await runtime.git.revParseHead(); + + await expect( + runner.run({ + jobId: 'job-invalid-provenance', + connectionId: 'warehouse', + sourceKey: 'metabase', + trigger: 'upload', + bundleRef: { kind: 'upload', uploadId: 'upload' }, + }), + ).rejects.toThrow(/provenance row references raw path outside this snapshot: cards\/missing\.json/); + + expect(await runtime.git.revParseHead()).toBe(preRunHead); + expect(deps.provenance.insertMany).not.toHaveBeenCalled(); + + const failureReport = createdReports.find((report) => report.body.status === 'failed'); + expect(failureReport.body.tracePath).toContain('job-invalid-provenance/trace.jsonl'); + expect(failureReport.body.failure).toMatchObject({ + phase: 'provenance_validation', + message: expect.stringContaining('cards/missing.json'), + }); + expect(failureReport.body.failure.details).toMatchObject({ + invalidRawPaths: ['cards/missing.json'], + currentRawPaths: ['cards/source.json'], + invalidRows: expect.arrayContaining([ + expect.objectContaining({ + row: expect.objectContaining({ + rawPath: 'cards/missing.json', + artifactKind: 'wiki', + artifactKey: 'account-segments', + actionType: 'wiki_written', + }), + origin: expect.objectContaining({ + source: 'work_unit_action', + unitKey: 'card-valid-artifacts', + actionIndex: 1, + unitRawFiles: ['cards/source.json'], + action: expect.objectContaining({ + target: 'wiki', + type: 'created', + key: 'account-segments', + rawPaths: ['cards/missing.json'], + }), + }), + }), + ]), + }); + expect(failureReport.body.provenanceRows).toEqual( + expect.arrayContaining([ + expect.objectContaining({ rawPath: 'cards/source.json', artifactKind: 'sl', artifactKey: 'mart_account_segments' }), + expect.objectContaining({ rawPath: 'cards/missing.json', artifactKind: 'wiki', artifactKey: 'account-segments' }), + ]), + ); + expect(failureReport.body.workUnits).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + unitKey: 'card-valid-artifacts', + rawFiles: ['cards/source.json'], + actions: expect.arrayContaining([ + expect.objectContaining({ + target: 'wiki', + key: 'account-segments', + rawPaths: ['cards/missing.json'], + }), + ]), + }), + ]), + ); + + const events = (await readFile(join(runtime.configDir, '.ktx/ingest-traces/job-invalid-provenance/trace.jsonl'), 'utf-8')) + .trim() + .split('\n') + .map((line) => JSON.parse(line)); + expect(events.map((event) => event.event)).toEqual( + expect.arrayContaining([ + 'final_artifact_gates_finished', + 'provenance_rows_validation_failed', + 'ingest_failed', + 'failure_report_created', + ]), + ); + expect(events.map((event) => event.event)).not.toContain('squash_finished'); + const validationFailure = events.find((event) => event.event === 'provenance_rows_validation_failed'); + expect(validationFailure).toMatchObject({ + phase: 'provenance', + data: { + invalidRawPaths: ['cards/missing.json'], + currentRawPaths: ['cards/source.json'], + invalidRows: expect.arrayContaining([ + expect.objectContaining({ + row: expect.objectContaining({ rawPath: 'cards/missing.json' }), + origin: expect.objectContaining({ + source: 'work_unit_action', + unitKey: 'card-valid-artifacts', + actionIndex: 1, + }), + }), + ]), + }, + }); + } finally { + await rm(runtime.homeDir, { recursive: true, force: true }); + } + }); + it('rejects slDisallowed patches that touch semantic-layer files', async () => { const runtime = await makeRealGitRuntime(); try { diff --git a/packages/context/src/ingest/ingest-bundle.runner.test.ts b/packages/context/src/ingest/ingest-bundle.runner.test.ts index a315dbca..ccd9ede7 100644 --- a/packages/context/src/ingest/ingest-bundle.runner.test.ts +++ b/packages/context/src/ingest/ingest-bundle.runner.test.ts @@ -167,10 +167,11 @@ const makeDeps = () => { loadPrompt: vi.fn().mockResolvedValue('base-framing'), }; const wikiService = { - forWorktree: vi.fn().mockReturnValue({}), + forWorktree: vi.fn(), readPage: vi.fn().mockResolvedValue(null), syncFromCommit: vi.fn().mockResolvedValue(undefined), }; + wikiService.forWorktree.mockReturnValue(wikiService); const knowledgeSlRefs = { syncFromWiki: vi.fn().mockResolvedValue({ inserted: 1, deleted: 0 }), }; @@ -178,7 +179,7 @@ const makeDeps = () => { listPagesForUser: vi.fn().mockResolvedValue([]), }; const semanticLayerService = { - forWorktree: vi.fn().mockReturnValue({}), + forWorktree: vi.fn(), listFilesForConnection: vi .fn() .mockImplementation((connectionId: string) => @@ -193,6 +194,7 @@ const makeDeps = () => { }), ), }; + semanticLayerService.forWorktree.mockReturnValue(semanticLayerService); const slSearchService = { indexSources: vi.fn().mockResolvedValue(undefined), }; diff --git a/packages/context/src/ingest/ingest-bundle.runner.ts b/packages/context/src/ingest/ingest-bundle.runner.ts index e81fdfb9..db78830e 100644 --- a/packages/context/src/ingest/ingest-bundle.runner.ts +++ b/packages/context/src/ingest/ingest-bundle.runner.ts @@ -22,6 +22,7 @@ import type { MemoryFlowEventSink, MemoryFlowPlannedWorkUnit } from './memory-fl import type { ContextEvidenceIndexSummary, IngestBundleRunnerDeps, + IngestProvenanceInsert, IngestProvenanceRow, IngestRunsPort, IngestSessionWorktree, @@ -32,7 +33,9 @@ import { buildStageIndexFromReportBody, postProcessorSavedMemoryCounts, type IngestReportPostProcessorOutcome, + type IngestReportProvenanceDetail, type IngestReportSnapshot, + type IngestReportWorkUnit, } from './reports.js'; import { buildReconcileSystemPrompt, @@ -142,6 +145,40 @@ function rawPathsForAction(action: MemoryAction, fallbackRawPaths: string[]): st return action.rawPaths && action.rawPaths.length > 0 ? [...new Set(action.rawPaths)] : fallbackRawPaths; } +type ProvenanceRowOrigin = + | { + source: 'work_unit_action'; + unitKey: string; + unitIndex: number; + unitRawFiles: string[]; + actionIndex: number; + action: MemoryAction; + } + | { + source: 'reconciliation_action'; + actionIndex: number; + action: MemoryAction; + } + | { + source: 'artifact_resolution'; + resolutionIndex: number; + resolution: NonNullable[number]; + } + | { + source: 'raw_snapshot_fallback'; + rawPath: string; + }; + +interface ProvenanceRowDiagnostic { + row: IngestProvenanceInsert; + origin: ProvenanceRowOrigin; +} + +interface ProvenancePlan { + rows: IngestProvenanceInsert[]; + diagnostics: ProvenanceRowDiagnostic[]; +} + export class IngestBundleRunner { private readonly logger: KtxLogger; private readonly chainByConnection = new Map>(); @@ -426,6 +463,157 @@ export class IngestBundleRunner { return error instanceof Error ? error.message : String(error); } + private buildProvenancePlan(input: { + job: IngestBundleJob; + syncId: string; + currentHashes: Map; + stageIndex: StageIndex; + reconcileActions: MemoryAction[]; + }): ProvenancePlan { + const rows: IngestProvenanceInsert[] = []; + const diagnostics: ProvenanceRowDiagnostic[] = []; + const actionToType = (action: MemoryAction): IngestProvenanceInsert['actionType'] => { + if (action.target === 'wiki') { + return 'wiki_written'; + } + return action.type === 'created' ? 'source_created' : 'measure_added'; + }; + const producedPaths = new Set(); + const pushRow = (row: IngestProvenanceInsert, origin: ProvenanceRowOrigin): void => { + rows.push(row); + diagnostics.push({ row, origin }); + producedPaths.add(row.rawPath); + }; + const pushActionProvenance = (rawPath: string, action: MemoryAction, origin: ProvenanceRowOrigin): void => { + const hash = input.currentHashes.get(rawPath) ?? ''; + pushRow( + { + connectionId: input.job.connectionId, + sourceKey: input.job.sourceKey, + syncId: input.syncId, + rawPath, + rawContentHash: hash, + artifactKind: action.target, + artifactKey: action.key, + targetConnectionId: action.target === 'sl' ? actionTargetConnectionId(action, input.job.connectionId) : null, + artifactContentHash: null, + actionType: actionToType(action), + }, + origin, + ); + }; + + input.stageIndex.workUnits.forEach((wu, unitIndex) => { + wu.actions.forEach((action, actionIndex) => { + for (const rawPath of rawPathsForAction(action, wu.rawFiles)) { + pushActionProvenance(rawPath, action, { + source: 'work_unit_action', + unitKey: wu.unitKey, + unitIndex, + unitRawFiles: wu.rawFiles, + actionIndex, + action, + }); + } + }); + }); + input.reconcileActions.forEach((action, actionIndex) => { + for (const rawPath of action.rawPaths ?? []) { + pushActionProvenance(rawPath, action, { + source: 'reconciliation_action', + actionIndex, + action, + }); + } + }); + (input.stageIndex.artifactResolutions ?? []).forEach((resolution, resolutionIndex) => { + const hash = input.currentHashes.get(resolution.rawPath) ?? ''; + pushRow( + { + connectionId: input.job.connectionId, + sourceKey: input.job.sourceKey, + syncId: input.syncId, + rawPath: resolution.rawPath, + rawContentHash: hash, + artifactKind: resolution.artifactKind, + artifactKey: resolution.artifactKey, + targetConnectionId: null, + artifactContentHash: null, + actionType: resolution.actionType, + }, + { + source: 'artifact_resolution', + resolutionIndex, + resolution, + }, + ); + }); + for (const [rawPath, hash] of input.currentHashes) { + if (producedPaths.has(rawPath)) { + continue; + } + pushRow( + { + connectionId: input.job.connectionId, + sourceKey: input.job.sourceKey, + syncId: input.syncId, + rawPath, + rawContentHash: hash, + artifactKind: null, + artifactKey: null, + targetConnectionId: null, + artifactContentHash: null, + actionType: 'skipped', + }, + { source: 'raw_snapshot_fallback', rawPath }, + ); + } + + return { rows, diagnostics }; + } + + private toReportProvenanceRows(rows: IngestProvenanceInsert[]): IngestReportProvenanceDetail[] { + return rows.map(({ rawPath, artifactKind, artifactKey, actionType, targetConnectionId }) => ({ + rawPath, + artifactKind, + artifactKey, + targetConnectionId: targetConnectionId ?? null, + actionType, + })); + } + + private toReportWorkUnits(stageIndex: StageIndex): IngestReportWorkUnit[] { + return stageIndex.workUnits.map((wu) => ({ + unitKey: wu.unitKey, + rawFiles: wu.rawFiles, + status: wu.status, + reason: wu.reason, + actions: wu.actions, + touchedSlSources: wu.touchedSlSources, + slDisallowed: wu.slDisallowed, + slDisallowedReason: wu.slDisallowedReason, + })); + } + + private provenanceValidationTraceData(input: { + plan: ProvenancePlan; + currentRawPaths: Set; + deletedRawPaths: Set; + }): Record { + const invalidRows = input.plan.diagnostics.filter( + ({ row }) => !input.currentRawPaths.has(row.rawPath) && !input.deletedRawPaths.has(row.rawPath), + ); + return { + rowCount: input.plan.rows.length, + currentRawPathCount: input.currentRawPaths.size, + deletedRawPathCount: input.deletedRawPaths.size, + currentRawPaths: [...input.currentRawPaths].sort(), + deletedRawPaths: [...input.deletedRawPaths].sort(), + invalidRawPaths: [...new Set(invalidRows.map(({ row }) => row.rawPath))].sort(), + invalidRows, + }; + } + private wikiPageKeysFromPaths(paths: string[]): string[] { return [ ...new Set( @@ -673,6 +861,16 @@ export class IngestBundleRunner { let latestWorkUnits: WorkUnitOutcome[] = []; let latestFailedWorkUnits: string[] = []; let latestReconciliationSkipped = true; + let latestReportWorkUnits: IngestReportWorkUnit[] = []; + let latestReconciliationActions: MemoryAction[] = []; + let latestConflictsResolved: StageIndex['conflictsResolved'] = []; + let latestEvictionsApplied: StageIndex['evictionsApplied'] = []; + let latestUnmappedFallbacks: StageIndex['unmappedFallbacks'] = []; + let latestArtifactResolutions: NonNullable = []; + let latestEvictionInputs: string[] = []; + let latestUnresolvedCards: UnresolvedCardInfo[] = []; + let latestReportProvenanceRows: IngestReportProvenanceDetail[] = []; + let activeFailureDetails: Record | undefined; let latestIsolatedDiffSummary: | { enabled: boolean; @@ -1495,6 +1693,7 @@ export class IngestBundleRunner { slDisallowed: o.slDisallowed, slDisallowedReason: o.slDisallowedReason, })); + latestReportWorkUnits = this.toReportWorkUnits(stageIndex); } const carryForwardResult = contextReport && this.deps.contextCandidateCarryforward @@ -1928,6 +2127,48 @@ export class IngestBundleRunner { }, ); + activePhase = 'provenance_validation'; + latestReportWorkUnits = this.toReportWorkUnits(stageIndex); + latestReconciliationActions = reconcileActions; + latestConflictsResolved = stageIndex.conflictsResolved; + latestEvictionsApplied = stageIndex.evictionsApplied; + latestUnmappedFallbacks = stageIndex.unmappedFallbacks; + latestArtifactResolutions = stageIndex.artifactResolutions ?? []; + latestEvictionInputs = eviction?.deletedRawPaths ?? []; + latestUnresolvedCards = unresolvedCards ?? []; + const provenancePlan = this.buildProvenancePlan({ + job, + syncId, + currentHashes, + stageIndex, + reconcileActions, + }); + const provenanceRows = provenancePlan.rows; + const currentRawPaths = new Set(currentHashes.keys()); + const deletedRawPaths = new Set(eviction?.deletedRawPaths ?? []); + const provenanceValidationData = this.provenanceValidationTraceData({ + plan: provenancePlan, + currentRawPaths, + deletedRawPaths, + }); + const reportProvenanceRows = this.toReportProvenanceRows(provenanceRows); + latestReportProvenanceRows = reportProvenanceRows; + activeFailureDetails = provenanceValidationData; + await traceTimed( + runTrace, + 'provenance', + 'provenance_rows_validation', + provenanceValidationData, + async () => { + validateProvenanceRawPaths({ + rows: provenanceRows, + currentRawPaths, + deletedRawPaths, + }); + }, + ); + activeFailureDetails = undefined; + // Stage 6 — squash commit activePhase = 'squash'; const stage6 = ctx?.startPhase(0.04); @@ -2003,89 +2244,10 @@ export class IngestBundleRunner { await stage5?.updateProgress(0.0, 'Recording history'); activePhase = 'provenance'; - // Provenance rows: per-artifact when the WU emitted actions, plus a `skipped` - // fallback for raw files that produced nothing so the next DiffSet still sees - // them. - const provenanceRows: Parameters[0] = []; - const actionToType = (a: MemoryAction): 'source_created' | 'measure_added' | 'wiki_written' => { - if (a.target === 'wiki') { - return 'wiki_written'; - } - // SL action: 'created' → source_created; 'updated' → measure_added (coarse-grained; - // action.detail preserves the finer distinction for the report body). - return a.type === 'created' ? 'source_created' : 'measure_added'; - }; - const producedPaths = new Set(); - const pushActionProvenance = (rawPath: string, action: MemoryAction): void => { - const hash = currentHashes.get(rawPath) ?? ''; - provenanceRows.push({ - connectionId: job.connectionId, - sourceKey: job.sourceKey, - syncId, - rawPath, - rawContentHash: hash, - artifactKind: action.target, - artifactKey: action.key, - targetConnectionId: action.target === 'sl' ? actionTargetConnectionId(action, job.connectionId) : null, - artifactContentHash: null, - actionType: actionToType(action), - }); - producedPaths.add(rawPath); - }; - for (const wu of stageIndex.workUnits) { - for (const action of wu.actions) { - for (const rawPath of rawPathsForAction(action, wu.rawFiles)) { - pushActionProvenance(rawPath, action); - } - } - } - for (const action of reconcileActions) { - for (const rawPath of action.rawPaths ?? []) { - pushActionProvenance(rawPath, action); - } - } - for (const resolution of stageIndex.artifactResolutions ?? []) { - const hash = currentHashes.get(resolution.rawPath) ?? ''; - provenanceRows.push({ - connectionId: job.connectionId, - sourceKey: job.sourceKey, - syncId, - rawPath: resolution.rawPath, - rawContentHash: hash, - artifactKind: resolution.artifactKind, - artifactKey: resolution.artifactKey, - targetConnectionId: null, - artifactContentHash: null, - actionType: resolution.actionType, - }); - producedPaths.add(resolution.rawPath); - } - for (const [rawPath, hash] of currentHashes) { - if (producedPaths.has(rawPath)) { - continue; - } - provenanceRows.push({ - connectionId: job.connectionId, - sourceKey: job.sourceKey, - syncId, - rawPath, - rawContentHash: hash, - artifactKind: null, - artifactKey: null, - targetConnectionId: null, - artifactContentHash: null, - actionType: 'skipped', - }); - } - validateProvenanceRawPaths({ - rows: provenanceRows, - currentRawPaths: new Set(currentHashes.keys()), - deletedRawPaths: new Set(eviction?.deletedRawPaths ?? []), - }); - await runTrace.event('debug', 'provenance', 'provenance_rows_validated', { + await this.deps.provenance.insertMany(provenanceRows); + await runTrace.event('debug', 'provenance', 'provenance_rows_inserted', { rowCount: provenanceRows.length, }); - await this.deps.provenance.insertMany(provenanceRows); memoryFlow?.emit({ type: 'provenance_recorded', rowCount: provenanceRows.length }); await stage5?.updateProgress( 1.0, @@ -2096,15 +2258,6 @@ export class IngestBundleRunner { await stage7?.updateProgress(0.0, 'Wrapping up'); activePhase = 'report'; - const reportProvenanceRows = provenanceRows.map( - ({ rawPath, artifactKind, artifactKey, actionType, targetConnectionId }) => ({ - rawPath, - artifactKind, - artifactKey, - targetConnectionId: targetConnectionId ?? null, - actionType, - }), - ); const reportToolTranscripts = Array.from(transcriptSummaries.values()).map((summary) => ({ unitKey: summary.unitKey, path: summary.path, @@ -2307,30 +2460,34 @@ export class IngestBundleRunner { failure: { phase: activePhase, message: this.errorMessage(error), + ...(activeFailureDetails ? { details: activeFailureDetails } : {}), }, - workUnits: latestWorkUnits.map((wu) => ({ - unitKey: wu.unitKey, - rawFiles: [], - status: wu.status, - reason: wu.reason, - actions: wu.actions, - touchedSlSources: wu.touchedSlSources, - slDisallowed: wu.slDisallowed, - slDisallowedReason: wu.slDisallowedReason, - })), + workUnits: + latestReportWorkUnits.length > 0 + ? latestReportWorkUnits + : latestWorkUnits.map((wu) => ({ + unitKey: wu.unitKey, + rawFiles: [], + status: wu.status, + reason: wu.reason, + actions: wu.actions, + touchedSlSources: wu.touchedSlSources, + slDisallowed: wu.slDisallowed, + slDisallowedReason: wu.slDisallowedReason, + })), failedWorkUnits: latestFailedWorkUnits, reconciliationSkipped: latestReconciliationSkipped, - conflictsResolved: [], - evictionsApplied: [], - unmappedFallbacks: [], - artifactResolutions: [], - evictionInputs: [], - reconciliationActions: [], + conflictsResolved: latestConflictsResolved, + evictionsApplied: latestEvictionsApplied, + unmappedFallbacks: latestUnmappedFallbacks, + artifactResolutions: latestArtifactResolutions, + evictionInputs: latestEvictionInputs, + reconciliationActions: latestReconciliationActions, evictionDecisions: [], - unresolvedCards: [], + unresolvedCards: latestUnresolvedCards, supersededBy: null, overrideOf: null, - provenanceRows: [], + provenanceRows: latestReportProvenanceRows, toolTranscripts: Array.from(transcriptSummaries.values()).map((summary) => ({ unitKey: summary.unitKey, path: summary.path, diff --git a/packages/context/src/ingest/report-snapshot.ts b/packages/context/src/ingest/report-snapshot.ts index 47240d46..b26cf611 100644 --- a/packages/context/src/ingest/report-snapshot.ts +++ b/packages/context/src/ingest/report-snapshot.ts @@ -126,6 +126,7 @@ const sourceFetchReportSchema = z.object({ const ingestReportFailureSchema = z.object({ phase: z.string().min(1), message: z.string().min(1), + details: z.record(z.string(), z.unknown()).optional(), }); export const ingestReportSnapshotSchema = z diff --git a/packages/context/src/ingest/reports.ts b/packages/context/src/ingest/reports.ts index f7eb445f..14d1193e 100644 --- a/packages/context/src/ingest/reports.ts +++ b/packages/context/src/ingest/reports.ts @@ -51,6 +51,7 @@ export interface IngestReportPostProcessorOutcome { export interface IngestReportFailure { phase: string; message: string; + details?: Record; } export interface IngestReportBody {