@kaelio/ktx 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/assets/python/{kaelio_ktx-0.2.0-py3-none-any.whl → kaelio_ktx-0.3.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/admin-reindex.js +10 -17
  4. package/dist/admin-reindex.test.js +1 -1
  5. package/dist/cli-program.test.js +0 -2
  6. package/dist/cli-project.d.ts +18 -0
  7. package/dist/cli-project.js +52 -0
  8. package/dist/cli-project.test.js +149 -0
  9. package/dist/cli-runtime.d.ts +0 -2
  10. package/dist/cli-runtime.js +2 -8
  11. package/dist/commands/runtime-commands.js +2 -2
  12. package/dist/context-build-view.js +1 -1
  13. package/dist/index.test.js +21 -25
  14. package/dist/ingest.js +9 -2
  15. package/dist/ingest.test.js +27 -3
  16. package/dist/managed-local-embeddings.d.ts +0 -2
  17. package/dist/managed-local-embeddings.js +2 -5
  18. package/dist/managed-local-embeddings.test.js +5 -8
  19. package/dist/managed-python-daemon.js +2 -2
  20. package/dist/managed-python-daemon.test.js +1 -1
  21. package/dist/managed-python-http.js +3 -3
  22. package/dist/managed-python-http.test.js +6 -6
  23. package/dist/print-command-tree.js +0 -2
  24. package/dist/public-ingest.d.ts +4 -2
  25. package/dist/public-ingest.js +9 -3
  26. package/dist/release-version.d.ts +1 -5
  27. package/dist/release-version.js +2 -39
  28. package/dist/runtime-requirements.js +1 -1
  29. package/dist/runtime.js +6 -6
  30. package/dist/runtime.test.js +7 -7
  31. package/dist/scan.js +7 -2
  32. package/dist/scan.test.js +1 -1
  33. package/dist/setup-embeddings.js +1 -1
  34. package/dist/setup-embeddings.test.js +2 -2
  35. package/dist/setup-runtime.test.js +1 -1
  36. package/node_modules/@ktx/context/dist/core/git.service.d.ts +1 -0
  37. package/node_modules/@ktx/context/dist/core/git.service.js +12 -0
  38. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/historic-sql.adapter.d.ts +2 -1
  39. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/historic-sql.adapter.js +18 -0
  40. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/local-ingest-acceptance.test.js +6 -6
  41. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/projection.d.ts +5 -0
  42. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/projection.js +48 -0
  43. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/projection.test.js +83 -0
  44. package/node_modules/@ktx/context/dist/ingest/adapters/live-database/daemon-introspection.js +4 -1
  45. package/node_modules/@ktx/context/dist/ingest/adapters/live-database/daemon-introspection.test.js +32 -0
  46. package/node_modules/@ktx/context/dist/ingest/finalization-scope.d.ts +22 -0
  47. package/node_modules/@ktx/context/dist/ingest/finalization-scope.js +95 -0
  48. package/node_modules/@ktx/context/dist/ingest/finalization-scope.test.d.ts +1 -0
  49. package/node_modules/@ktx/context/dist/ingest/finalization-scope.test.js +114 -0
  50. package/node_modules/@ktx/context/dist/ingest/index.d.ts +1 -2
  51. package/node_modules/@ktx/context/dist/ingest/index.js +0 -1
  52. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.d.ts +2 -0
  53. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.isolated-diff.test.js +166 -0
  54. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.js +235 -45
  55. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.test.js +193 -38
  56. package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +22 -3
  57. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +0 -4
  58. package/node_modules/@ktx/context/dist/ingest/local-ingest.js +0 -7
  59. package/node_modules/@ktx/context/dist/ingest/local-stage-ingest.js +15 -5
  60. package/node_modules/@ktx/context/dist/ingest/local-stage-ingest.test.js +29 -0
  61. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +2 -2
  62. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.js +1 -1
  63. package/node_modules/@ktx/context/dist/ingest/memory-flow/types.d.ts +1 -1
  64. package/node_modules/@ktx/context/dist/ingest/ports.d.ts +1 -20
  65. package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +71 -0
  66. package/node_modules/@ktx/context/dist/ingest/report-snapshot.js +27 -0
  67. package/node_modules/@ktx/context/dist/ingest/reports.d.ts +23 -5
  68. package/node_modules/@ktx/context/dist/ingest/reports.js +7 -24
  69. package/node_modules/@ktx/context/dist/ingest/types.d.ts +33 -0
  70. package/node_modules/@ktx/context/dist/llm/index.d.ts +1 -1
  71. package/node_modules/@ktx/context/dist/llm/index.js +1 -1
  72. package/node_modules/@ktx/context/dist/llm/local-config.d.ts +0 -1
  73. package/node_modules/@ktx/context/dist/llm/local-config.js +2 -12
  74. package/node_modules/@ktx/context/dist/llm/local-config.test.js +2 -23
  75. package/node_modules/@ktx/context/dist/package-exports.test.js +2 -2
  76. package/node_modules/@ktx/context/dist/project/config.d.ts +16 -0
  77. package/node_modules/@ktx/context/dist/project/driver-schemas.d.ts +8 -0
  78. package/node_modules/@ktx/context/dist/project/driver-schemas.js +4 -0
  79. package/node_modules/@ktx/context/dist/scan/enabled-tables.d.ts +3 -0
  80. package/node_modules/@ktx/context/dist/scan/enabled-tables.js +15 -0
  81. package/node_modules/@ktx/context/dist/scan/local-scan.d.ts +2 -4
  82. package/node_modules/@ktx/context/dist/scan/local-scan.js +2 -15
  83. package/package.json +1 -1
  84. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.d.ts +0 -4
  85. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.js +0 -38
  86. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.test.js +0 -63
  87. /package/{node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.test.d.ts → dist/cli-project.test.d.ts} +0 -0
@@ -104,6 +104,24 @@ function makeWikiService(root) {
104
104
  content: content.trim(),
105
105
  };
106
106
  }),
107
+ writePage: vi.fn(async (_scope, _scopeId, key, frontmatter, content) => {
108
+ await mkdir(join(root, 'wiki/global'), { recursive: true });
109
+ const refs = (frontmatter.refs ?? []).map((ref) => ` - ${ref}`).join('\n');
110
+ const slRefs = (frontmatter.sl_refs ?? []).map((ref) => ` - ${ref}`).join('\n');
111
+ await writeFile(join(root, 'wiki/global', `${key}.md`), [
112
+ '---',
113
+ `summary: ${frontmatter.summary ?? key}`,
114
+ `usage_mode: ${frontmatter.usage_mode ?? 'auto'}`,
115
+ 'refs:',
116
+ refs,
117
+ 'sl_refs:',
118
+ slRefs,
119
+ '---',
120
+ '',
121
+ content,
122
+ '',
123
+ ].join('\n'));
124
+ }),
107
125
  syncFromCommit: vi.fn(),
108
126
  };
109
127
  }
@@ -1758,4 +1776,152 @@ describe('IngestBundleRunner isolated diff path', () => {
1758
1776
  await rm(runtime.homeDir, { recursive: true, force: true });
1759
1777
  }
1760
1778
  });
1779
+ it('runs finalization before wiki sl-ref repair and final gates', async () => {
1780
+ const runtime = await makeRealGitRuntime();
1781
+ try {
1782
+ const { deps, adapter } = makeDeps(runtime);
1783
+ adapter.chunk.mockResolvedValue({
1784
+ workUnits: [{ unitKey: 'wiki-page', rawFiles: ['cards/source.json'], peerFileIndex: [], dependencyPaths: [] }],
1785
+ });
1786
+ adapter.finalize = vi.fn(async ({ workdir }) => {
1787
+ await mkdir(join(workdir, 'semantic-layer/warehouse'), { recursive: true });
1788
+ await mkdir(join(workdir, 'wiki/global'), { recursive: true });
1789
+ await writeFile(join(workdir, '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');
1790
+ await writeFile(join(workdir, 'wiki/global/finalized-accounts.md'), '---\nsummary: Finalized accounts\nusage_mode: auto\nsl_refs:\n - mart_account_segments\n - missing_source\n---\n\nAccounts use `mart_account_segments.total_contract_arr`.\n');
1791
+ return {
1792
+ warnings: [],
1793
+ errors: [],
1794
+ touchedSources: [{ connectionId: 'warehouse', sourceName: 'mart_account_segments' }],
1795
+ changedWikiPageKeys: ['finalized-accounts'],
1796
+ actions: [
1797
+ {
1798
+ target: 'sl',
1799
+ type: 'created',
1800
+ key: 'mart_account_segments',
1801
+ detail: 'Finalized accounts',
1802
+ targetConnectionId: 'warehouse',
1803
+ rawPaths: ['cards/source.json'],
1804
+ },
1805
+ {
1806
+ target: 'wiki',
1807
+ type: 'created',
1808
+ key: 'finalized-accounts',
1809
+ detail: 'Finalized wiki',
1810
+ rawPaths: ['cards/source.json'],
1811
+ },
1812
+ ],
1813
+ };
1814
+ });
1815
+ deps.agentRunner.runLoop = vi.fn(async () => ({ stopReason: 'natural' }));
1816
+ const runner = new IngestBundleRunner(deps);
1817
+ await mockStageRawFiles(runner, runtime, [['cards/source.json', 'h1']]);
1818
+ await runner.run({
1819
+ jobId: 'job-finalization',
1820
+ connectionId: 'warehouse',
1821
+ sourceKey: 'metabase',
1822
+ trigger: 'upload',
1823
+ bundleRef: { kind: 'upload', uploadId: 'upload' },
1824
+ });
1825
+ const trace = await readFile(join(runtime.configDir, '.ktx/ingest-traces/job-finalization/trace.jsonl'), 'utf-8');
1826
+ expect(trace.indexOf('finalization_committed')).toBeLessThan(trace.indexOf('wiki_sl_refs_repaired'));
1827
+ expect(trace.indexOf('wiki_sl_refs_repaired')).toBeLessThan(trace.indexOf('final_artifact_gates'));
1828
+ await expect(readFile(join(runtime.configDir, 'wiki/global/finalized-accounts.md'), 'utf-8')).resolves.toContain('sl_refs:\n - mart_account_segments');
1829
+ }
1830
+ finally {
1831
+ await rm(runtime.homeDir, { recursive: true, force: true });
1832
+ }
1833
+ });
1834
+ it('fails when finalization edits a path already changed earlier in the run', async () => {
1835
+ const runtime = await makeRealGitRuntime();
1836
+ try {
1837
+ const { deps, adapter } = makeDeps(runtime);
1838
+ adapter.chunk.mockResolvedValue({
1839
+ workUnits: [{ unitKey: 'wiki-page', rawFiles: ['cards/source.json'], peerFileIndex: [], dependencyPaths: [] }],
1840
+ });
1841
+ let currentSession = null;
1842
+ deps.toolsetFactory.createIngestWuToolset = vi.fn((toolSession) => {
1843
+ currentSession = toolSession;
1844
+ return { toRuntimeTools: vi.fn(() => ({})) };
1845
+ });
1846
+ deps.agentRunner.runLoop = vi.fn(async () => {
1847
+ const root = rootOfConfig(currentSession.configService, runtime.configDir);
1848
+ await mkdir(join(root, 'wiki/global'), { recursive: true });
1849
+ await writeFile(join(root, 'wiki/global/orders.md'), '---\nsummary: Orders\nusage_mode: auto\n---\n\nWU body\n');
1850
+ currentSession.actions.push({
1851
+ target: 'wiki',
1852
+ type: 'created',
1853
+ key: 'orders',
1854
+ detail: 'WU orders',
1855
+ rawPaths: ['cards/source.json'],
1856
+ });
1857
+ await currentSession.gitService.commitFiles(['wiki/global/orders.md'], 'wu orders', 'KTX Test', 'system@ktx.local');
1858
+ return { stopReason: 'natural' };
1859
+ });
1860
+ adapter.finalize = vi.fn(async ({ workdir }) => {
1861
+ await writeFile(join(workdir, 'wiki/global/orders.md'), '---\nsummary: Orders\nusage_mode: auto\n---\n\nFinalized body\n');
1862
+ return {
1863
+ warnings: [],
1864
+ errors: [],
1865
+ touchedSources: [],
1866
+ changedWikiPageKeys: ['orders'],
1867
+ actions: [{ target: 'wiki', type: 'updated', key: 'orders', detail: 'Conflicting finalization' }],
1868
+ };
1869
+ });
1870
+ const runner = new IngestBundleRunner(deps);
1871
+ await mockStageRawFiles(runner, runtime, [['cards/source.json', 'h1']]);
1872
+ await expect(runner.run({
1873
+ jobId: 'job-finalization-overlap',
1874
+ connectionId: 'warehouse',
1875
+ sourceKey: 'metabase',
1876
+ trigger: 'upload',
1877
+ bundleRef: { kind: 'upload', uploadId: 'upload' },
1878
+ })).rejects.toThrow(/finalization modified path\(s\) already changed earlier in this run: wiki\/global\/orders\.md/);
1879
+ }
1880
+ finally {
1881
+ await rm(runtime.homeDir, { recursive: true, force: true });
1882
+ }
1883
+ });
1884
+ it('rejects finalization writes to unauthorized semantic-layer targets', async () => {
1885
+ const runtime = await makeRealGitRuntime();
1886
+ try {
1887
+ const { deps, adapter } = makeDeps(runtime);
1888
+ adapter.chunk.mockResolvedValue({ workUnits: [] });
1889
+ adapter.finalize = vi.fn(async ({ workdir }) => {
1890
+ await mkdir(join(workdir, 'semantic-layer/other-warehouse'), { recursive: true });
1891
+ await writeFile(join(workdir, 'semantic-layer/other-warehouse/orders.yaml'), 'name: orders\ngrain: [order_id]\ncolumns: [{name: order_id, type: string}]\njoins: []\nmeasures: []\n');
1892
+ return {
1893
+ warnings: [],
1894
+ errors: [],
1895
+ touchedSources: [{ connectionId: 'other-warehouse', sourceName: 'orders' }],
1896
+ changedWikiPageKeys: [],
1897
+ actions: [
1898
+ {
1899
+ target: 'sl',
1900
+ type: 'created',
1901
+ key: 'orders',
1902
+ targetConnectionId: 'other-warehouse',
1903
+ detail: 'Forbidden target',
1904
+ rawPaths: ['cards/source.json'],
1905
+ },
1906
+ ],
1907
+ };
1908
+ });
1909
+ const runner = new IngestBundleRunner(deps);
1910
+ await mockStageRawFiles(runner, runtime, [['cards/source.json', 'h1']]);
1911
+ await expect(runner.run({
1912
+ jobId: 'job-finalization-target-policy',
1913
+ connectionId: 'warehouse',
1914
+ sourceKey: 'metabase',
1915
+ trigger: 'upload',
1916
+ bundleRef: { kind: 'upload', uploadId: 'upload' },
1917
+ })).rejects.toThrow(/semantic-layer target connection not allowed/);
1918
+ const trace = await readFile(join(runtime.configDir, '.ktx/ingest-traces/job-finalization-target-policy/trace.jsonl'), 'utf-8');
1919
+ expect(trace).toContain('finalization_committed');
1920
+ expect(trace).toContain('semantic_layer_target_policy');
1921
+ expect(trace).toContain('ingest_failed');
1922
+ }
1923
+ finally {
1924
+ await rm(runtime.homeDir, { recursive: true, force: true });
1925
+ }
1926
+ });
1761
1927
  });
@@ -11,13 +11,14 @@ import { NOTION_DEFAULT_MAX_KNOWLEDGE_CREATES_PER_RUN } from './adapters/notion/
11
11
  import { validateFinalIngestArtifacts, validateProvenanceRawPaths } from './artifact-gates.js';
12
12
  import { selectRelevantCanonicalPins } from './canonical-pins.js';
13
13
  import { finalGateRepairPaths, repairFinalGateFailure } from './final-gate-repair.js';
14
+ import { compareFinalizationDeclarations, deriveFinalizationTouchedSources, deriveFinalizationWikiPageKeys, } from './finalization-scope.js';
14
15
  import { FileIngestTraceWriter, ingestTracePathForJob, traceTimed } from './ingest-trace.js';
15
16
  import { integrateWorkUnitPatch } from './isolated-diff/patch-integrator.js';
16
17
  import { resolveTextualConflict } from './isolated-diff/textual-conflict-resolver.js';
17
18
  import { runIsolatedWorkUnit } from './isolated-diff/work-unit-executor.js';
18
19
  import { sanitizeMemoryFlowError } from './memory-flow/live-buffer.js';
19
20
  import { buildSyncId, rawSourcesDirForSync } from './raw-sources-paths.js';
20
- import { buildStageIndexFromReportBody, postProcessorSavedMemoryCounts, } from './reports.js';
21
+ import { buildStageIndexFromReportBody, } from './reports.js';
21
22
  import { buildReconcileSystemPrompt, buildReconcileToolSet, buildReconcileUserPrompt, } from './stages/build-reconcile-context.js';
22
23
  import { buildWuSystemPrompt, buildWuToolSet, buildWuUserPrompt } from './stages/build-wu-context.js';
23
24
  import { stageRawFilesStage1 } from './stages/stage-1-stage-raw-files.js';
@@ -286,6 +287,15 @@ export class IngestBundleRunner {
286
287
  }
287
288
  return false;
288
289
  }
290
+ async loadSourcesByConnection(workdir, connectionIds) {
291
+ const service = this.deps.semanticLayerService.forWorktree(workdir);
292
+ const result = new Map();
293
+ for (const connectionId of connectionIds) {
294
+ const { sources } = await service.loadAllSources(connectionId);
295
+ result.set(connectionId, sources);
296
+ }
297
+ return result;
298
+ }
289
299
  resolveContextCuratorBudget(bundleRef, stageIndex) {
290
300
  const rawConfig = bundleRef.kind === 'scheduled_pull' && bundleRef.config && typeof bundleRef.config === 'object'
291
301
  ? bundleRef.config
@@ -374,6 +384,15 @@ export class IngestBundleRunner {
374
384
  });
375
385
  }
376
386
  });
387
+ input.finalizationActions.forEach((action, actionIndex) => {
388
+ for (const rawPath of action.rawPaths ?? []) {
389
+ pushActionProvenance(rawPath, action, {
390
+ source: 'finalization_action',
391
+ actionIndex,
392
+ action,
393
+ });
394
+ }
395
+ });
377
396
  (input.stageIndex.artifactResolutions ?? []).forEach((resolution, resolutionIndex) => {
378
397
  const hash = input.currentHashes.get(resolution.rawPath) ?? '';
379
398
  pushRow({
@@ -412,6 +431,29 @@ export class IngestBundleRunner {
412
431
  }
413
432
  return { rows, diagnostics };
414
433
  }
434
+ partitionFinalizationActionsForProvenance(input) {
435
+ const defensible = new Set([
436
+ ...input.currentRawPaths,
437
+ ...input.currentEvictionRawPaths,
438
+ ...input.overrideEvictionRawPaths,
439
+ ]);
440
+ const actions = [];
441
+ const exclusions = [];
442
+ for (const action of input.actions) {
443
+ const rawPaths = action.rawPaths ?? [];
444
+ if (rawPaths.length === 0) {
445
+ exclusions.push({ action, reason: 'missing_raw_paths' });
446
+ continue;
447
+ }
448
+ const invalidRawPaths = rawPaths.filter((rawPath) => !defensible.has(rawPath)).sort();
449
+ if (invalidRawPaths.length > 0) {
450
+ exclusions.push({ action, reason: 'raw_path_not_defensible', invalidRawPaths });
451
+ continue;
452
+ }
453
+ actions.push(action);
454
+ }
455
+ return { actions, exclusions };
456
+ }
415
457
  toReportProvenanceRows(rows) {
416
458
  return rows.map(({ rawPath, artifactKind, artifactKey, actionType, targetConnectionId }) => ({
417
459
  rawPath,
@@ -697,6 +739,7 @@ export class IngestBundleRunner {
697
739
  let latestEvictionInputs = [];
698
740
  let latestUnresolvedCards = [];
699
741
  let latestReportProvenanceRows = [];
742
+ let latestFinalizationOutcome;
700
743
  let activeFailureDetails;
701
744
  let latestIsolatedDiffSummary;
702
745
  await trace.event('info', 'run', 'ingest_started', {
@@ -848,7 +891,7 @@ export class IngestBundleRunner {
848
891
  let unresolvedCards;
849
892
  let sourceContextReport;
850
893
  let parseArtifacts;
851
- let postProcessorOutcome;
894
+ let finalizationOutcome;
852
895
  let wikiSlRefRepairResult = null;
853
896
  let reconcileNotes = [];
854
897
  let triageResult = null;
@@ -1491,55 +1534,187 @@ export class IngestBundleRunner {
1491
1534
  artifactResolutionCount: stageIndex.artifactResolutions?.length ?? 0,
1492
1535
  });
1493
1536
  await stage4?.updateProgress(1.0, reconcileOutcome.skipped ? 'No reconciliation needed' : 'Reconciled');
1494
- const postProcessor = this.deps.postProcessors?.[job.sourceKey];
1495
- activePhase = 'post_processor';
1496
- if (postProcessor) {
1497
- const stagePostProcessor = ctx?.startPhase(0.04);
1498
- emitStageProgress('post_processor', 87, 'Running deterministic imports');
1499
- await stagePostProcessor?.updateProgress(0.0, 'Running deterministic imports');
1500
- try {
1501
- const result = await traceTimed(runTrace, 'post_processor', 'post_processor', { sourceKey: job.sourceKey }, () => postProcessor.run({
1502
- connectionId: job.connectionId,
1503
- sourceKey: job.sourceKey,
1504
- syncId,
1505
- jobId: job.jobId,
1506
- runId: createdRunRow.id,
1507
- workdir: sessionWorktree.workdir,
1508
- parseArtifacts,
1509
- }));
1510
- postProcessorOutcome = {
1537
+ const preFinalizationSha = await sessionWorktree.git.revParseHead();
1538
+ const preFinalizationSourcesByConnection = await this.loadSourcesByConnection(sessionWorktree.workdir, slConnectionIds);
1539
+ let finalizationActions = [];
1540
+ let finalizationTouchedPaths = [];
1541
+ let finalizationTouchedSources = [];
1542
+ let finalizationChangedWikiPageKeys = [];
1543
+ let finalizationSha = null;
1544
+ activePhase = 'finalization';
1545
+ if (adapter.finalize) {
1546
+ const stageFinalization = ctx?.startPhase(0.04);
1547
+ emitStageProgress('finalization', 87, 'Running deterministic finalization');
1548
+ await stageFinalization?.updateProgress(0.0, 'Running deterministic finalization');
1549
+ await runTrace.event('debug', 'finalization', 'finalization_started', { sourceKey: job.sourceKey });
1550
+ const result = await adapter.finalize({
1551
+ connectionId: job.connectionId,
1552
+ sourceKey: job.sourceKey,
1553
+ syncId,
1554
+ jobId: job.jobId,
1555
+ runId: createdRunRow.id,
1556
+ stagedDir,
1557
+ workdir: sessionWorktree.workdir,
1558
+ ...(overrideReport ? {} : { parseArtifacts }),
1559
+ stageIndex,
1560
+ workUnitOutcomes,
1561
+ reconciliationActions: reconcileActions,
1562
+ ...(overrideReport
1563
+ ? {
1564
+ overrideReplay: {
1565
+ priorJobId: overrideReport.jobId,
1566
+ priorRunId: overrideReport.runId,
1567
+ priorSyncId: overrideReport.body.syncId,
1568
+ evictionRawPaths: overrideReport.body.evictionInputs,
1569
+ },
1570
+ }
1571
+ : {}),
1572
+ });
1573
+ if (result.errors.length > 0) {
1574
+ finalizationOutcome = {
1511
1575
  sourceKey: job.sourceKey,
1512
- status: result.errors.length > 0 && result.touchedSources.length === 0 ? 'failed' : 'success',
1576
+ status: 'failed',
1577
+ commitSha: null,
1578
+ touchedPaths: [],
1579
+ declaredTouchedSources: result.touchedSources,
1580
+ derivedTouchedSources: [],
1581
+ declaredChangedWikiPageKeys: result.changedWikiPageKeys,
1582
+ derivedChangedWikiPageKeys: [],
1583
+ mismatches: [],
1513
1584
  result: result.result,
1514
1585
  errors: result.errors,
1515
1586
  warnings: result.warnings,
1516
- touchedSources: result.touchedSources,
1587
+ actions: result.actions ?? [],
1588
+ provenanceExclusions: [],
1517
1589
  };
1518
- emitStageProgress('post_processor', 88, 'Deterministic imports complete');
1519
- await stagePostProcessor?.updateProgress(1.0, 'Deterministic imports complete');
1590
+ latestFinalizationOutcome = finalizationOutcome;
1591
+ await runTrace.event('error', 'finalization', 'finalization_failed', {
1592
+ sourceKey: job.sourceKey,
1593
+ errors: result.errors,
1594
+ warnings: result.warnings,
1595
+ });
1596
+ throw new Error(`deterministic finalization failed: ${result.errors.join('; ')}`);
1520
1597
  }
1521
- catch (error) {
1522
- postProcessorOutcome = {
1598
+ const changedBeforeFinalization = new Set([
1599
+ ...projectionTouchedPaths,
1600
+ ...workUnitOutcomes.flatMap((outcome) => outcome.patchTouchedPaths ?? []),
1601
+ ...(preReconciliationSha && preFinalizationSha !== preReconciliationSha
1602
+ ? (await sessionWorktree.git.diffNameStatus(preReconciliationSha, preFinalizationSha)).map((entry) => entry.path)
1603
+ : []),
1604
+ ]);
1605
+ finalizationTouchedPaths = await sessionWorktree.git.changedPaths();
1606
+ const overlapping = finalizationTouchedPaths.filter((path) => changedBeforeFinalization.has(path));
1607
+ if (overlapping.length > 0) {
1608
+ await runTrace.event('error', 'finalization', 'finalization_failed', {
1609
+ sourceKey: job.sourceKey,
1610
+ reason: 'path_overlap',
1611
+ overlappingPaths: overlapping.sort(),
1612
+ });
1613
+ throw new Error(`finalization modified path(s) already changed earlier in this run: ${overlapping.sort().join(', ')}`);
1614
+ }
1615
+ const finalizationCommit = finalizationTouchedPaths.length > 0
1616
+ ? await sessionWorktree.git.commitFiles(finalizationTouchedPaths, `ingest(${job.sourceKey}): deterministic finalization syncId=${syncId}`, this.deps.storage.systemGitAuthor.name, this.deps.storage.systemGitAuthor.email)
1617
+ : await sessionWorktree.git.commitStaged(`ingest(${job.sourceKey}): deterministic finalization syncId=${syncId}`, this.deps.storage.systemGitAuthor.name, this.deps.storage.systemGitAuthor.email);
1618
+ finalizationSha = finalizationCommit.created ? finalizationCommit.commitHash : null;
1619
+ const postFinalizationSha = await sessionWorktree.git.revParseHead();
1620
+ finalizationTouchedPaths =
1621
+ preFinalizationSha !== postFinalizationSha
1622
+ ? (await sessionWorktree.git.diffNameStatus(preFinalizationSha, postFinalizationSha)).map((entry) => entry.path)
1623
+ : [];
1624
+ const changedConnectionIds = [
1625
+ ...new Set([
1626
+ ...slConnectionIds,
1627
+ ...finalizationTouchedPaths
1628
+ .filter((path) => path.startsWith('semantic-layer/'))
1629
+ .map((path) => path.split('/')[1])
1630
+ .filter((connectionId) => Boolean(connectionId)),
1631
+ ]),
1632
+ ].sort();
1633
+ const postFinalizationSourcesByConnection = await this.loadSourcesByConnection(sessionWorktree.workdir, changedConnectionIds);
1634
+ const scope = await deriveFinalizationTouchedSources({
1635
+ changedPaths: finalizationTouchedPaths,
1636
+ beforeSourcesByConnection: preFinalizationSourcesByConnection,
1637
+ afterSourcesByConnection: postFinalizationSourcesByConnection,
1638
+ });
1639
+ if (scope.unresolvedPaths.length > 0) {
1640
+ await runTrace.event('error', 'finalization', 'finalization_failed', {
1641
+ sourceKey: job.sourceKey,
1642
+ reason: 'unresolved_semantic_layer_paths',
1643
+ unresolvedPaths: scope.unresolvedPaths,
1644
+ });
1645
+ throw new Error(`could not resolve finalization semantic-layer path(s): ${scope.unresolvedPaths.join(', ')}`);
1646
+ }
1647
+ finalizationTouchedSources = scope.touchedSources;
1648
+ finalizationChangedWikiPageKeys = deriveFinalizationWikiPageKeys(finalizationTouchedPaths);
1649
+ const mismatches = compareFinalizationDeclarations({
1650
+ declaredTouchedSources: result.touchedSources,
1651
+ derivedTouchedSources: finalizationTouchedSources,
1652
+ declaredChangedWikiPageKeys: result.changedWikiPageKeys,
1653
+ derivedChangedWikiPageKeys: finalizationChangedWikiPageKeys,
1654
+ });
1655
+ if (mismatches.length > 0) {
1656
+ finalizationOutcome = {
1523
1657
  sourceKey: job.sourceKey,
1524
1658
  status: 'failed',
1525
- errors: [error instanceof Error ? error.message : String(error)],
1526
- warnings: [],
1527
- touchedSources: [],
1659
+ commitSha: finalizationSha,
1660
+ touchedPaths: finalizationTouchedPaths,
1661
+ declaredTouchedSources: result.touchedSources,
1662
+ derivedTouchedSources: finalizationTouchedSources,
1663
+ declaredChangedWikiPageKeys: result.changedWikiPageKeys,
1664
+ derivedChangedWikiPageKeys: finalizationChangedWikiPageKeys,
1665
+ mismatches,
1666
+ result: result.result,
1667
+ errors: ['finalization touched artifact declaration mismatch'],
1668
+ warnings: result.warnings,
1669
+ actions: result.actions ?? [],
1670
+ provenanceExclusions: [],
1528
1671
  };
1529
- await this.deps.runs.markFailed(runRow.id);
1530
- throw error;
1672
+ latestFinalizationOutcome = finalizationOutcome;
1673
+ await runTrace.event('error', 'finalization', 'finalization_failed', {
1674
+ sourceKey: job.sourceKey,
1675
+ reason: 'declaration_mismatch',
1676
+ mismatches,
1677
+ });
1678
+ throw new Error(`finalization touched artifact declaration mismatch: ${mismatches
1679
+ .map((mismatch) => `${mismatch.direction}:${mismatch.artifactKind}:${mismatch.key}`)
1680
+ .join(', ')}`);
1531
1681
  }
1682
+ finalizationActions = result.actions ?? [];
1683
+ finalizationOutcome = {
1684
+ sourceKey: job.sourceKey,
1685
+ status: 'success',
1686
+ commitSha: finalizationSha,
1687
+ touchedPaths: finalizationTouchedPaths,
1688
+ declaredTouchedSources: result.touchedSources,
1689
+ derivedTouchedSources: finalizationTouchedSources,
1690
+ declaredChangedWikiPageKeys: result.changedWikiPageKeys,
1691
+ derivedChangedWikiPageKeys: finalizationChangedWikiPageKeys,
1692
+ mismatches,
1693
+ result: result.result,
1694
+ errors: [],
1695
+ warnings: result.warnings,
1696
+ actions: finalizationActions,
1697
+ provenanceExclusions: [],
1698
+ };
1699
+ latestFinalizationOutcome = finalizationOutcome;
1700
+ emitStageProgress('finalization', 88, 'Deterministic finalization complete');
1701
+ await stageFinalization?.updateProgress(1.0, 'Deterministic finalization complete');
1702
+ await runTrace.event('debug', 'finalization', 'finalization_committed', {
1703
+ sourceKey: job.sourceKey,
1704
+ commitSha: finalizationSha,
1705
+ touchedPaths: finalizationTouchedPaths,
1706
+ touchedSources: finalizationTouchedSources,
1707
+ changedWikiPageKeys: finalizationChangedWikiPageKeys,
1708
+ warnings: result.warnings,
1709
+ });
1710
+ }
1711
+ else {
1712
+ await runTrace.event('debug', 'finalization', 'finalization_skipped', { sourceKey: job.sourceKey });
1532
1713
  }
1533
- await runTrace.event('debug', 'post_processor', 'post_processor_finished', {
1534
- sourceKey: job.sourceKey,
1535
- status: postProcessorOutcome?.status ?? 'skipped',
1536
- touchedSources: postProcessorOutcome?.touchedSources ?? [],
1537
- warnings: postProcessorOutcome?.warnings ?? [],
1538
- });
1539
1714
  const repairConnectionIds = [
1540
1715
  ...new Set([
1541
1716
  ...slConnectionIds,
1542
- ...(postProcessorOutcome?.touchedSources ?? []).map((source) => source.connectionId),
1717
+ ...finalizationTouchedSources.map((source) => source.connectionId),
1543
1718
  ]),
1544
1719
  ].sort();
1545
1720
  activePhase = 'wiki_sl_ref_repair';
@@ -1566,6 +1741,7 @@ export class IngestBundleRunner {
1566
1741
  .flatMap((outcome) => outcome.patchTouchedPaths ?? [])
1567
1742
  .flatMap((path) => this.wikiPageKeysFromPaths([path])),
1568
1743
  ...this.wikiPageKeysFromActions(reconcileActions),
1744
+ ...finalizationChangedWikiPageKeys,
1569
1745
  ...postReconciliationPaths.flatMap((path) => this.wikiPageKeysFromPaths([path])),
1570
1746
  ...wikiSlRefRepairResult.repairs.filter((repair) => repair.scope === 'GLOBAL').map((repair) => repair.pageKey),
1571
1747
  ]);
@@ -1574,7 +1750,7 @@ export class IngestBundleRunner {
1574
1750
  ...workUnitOutcomes.flatMap((outcome) => outcome.touchedSlSources),
1575
1751
  ...this.touchedSlSourcesFromActions(reconcileActions, job.connectionId),
1576
1752
  ...this.touchedSlSourcesFromPaths(postReconciliationPaths),
1577
- ...(postProcessorOutcome?.touchedSources ?? []),
1753
+ ...finalizationTouchedSources,
1578
1754
  ]);
1579
1755
  const finalWikiGateScope = await this.wikiPageKeysForFinalGates({
1580
1756
  wikiService: this.deps.wikiService.forWorktree(sessionWorktree.workdir),
@@ -1587,7 +1763,7 @@ export class IngestBundleRunner {
1587
1763
  ...projectionTouchedPaths,
1588
1764
  ...workUnitOutcomes.flatMap((outcome) => outcome.patchTouchedPaths ?? []),
1589
1765
  ...postReconciliationPaths,
1590
- ...(postProcessorOutcome?.touchedSources ?? []).map((source) => `semantic-layer/${source.connectionId}/${source.sourceName}.yaml`),
1766
+ ...finalizationTouchedPaths,
1591
1767
  ];
1592
1768
  const targetPolicyTraceData = {
1593
1769
  allowedTargetConnectionIds: slConnectionIds,
@@ -1715,12 +1891,23 @@ export class IngestBundleRunner {
1715
1891
  latestArtifactResolutions = stageIndex.artifactResolutions ?? [];
1716
1892
  latestEvictionInputs = eviction?.deletedRawPaths ?? [];
1717
1893
  latestUnresolvedCards = unresolvedCards ?? [];
1894
+ const finalizationProvenance = this.partitionFinalizationActionsForProvenance({
1895
+ actions: finalizationActions,
1896
+ currentRawPaths: new Set(currentHashes.keys()),
1897
+ currentEvictionRawPaths: new Set(stageIndex.evictionsApplied.map((entry) => entry.rawPath)),
1898
+ overrideEvictionRawPaths: new Set(overrideReport?.body.evictionInputs ?? []),
1899
+ });
1900
+ if (finalizationOutcome) {
1901
+ finalizationOutcome.provenanceExclusions = finalizationProvenance.exclusions;
1902
+ latestFinalizationOutcome = finalizationOutcome;
1903
+ }
1718
1904
  const provenancePlan = this.buildProvenancePlan({
1719
1905
  job,
1720
1906
  syncId,
1721
1907
  currentHashes,
1722
1908
  stageIndex,
1723
1909
  reconcileActions,
1910
+ finalizationActions: finalizationProvenance.actions,
1724
1911
  });
1725
1912
  const provenanceRows = provenancePlan.rows;
1726
1913
  const currentRawPaths = new Set(currentHashes.keys());
@@ -1769,13 +1956,15 @@ export class IngestBundleRunner {
1769
1956
  commitSha,
1770
1957
  touchedPaths: mergeResult.touchedPaths,
1771
1958
  });
1772
- const memoryFlowSavedActions = stageIndex.workUnits.flatMap((wu) => wu.actions).concat(reconcileActions);
1773
- const postProcessorMemoryCounts = postProcessorSavedMemoryCounts(postProcessorOutcome);
1959
+ const memoryFlowSavedActions = stageIndex.workUnits
1960
+ .flatMap((wu) => wu.actions)
1961
+ .concat(reconcileActions)
1962
+ .concat(finalizationActions);
1774
1963
  memoryFlow?.emit({
1775
1964
  type: 'saved',
1776
1965
  commitSha,
1777
- wikiCount: countMemoryFlowActions(memoryFlowSavedActions, 'wiki') + postProcessorMemoryCounts.wikiCount,
1778
- slCount: countMemoryFlowActions(memoryFlowSavedActions, 'sl') + postProcessorMemoryCounts.slCount,
1966
+ wikiCount: countMemoryFlowActions(memoryFlowSavedActions, 'wiki'),
1967
+ slCount: countMemoryFlowActions(memoryFlowSavedActions, 'sl'),
1779
1968
  });
1780
1969
  await stage6?.updateProgress(1.0, commitSha ? `Saved changes (${commitSha.slice(0, 8)})` : 'No changes to save');
1781
1970
  // Sync the shared `knowledge` index from the squashed diff in a single
@@ -1792,7 +1981,7 @@ export class IngestBundleRunner {
1792
1981
  ...new Set(memoryFlowSavedActions
1793
1982
  .filter((action) => action.target === 'sl')
1794
1983
  .map((action) => actionTargetConnectionId(action, job.connectionId))
1795
- .concat((postProcessorOutcome?.touchedSources ?? []).map((source) => source.connectionId))),
1984
+ .concat(finalizationTouchedSources.map((source) => source.connectionId))),
1796
1985
  ].sort();
1797
1986
  for (const connectionId of touchedConnections) {
1798
1987
  try {
@@ -1873,7 +2062,7 @@ export class IngestBundleRunner {
1873
2062
  overrideOf: overrideReport?.jobId ?? null,
1874
2063
  provenanceRows: reportProvenanceRows,
1875
2064
  toolTranscripts: reportToolTranscripts,
1876
- postProcessor: postProcessorOutcome,
2065
+ finalization: finalizationOutcome,
1877
2066
  wikiSlRefRepairs: wikiSlRefRepairResult.repairs,
1878
2067
  wikiSlRefRepairWarnings: wikiSlRefRepairResult.warnings,
1879
2068
  ...(reportMemoryFlow ? { memoryFlow: reportMemoryFlow } : {}),
@@ -2031,6 +2220,7 @@ export class IngestBundleRunner {
2031
2220
  artifactResolutions: latestArtifactResolutions,
2032
2221
  evictionInputs: latestEvictionInputs,
2033
2222
  reconciliationActions: latestReconciliationActions,
2223
+ finalization: latestFinalizationOutcome,
2034
2224
  evictionDecisions: [],
2035
2225
  unresolvedCards: latestUnresolvedCards,
2036
2226
  supersededBy: null,