@kaelio/ktx 0.1.0-rc.6 → 0.1.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 (55) hide show
  1. package/assets/python/{kaelio_ktx-0.1.0rc6-py3-none-any.whl → kaelio_ktx-0.1.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/commands/mcp-commands.js +11 -3
  4. package/dist/commands/mcp-commands.test.js +30 -1
  5. package/dist/ingest.test.js +2 -26
  6. package/dist/next-steps.js +1 -1
  7. package/dist/next-steps.test.js +2 -0
  8. package/dist/runtime-requirements.d.ts +1 -2
  9. package/dist/runtime-requirements.js +0 -7
  10. package/dist/runtime-requirements.test.js +2 -2
  11. package/dist/setup-agents.d.ts +11 -3
  12. package/dist/setup-agents.js +397 -134
  13. package/dist/setup-agents.test.js +359 -61
  14. package/dist/setup-runtime.d.ts +0 -1
  15. package/dist/setup-runtime.js +0 -1
  16. package/dist/setup-runtime.test.js +7 -13
  17. package/dist/setup.d.ts +3 -0
  18. package/dist/setup.js +51 -25
  19. package/dist/setup.test.js +112 -16
  20. package/node_modules/@ktx/connector-clickhouse/dist/package-exports.test.js +1 -1
  21. package/node_modules/@ktx/context/dist/core/git.service.d.ts +0 -1
  22. package/node_modules/@ktx/context/dist/core/git.service.js +0 -12
  23. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/historic-sql.adapter.d.ts +1 -2
  24. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/historic-sql.adapter.js +0 -18
  25. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/local-ingest-acceptance.test.js +6 -6
  26. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.d.ts +4 -0
  27. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.js +38 -0
  28. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.test.js +63 -0
  29. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/projection.d.ts +0 -5
  30. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/projection.js +0 -48
  31. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/projection.test.js +0 -83
  32. package/node_modules/@ktx/context/dist/ingest/index.d.ts +2 -1
  33. package/node_modules/@ktx/context/dist/ingest/index.js +1 -0
  34. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.d.ts +0 -2
  35. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.isolated-diff.test.js +0 -166
  36. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.js +45 -235
  37. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.test.js +38 -193
  38. package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +3 -22
  39. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +4 -0
  40. package/node_modules/@ktx/context/dist/ingest/local-ingest.js +7 -0
  41. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +4 -4
  42. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.js +1 -1
  43. package/node_modules/@ktx/context/dist/ingest/memory-flow/types.d.ts +1 -1
  44. package/node_modules/@ktx/context/dist/ingest/ports.d.ts +20 -1
  45. package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +2 -73
  46. package/node_modules/@ktx/context/dist/ingest/report-snapshot.js +0 -27
  47. package/node_modules/@ktx/context/dist/ingest/reports.d.ts +5 -23
  48. package/node_modules/@ktx/context/dist/ingest/reports.js +24 -7
  49. package/node_modules/@ktx/context/dist/ingest/types.d.ts +0 -33
  50. package/node_modules/@ktx/context/dist/package-exports.test.js +1 -2
  51. package/package.json +4 -4
  52. package/node_modules/@ktx/context/dist/ingest/finalization-scope.d.ts +0 -22
  53. package/node_modules/@ktx/context/dist/ingest/finalization-scope.js +0 -95
  54. package/node_modules/@ktx/context/dist/ingest/finalization-scope.test.js +0 -114
  55. /package/node_modules/@ktx/context/dist/ingest/{finalization-scope.test.d.ts → adapters/historic-sql/post-processor.test.d.ts} +0 -0
@@ -36,27 +36,22 @@ describe('runKtxSetupRuntimeStep', () => {
36
36
  afterEach(async () => {
37
37
  await rm(tempDir, { recursive: true, force: true });
38
38
  });
39
- it('ensures core runtime for agent setup and records the runtime step', async () => {
39
+ it('skips runtime setup when the project has no direct runtime requirements', async () => {
40
40
  const io = makeIo();
41
- const ensureRuntime = vi.fn(async () => ({}));
41
+ const ensureRuntime = vi.fn();
42
42
  await expect(runKtxSetupRuntimeStep({
43
43
  projectDir: tempDir,
44
44
  inputMode: 'auto',
45
45
  cliVersion: '0.2.0',
46
46
  runtimeInstallPolicy: 'prompt',
47
- agents: true,
48
47
  }, io.io, {
49
48
  loadProject: projectConfig(buildDefaultKtxProjectConfig()),
50
49
  ensureRuntime,
51
50
  env: {},
52
- })).resolves.toMatchObject({ status: 'ready' });
53
- expect(ensureRuntime).toHaveBeenCalledWith(expect.objectContaining({
54
- cliVersion: '0.2.0',
55
- installPolicy: 'prompt',
56
- feature: 'core',
57
- }));
58
- expect((await readKtxSetupState(tempDir)).completed_steps).toContain('runtime');
59
- expect(io.stdout()).toContain('Runtime ready: yes (core)');
51
+ })).resolves.toMatchObject({ status: 'skipped' });
52
+ expect(ensureRuntime).not.toHaveBeenCalled();
53
+ expect((await readKtxSetupState(tempDir)).completed_steps).not.toContain('runtime');
54
+ expect(io.stdout()).toContain('Runtime setup skipped.');
60
55
  });
61
56
  it('fails fast when required runtime features cannot be installed in no-input mode', async () => {
62
57
  const io = makeIo();
@@ -68,7 +63,7 @@ describe('runKtxSetupRuntimeStep', () => {
68
63
  inputMode: 'disabled',
69
64
  cliVersion: '0.2.0',
70
65
  runtimeInstallPolicy: 'never',
71
- agents: true,
66
+ databaseIntrospectionFallback: true,
72
67
  }, io.io, {
73
68
  loadProject: projectConfig(buildDefaultKtxProjectConfig()),
74
69
  ensureRuntime,
@@ -101,7 +96,6 @@ describe('runKtxSetupRuntimeStep', () => {
101
96
  inputMode: 'auto',
102
97
  cliVersion: '0.2.0',
103
98
  runtimeInstallPolicy: 'auto',
104
- agents: false,
105
99
  }, io.io, {
106
100
  loadProject: projectConfig(config),
107
101
  ensureLocalEmbeddings,
package/dist/setup.d.ts CHANGED
@@ -143,4 +143,7 @@ export interface ReadKtxSetupStatusOptions {
143
143
  }
144
144
  export declare function readKtxSetupStatus(projectDir: string, options?: ReadKtxSetupStatusOptions): Promise<KtxSetupStatus>;
145
145
  export declare function formatKtxSetupStatus(status: KtxSetupStatus): string;
146
+ export declare function formatKtxSetupCompletionSummary(status: KtxSetupStatus, options?: {
147
+ agentNextActions?: string;
148
+ }): string;
146
149
  export declare function runKtxSetup(args: KtxSetupArgs, io: KtxCliIo, deps?: KtxSetupDeps): Promise<number>;
package/dist/setup.js CHANGED
@@ -7,7 +7,7 @@ import { runtimeInstallPolicyFromFlags } from './managed-python-command.js';
7
7
  import { readManagedPythonRuntimeStatus } from './managed-python-runtime.js';
8
8
  import { resolveProjectRuntimeRequirements } from './runtime-requirements.js';
9
9
  import { isKtxSetupExitError } from './setup-interrupt.js';
10
- import { readKtxAgentInstallManifest, runKtxSetupAgentsStep, } from './setup-agents.js';
10
+ import { readKtxAgentInstallManifest, runKtxSetupAgentsStep, targetDisplayName, } from './setup-agents.js';
11
11
  import { runKtxSetupDatabasesStep, } from './setup-databases.js';
12
12
  import { runKtxSetupEmbeddingsStep } from './setup-embeddings.js';
13
13
  import { isKtxSetupLlmConfigReady, runKtxSetupAnthropicModelStep, } from './setup-models.js';
@@ -136,7 +136,6 @@ export async function readKtxSetupStatus(projectDir, options = {}) {
136
136
  }
137
137
  const agents = [...agentMap.values()];
138
138
  const runtimeRequirements = resolveProjectRuntimeRequirements(project.config, {
139
- agents: agents.length > 0,
140
139
  env: options.env ?? process.env,
141
140
  });
142
141
  let runtimeReady = runtimeRequirements.features.length === 0 || completedSteps.includes('runtime');
@@ -219,6 +218,26 @@ export function formatKtxSetupStatus(status) {
219
218
  }
220
219
  return `${lines.join('\n')}\n`;
221
220
  }
221
+ export function formatKtxSetupCompletionSummary(status, options = {}) {
222
+ const readyAgents = status.agents.filter((agent) => agent.ready).map((agent) => targetDisplayName(agent.target));
223
+ const lines = [
224
+ 'Project',
225
+ ` ${status.project.path}`,
226
+ '',
227
+ 'Context',
228
+ ` ${status.context.ready ? 'built' : formatContextBuilt(status.context)}`,
229
+ '',
230
+ 'Agents configured',
231
+ ` ${readyAgents.length > 0 ? readyAgents.join(', ') : 'not installed'}`,
232
+ ];
233
+ const agentNextActions = options.agentNextActions?.trim();
234
+ if (agentNextActions) {
235
+ lines.push('', 'REQUIRED BEFORE USING AGENTS', '', ...agentNextActions.split('\n').map((line) => (line ? ` ${line}` : '')));
236
+ }
237
+ lines.push('', agentNextActions ? 'After that, try' : 'Try it');
238
+ lines.push(' Ask your agent: "Use KTX to show me the available tables."');
239
+ return lines.join('\n');
240
+ }
222
241
  function setupStatusReady(status) {
223
242
  if (!status.project.ready) {
224
243
  return false;
@@ -238,10 +257,8 @@ function setupHasContextTargets(status) {
238
257
  function setupContextReady(status) {
239
258
  return status.context.ready;
240
259
  }
241
- function writeContextNotReadyForAgents(projectDir, io) {
242
- io.stderr.write('KTX context is not ready for agents.\n\n');
243
- io.stderr.write(`Build context first:\n ktx setup --project-dir ${resolve(projectDir)}\n\n`);
244
- io.stderr.write(`Then install agent integration:\n ktx setup --agents --project-dir ${resolve(projectDir)}\n`);
260
+ function shouldPrintConciseReadySummary(status) {
261
+ return setupStatusReady(status) && setupContextReady(status) && status.agents.some((agent) => agent.ready);
245
262
  }
246
263
  function setupRuntimeInstallPolicy(args) {
247
264
  if (args.yes) {
@@ -269,6 +286,7 @@ async function runKtxSetupInner(args, io, deps = {}) {
269
286
  setupUi.intro('KTX setup', io);
270
287
  let entryAction;
271
288
  let projectResult;
289
+ let agentNextActions;
272
290
  const canShowEntryMenu = args.showEntryMenu === true &&
273
291
  args.inputMode !== 'disabled' &&
274
292
  !args.agents &&
@@ -318,16 +336,17 @@ async function runKtxSetupInner(args, io, deps = {}) {
318
336
  }
319
337
  }
320
338
  const runOnly = readyAction;
339
+ const agentOnlySetup = agentsRequested || runOnly === 'agents';
321
340
  const shouldRunModels = !runOnly || runOnly === 'models';
322
341
  const shouldRunEmbeddings = !runOnly || runOnly === 'embeddings';
323
342
  const shouldRunDatabases = !runOnly || runOnly === 'databases';
324
343
  const shouldRunSources = !runOnly || runOnly === 'sources';
325
- const shouldRunRuntime = agentsRequested || !runOnly || runOnly === 'runtime' || runOnly === 'context' || runOnly === 'agents';
326
- const shouldRunContext = agentsRequested || !runOnly || runOnly === 'context';
344
+ const shouldRunRuntime = !agentOnlySetup && (!runOnly || runOnly === 'runtime' || runOnly === 'context');
345
+ const shouldRunContext = !agentOnlySetup && (!runOnly || runOnly === 'context');
327
346
  const shouldRunAgents = agentsRequested || !runOnly || runOnly === 'agents';
328
347
  const showPromptInstructions = projectResult.confirmedCreation !== true;
329
- const setupSteps = agentsRequested
330
- ? ['runtime', 'context']
348
+ const setupSteps = agentOnlySetup
349
+ ? []
331
350
  : ['models', 'embeddings', 'databases', 'sources', 'runtime', 'context'];
332
351
  if (shouldRunAgents && args.skipAgents !== true) {
333
352
  setupSteps.push('agents');
@@ -456,7 +475,6 @@ async function runKtxSetupInner(args, io, deps = {}) {
456
475
  inputMode: args.inputMode,
457
476
  cliVersion: args.cliVersion,
458
477
  runtimeInstallPolicy: setupRuntimeInstallPolicy(args),
459
- agents: shouldRunAgents && args.skipAgents !== true,
460
478
  }, io);
461
479
  }
462
480
  else if (step === 'context') {
@@ -473,7 +491,7 @@ async function runKtxSetupInner(args, io, deps = {}) {
473
491
  }
474
492
  else {
475
493
  const agentsRunner = deps.agents ?? ((agentArgs, agentIo) => runKtxSetupAgentsStep(agentArgs, agentIo, deps.agentsDeps));
476
- stepResult = await agentsRunner({
494
+ const agentResult = await agentsRunner({
477
495
  projectDir: projectResult.projectDir,
478
496
  inputMode: args.inputMode,
479
497
  yes: args.yes,
@@ -482,7 +500,12 @@ async function runKtxSetupInner(args, io, deps = {}) {
482
500
  scope: args.agentScope ?? 'project',
483
501
  mode: 'mcp',
484
502
  skipAgents: false,
503
+ showNextActions: agentsRequested,
485
504
  }, io);
505
+ stepResult = agentResult;
506
+ if (agentResult.status === 'ready') {
507
+ agentNextActions = agentResult.nextActions;
508
+ }
486
509
  }
487
510
  if (stepResult.status === 'failed' || stepResult.status === 'missing-input') {
488
511
  return 1;
@@ -504,10 +527,6 @@ async function runKtxSetupInner(args, io, deps = {}) {
504
527
  }
505
528
  if (step === 'context' && stepResult.status !== 'ready') {
506
529
  if (shouldRunAgents && args.skipAgents !== true) {
507
- if (agentsRequested) {
508
- writeContextNotReadyForAgents(projectResult.projectDir, io);
509
- return args.inputMode === 'disabled' ? 1 : 0;
510
- }
511
530
  return 0;
512
531
  }
513
532
  }
@@ -520,15 +539,22 @@ async function runKtxSetupInner(args, io, deps = {}) {
520
539
  const status = await readKtxSetupStatus(projectResult.projectDir, { cliVersion: args.cliVersion });
521
540
  const focusedOnAgents = args.agents || entryAction === 'agents';
522
541
  if (!focusedOnAgents) {
523
- setupUi.note(formatKtxSetupStatus(status).trimEnd(), 'Project status', io, {
524
- format: (line) => line,
525
- });
526
- setupUi.note(formatSetupNextStepLines({
527
- setupReady: setupStatusReady(status),
528
- hasContextTargets: setupHasContextTargets(status),
529
- contextReady: setupContextReady(status),
530
- agentIntegrationReady: status.agents.some((agent) => agent.ready),
531
- }).join('\n'), 'What you can do next', io);
542
+ if (shouldPrintConciseReadySummary(status)) {
543
+ setupUi.note(formatKtxSetupCompletionSummary(status, { agentNextActions }), agentNextActions ? 'Finish KTX agent setup' : 'KTX project ready', io, {
544
+ format: (line) => line,
545
+ });
546
+ }
547
+ else {
548
+ setupUi.note(formatKtxSetupStatus(status).trimEnd(), 'Project status', io, {
549
+ format: (line) => line,
550
+ });
551
+ setupUi.note(formatSetupNextStepLines({
552
+ setupReady: setupStatusReady(status),
553
+ hasContextTargets: setupHasContextTargets(status),
554
+ contextReady: setupContextReady(status),
555
+ agentIntegrationReady: status.agents.some((agent) => agent.ready),
556
+ }).join('\n'), 'What you can do next', io);
557
+ }
532
558
  }
533
559
  return 0;
534
560
  }
@@ -8,7 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8
8
  import { localFakeBundleReport, persistLocalBundleReport } from './ingest.test-utils.js';
9
9
  import { contextBuildCommands, writeKtxSetupContextState } from './setup-context.js';
10
10
  import { runDemoTour } from './setup-demo-tour.js';
11
- import { formatKtxSetupStatus, readKtxSetupStatus, runKtxSetup } from './setup.js';
11
+ import { formatKtxSetupCompletionSummary, formatKtxSetupStatus, readKtxSetupStatus, runKtxSetup } from './setup.js';
12
12
  vi.mock('./setup-demo-tour.js', () => ({
13
13
  runDemoTour: vi.fn(async () => 0),
14
14
  }));
@@ -337,6 +337,98 @@ describe('setup status', () => {
337
337
  expect(rendered).toContain('KTX context built: no');
338
338
  expect(rendered).not.toContain('No KTX project found.');
339
339
  });
340
+ it('formats a concise ready summary for completed agent setup', () => {
341
+ const rendered = formatKtxSetupCompletionSummary({
342
+ project: { path: tempDir, ready: true },
343
+ llm: { ready: true, model: 'sonnet' },
344
+ embeddings: { ready: true, model: 'text-embedding-3-small' },
345
+ databases: [{ connectionId: 'postgres-warehouse', ready: true }],
346
+ sources: [{ connectionId: 'dbt-main', type: 'dbt', ready: true }],
347
+ runtime: { required: true, ready: true, features: ['core'] },
348
+ context: { ready: true, status: 'completed' },
349
+ agents: [
350
+ { target: 'claude-code', scope: 'project', ready: true },
351
+ { target: 'claude-desktop', scope: 'global', ready: true },
352
+ ],
353
+ }, {
354
+ agentNextActions: [
355
+ '1. Start MCP',
356
+ ' Run this command before using Claude Code:',
357
+ '',
358
+ ' RUN:',
359
+ ` ktx mcp start --project-dir ${tempDir}`,
360
+ '',
361
+ ' If you need to stop MCP later:',
362
+ ` ktx mcp stop --project-dir ${tempDir}`,
363
+ '',
364
+ '2. Open Claude Code',
365
+ ' Open Claude Code from the KTX project directory:',
366
+ '',
367
+ ' RUN:',
368
+ ` cd '${tempDir}'`,
369
+ ' claude',
370
+ ].join('\n'),
371
+ });
372
+ expect(rendered).toContain(`Project\n ${tempDir}`);
373
+ expect(rendered).toContain('Context\n built');
374
+ expect(rendered).toContain('Agents configured\n Claude Code, Claude Desktop');
375
+ expect(rendered).toContain('REQUIRED BEFORE USING AGENTS\n\n 1. Start MCP');
376
+ expect(rendered).toContain(' Run this command before using Claude Code:');
377
+ expect(rendered).toContain(' RUN:');
378
+ expect(rendered).toContain(' If you need to stop MCP later:');
379
+ expect(rendered).toContain(`ktx mcp stop --project-dir ${tempDir}`);
380
+ expect(rendered).toContain('After that, try\n Ask your agent: "Use KTX to show me the available tables."');
381
+ expect(rendered).not.toContain('Verify');
382
+ expect(rendered).not.toContain('Project ready: yes');
383
+ expect(rendered).not.toContain('What you can do next');
384
+ });
385
+ it('prints agent next actions inside the final ready summary during full setup', async () => {
386
+ const testIo = makeIo();
387
+ await expect(runKtxSetup({
388
+ command: 'run',
389
+ projectDir: tempDir,
390
+ mode: 'new',
391
+ agents: false,
392
+ target: 'claude-code',
393
+ skipAgents: false,
394
+ inputMode: 'disabled',
395
+ yes: true,
396
+ cliVersion: '0.2.0',
397
+ skipLlm: true,
398
+ skipEmbeddings: true,
399
+ skipDatabases: true,
400
+ skipSources: true,
401
+ databaseSchemas: [],
402
+ }, testIo.io, {
403
+ runtime: async () => runtimeReady(tempDir),
404
+ context: async () => {
405
+ await writeKtxSetupContextState(tempDir, {
406
+ runId: 'setup-context-local-test',
407
+ status: 'completed',
408
+ primarySourceConnectionIds: [],
409
+ contextSourceConnectionIds: [],
410
+ reportIds: [],
411
+ artifactPaths: [],
412
+ retryableFailedTargets: [],
413
+ commands: contextBuildCommands(tempDir, 'setup-context-local-test'),
414
+ });
415
+ await writeKtxSetupState(tempDir, { completed_steps: ['project', 'context'] });
416
+ return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
417
+ },
418
+ })).resolves.toBe(0);
419
+ const output = testIo.stdout();
420
+ expect(output).toContain('Claude Code · Project scope');
421
+ expect(output).toContain(join(tempDir, '.mcp.json'));
422
+ expect(output).toContain('Requires MCP to be started.');
423
+ expect(output).toContain('Analytics skill installed.');
424
+ expect(output).not.toContain('Agent integration complete');
425
+ expect(output).toContain('Finish KTX agent setup');
426
+ expect(output).not.toContain('KTX project ready');
427
+ expect(output).toContain('REQUIRED BEFORE USING AGENTS');
428
+ expect(output).toContain('Run this command before using Claude Code:');
429
+ expect(output).toContain(`ktx mcp start --project-dir ${tempDir}`);
430
+ expect(output).not.toContain('Finish agent setup');
431
+ });
340
432
  it('prints the setup shell intro for auto-created run mode', async () => {
341
433
  const testIo = makeIo();
342
434
  await expect(runKtxSetup({
@@ -1257,7 +1349,7 @@ describe('setup status', () => {
1257
1349
  const committedConfig = await execFileAsync('git', ['-C', tempDir, 'show', 'HEAD:ktx.yaml']);
1258
1350
  expect(committedConfig.stdout).toContain('warehouse:');
1259
1351
  });
1260
- it('runs agent setup after context succeeds in --agents mode', async () => {
1352
+ it('runs agent setup without runtime or context in --agents mode', async () => {
1261
1353
  const calls = [];
1262
1354
  const io = makeIo();
1263
1355
  await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
@@ -1284,11 +1376,11 @@ describe('setup status', () => {
1284
1376
  sources: async () => ({ status: 'skipped', projectDir: tempDir }),
1285
1377
  runtime: async () => {
1286
1378
  calls.push('runtime');
1287
- return runtimeReady(tempDir);
1379
+ throw new Error('runtime should not run');
1288
1380
  },
1289
1381
  context: async () => {
1290
1382
  calls.push('context');
1291
- return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
1383
+ throw new Error('context should not run');
1292
1384
  },
1293
1385
  agents: async () => {
1294
1386
  calls.push('agents');
@@ -1299,10 +1391,12 @@ describe('setup status', () => {
1299
1391
  };
1300
1392
  },
1301
1393
  })).resolves.toBe(0);
1302
- expect(calls).toEqual(['runtime', 'context', 'agents']);
1394
+ expect(calls).toEqual(['agents']);
1303
1395
  });
1304
- it('does not install agents when non-interactive --agents finds context incomplete', async () => {
1396
+ it('installs agents when non-interactive --agents finds context incomplete', async () => {
1305
1397
  const io = makeIo();
1398
+ const runtime = vi.fn(async () => runtimeReady(tempDir));
1399
+ const context = vi.fn(async () => ({ status: 'skipped', projectDir: tempDir }));
1306
1400
  const agents = vi.fn(async () => ({
1307
1401
  status: 'ready',
1308
1402
  projectDir: tempDir,
@@ -1326,12 +1420,14 @@ describe('setup status', () => {
1326
1420
  skipAgents: false,
1327
1421
  databaseSchemas: [],
1328
1422
  }, io.io, {
1329
- runtime: async () => runtimeReady(tempDir),
1330
- context: async () => ({ status: 'skipped', projectDir: tempDir }),
1423
+ runtime,
1424
+ context,
1331
1425
  agents,
1332
- })).resolves.toBe(1);
1333
- expect(agents).not.toHaveBeenCalled();
1334
- expect(io.stderr()).toContain('KTX context is not ready for agents.');
1426
+ })).resolves.toBe(0);
1427
+ expect(runtime).not.toHaveBeenCalled();
1428
+ expect(context).not.toHaveBeenCalled();
1429
+ expect(agents).toHaveBeenCalledTimes(1);
1430
+ expect(io.stderr()).not.toContain('KTX context is not ready for agents.');
1335
1431
  });
1336
1432
  it('routes a ready project menu selection to agent setup', async () => {
1337
1433
  const calls = [];
@@ -1433,7 +1529,7 @@ describe('setup status', () => {
1433
1529
  process.env.KTX_RUNTIME_ROOT = previousRuntimeRoot;
1434
1530
  }
1435
1531
  }
1436
- expect(calls).toEqual(['runtime', 'agents']);
1532
+ expect(calls).toEqual(['agents']);
1437
1533
  });
1438
1534
  it('skips to agent setup when context is ready but agents are not configured', async () => {
1439
1535
  const calls = [];
@@ -1517,9 +1613,9 @@ describe('setup status', () => {
1517
1613
  },
1518
1614
  })).resolves.toBe(0);
1519
1615
  expect(readyMenuSelect).not.toHaveBeenCalled();
1520
- expect(calls).toEqual(['runtime', 'agents']);
1616
+ expect(calls).toEqual(['agents']);
1521
1617
  });
1522
- it('runs only project resolution, runtime, context gate, and agent setup in --agents mode', async () => {
1618
+ it('runs only project resolution and agent setup in --agents mode', async () => {
1523
1619
  const io = makeIo();
1524
1620
  const runtime = vi.fn(async () => runtimeReady(tempDir));
1525
1621
  const context = vi.fn(async () => ({ status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' }));
@@ -1552,8 +1648,8 @@ describe('setup status', () => {
1552
1648
  context,
1553
1649
  agents,
1554
1650
  })).resolves.toBe(0);
1555
- expect(runtime).toHaveBeenCalledTimes(1);
1556
- expect(context).toHaveBeenCalledTimes(1);
1651
+ expect(runtime).not.toHaveBeenCalled();
1652
+ expect(context).not.toHaveBeenCalled();
1557
1653
  expect(agents).toHaveBeenCalledTimes(1);
1558
1654
  });
1559
1655
  it('does not run embedding setup when the model step fails', async () => {
@@ -6,5 +6,5 @@ describe('@ktx/connector-clickhouse package exports', () => {
6
6
  expect(connector.KtxClickHouseScanConnector).toBeTypeOf('function');
7
7
  expect(connector.clickHouseClientConfigFromConfig).toBeTypeOf('function');
8
8
  expect(connector.createClickHouseLiveDatabaseIntrospection).toBeTypeOf('function');
9
- });
9
+ }, 20_000);
10
10
  });
@@ -101,7 +101,6 @@ export declare class GitService {
101
101
  status: 'A' | 'M' | 'D';
102
102
  path: string;
103
103
  }>>;
104
- changedPaths(): Promise<string[]>;
105
104
  /**
106
105
  * List all paths under the working tree that match `pathSpec`, scoped to HEAD.
107
106
  * Used for the reconciler's first-ever run when there's no watermark to diff from.
@@ -424,18 +424,6 @@ export class GitService {
424
424
  }
425
425
  return out;
426
426
  }
427
- async changedPaths() {
428
- const raw = await this.git.raw(['status', '--porcelain=v1', '-z']);
429
- const fields = raw.split('\0').filter(Boolean);
430
- const paths = [];
431
- for (const field of fields) {
432
- const path = field.slice(3);
433
- if (path.length > 0) {
434
- paths.push(path);
435
- }
436
- }
437
- return [...new Set(paths)].sort();
438
- }
439
427
  /**
440
428
  * List all paths under the working tree that match `pathSpec`, scoped to HEAD.
441
429
  * Used for the reconciler's first-ever run when there's no watermark to diff from.
@@ -1,4 +1,4 @@
1
- import type { ChunkResult, DeterministicFinalizationContext, DiffSet, FetchContext, FinalizationResult, ScopeDescriptor, SourceAdapter } from '../../types.js';
1
+ import type { ChunkResult, DiffSet, FetchContext, ScopeDescriptor, SourceAdapter } from '../../types.js';
2
2
  import { type HistoricSqlSourceAdapterDeps } from './types.js';
3
3
  export declare class HistoricSqlSourceAdapter implements SourceAdapter {
4
4
  private readonly deps;
@@ -11,5 +11,4 @@ export declare class HistoricSqlSourceAdapter implements SourceAdapter {
11
11
  fetch(pullConfig: unknown, stagedDir: string, ctx: FetchContext): Promise<void>;
12
12
  chunk(stagedDir: string, diffSet?: DiffSet): Promise<ChunkResult>;
13
13
  describeScope(stagedDir: string): Promise<ScopeDescriptor>;
14
- finalize(ctx: DeterministicFinalizationContext): Promise<FinalizationResult>;
15
14
  }
@@ -1,6 +1,5 @@
1
1
  import { chunkHistoricSqlUnifiedStagedDir, describeHistoricSqlUnifiedScope } from './chunk-unified.js';
2
2
  import { detectHistoricSqlStagedDir } from './detect.js';
3
- import { projectHistoricSqlEvidence } from './projection.js';
4
3
  import { stageHistoricSqlAggregatedSnapshot } from './stage-unified.js';
5
4
  export class HistoricSqlSourceAdapter {
6
5
  deps;
@@ -31,21 +30,4 @@ export class HistoricSqlSourceAdapter {
31
30
  describeScope(stagedDir) {
32
31
  return describeHistoricSqlUnifiedScope(stagedDir);
33
32
  }
34
- async finalize(ctx) {
35
- const projection = await projectHistoricSqlEvidence({
36
- workdir: ctx.workdir,
37
- connectionId: ctx.connectionId,
38
- syncId: ctx.syncId,
39
- runId: ctx.runId,
40
- overrideReplay: ctx.overrideReplay,
41
- });
42
- return {
43
- result: projection,
44
- warnings: projection.warnings,
45
- errors: [],
46
- touchedSources: projection.touchedSources,
47
- changedWikiPageKeys: projection.changedWikiPageKeys,
48
- actions: projection.actions,
49
- };
50
- }
51
33
  }
@@ -194,12 +194,12 @@ describe('historic-SQL local ingest retrieval acceptance', () => {
194
194
  expect(result.result.failedWorkUnits).toEqual([]);
195
195
  expect(result.result.workUnitCount).toBe(3);
196
196
  expect(agentRunner.runLoop).toHaveBeenCalledTimes(3);
197
- const finalization = result.report.body.finalization;
198
- expect(finalization).toBeDefined();
199
- if (!finalization) {
200
- throw new Error('Expected historic-SQL finalization result');
197
+ const postProcessor = result.report.body.postProcessor;
198
+ expect(postProcessor).toBeDefined();
199
+ if (!postProcessor) {
200
+ throw new Error('Expected historic-SQL post-processor result');
201
201
  }
202
- expect(finalization).toMatchObject({
202
+ expect(postProcessor).toMatchObject({
203
203
  sourceKey: 'historic-sql',
204
204
  status: 'success',
205
205
  result: {
@@ -207,7 +207,7 @@ describe('historic-SQL local ingest retrieval acceptance', () => {
207
207
  patternPagesWritten: 1,
208
208
  },
209
209
  });
210
- expect(finalization.declaredTouchedSources).toEqual(expect.arrayContaining([
210
+ expect(postProcessor.touchedSources).toEqual(expect.arrayContaining([
211
211
  { connectionId: 'warehouse', sourceName: 'customers' },
212
212
  { connectionId: 'warehouse', sourceName: 'orders' },
213
213
  ]));
@@ -0,0 +1,4 @@
1
+ import type { IngestBundlePostProcessorInput, IngestBundlePostProcessorPort, IngestBundlePostProcessorResult } from '../../ports.js';
2
+ export declare class HistoricSqlProjectionPostProcessor implements IngestBundlePostProcessorPort {
3
+ run(input: IngestBundlePostProcessorInput): Promise<IngestBundlePostProcessorResult>;
4
+ }
@@ -0,0 +1,38 @@
1
+ import { createSimpleGit } from '../../../core/git-env.js';
2
+ import { projectHistoricSqlEvidence } from './projection.js';
3
+ async function commitProjectionChanges(workdir) {
4
+ const git = createSimpleGit(workdir);
5
+ if (!(await git.checkIsRepo().catch(() => false))) {
6
+ return;
7
+ }
8
+ const status = await git.status();
9
+ const paths = status.files
10
+ .map((file) => file.path)
11
+ .filter((path) => path.startsWith('semantic-layer/') || path.startsWith('wiki/global/historic-sql'));
12
+ if (paths.length === 0) {
13
+ return;
14
+ }
15
+ await git.add(paths);
16
+ const staged = await git.diff(['--cached', '--name-only']);
17
+ if (!staged.trim()) {
18
+ return;
19
+ }
20
+ await git.commit('Project historic SQL evidence', { '--author': 'System User <system@example.com>' });
21
+ }
22
+ export class HistoricSqlProjectionPostProcessor {
23
+ async run(input) {
24
+ const projection = await projectHistoricSqlEvidence({
25
+ workdir: input.workdir,
26
+ connectionId: input.connectionId,
27
+ syncId: input.syncId,
28
+ runId: input.runId,
29
+ });
30
+ await commitProjectionChanges(input.workdir);
31
+ return {
32
+ result: projection,
33
+ warnings: projection.warnings,
34
+ errors: [],
35
+ touchedSources: projection.touchedSources,
36
+ };
37
+ }
38
+ }
@@ -0,0 +1,63 @@
1
+ import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import YAML from 'yaml';
5
+ import { describe, expect, it } from 'vitest';
6
+ import { HistoricSqlProjectionPostProcessor } from './post-processor.js';
7
+ async function tempWorkdir() {
8
+ return mkdtemp(join(tmpdir(), 'historic-sql-post-processor-'));
9
+ }
10
+ async function writeJson(root, relPath, value) {
11
+ const target = join(root, relPath);
12
+ await mkdir(join(target, '..'), { recursive: true });
13
+ await writeFile(target, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
14
+ }
15
+ describe('HistoricSqlProjectionPostProcessor', () => {
16
+ it('projects current run evidence before the ingest squash commit', async () => {
17
+ const workdir = await tempWorkdir();
18
+ await mkdir(join(workdir, 'semantic-layer/warehouse/_schema'), { recursive: true });
19
+ await writeFile(join(workdir, 'semantic-layer/warehouse/_schema/public.yaml'), YAML.stringify({ tables: { orders: { table: 'public.orders', columns: [{ name: 'id', type: 'string' }] } } }), 'utf-8');
20
+ await writeJson(workdir, 'raw-sources/warehouse/historic-sql/sync-1/manifest.json', {
21
+ source: 'historic-sql',
22
+ connectionId: 'warehouse',
23
+ dialect: 'postgres',
24
+ fetchedAt: '2026-05-11T00:00:00.000Z',
25
+ windowStart: '2026-02-10T00:00:00.000Z',
26
+ windowEnd: '2026-05-11T00:00:00.000Z',
27
+ snapshotRowCount: 1,
28
+ touchedTableCount: 1,
29
+ parseFailures: 0,
30
+ warnings: [],
31
+ probeWarnings: [],
32
+ staleArchiveAfterDays: 90,
33
+ });
34
+ await writeJson(workdir, 'raw-sources/warehouse/historic-sql/sync-1/tables/public.orders.json', { table: 'public.orders' });
35
+ await writeJson(workdir, '.ktx/ingest-evidence/historic-sql/run-1/orders.json', {
36
+ kind: 'table_usage',
37
+ connectionId: 'warehouse',
38
+ table: 'public.orders',
39
+ rawPath: 'tables/public.orders.json',
40
+ usage: {
41
+ narrative: 'Orders are repeatedly queried by lifecycle status.',
42
+ frequencyTier: 'high',
43
+ commonFilters: ['status'],
44
+ commonJoins: [],
45
+ staleSince: null,
46
+ },
47
+ });
48
+ const result = await new HistoricSqlProjectionPostProcessor().run({
49
+ connectionId: 'warehouse',
50
+ sourceKey: 'historic-sql',
51
+ syncId: 'sync-1',
52
+ jobId: 'job-1',
53
+ runId: 'run-1',
54
+ workdir,
55
+ parseArtifacts: null,
56
+ });
57
+ expect(result.errors).toEqual([]);
58
+ expect(result.warnings).toEqual([]);
59
+ expect(result.touchedSources).toEqual([{ connectionId: 'warehouse', sourceName: 'orders' }]);
60
+ expect(result.result).toMatchObject({ tableUsageMerged: 1 });
61
+ await expect(readFile(join(workdir, 'semantic-layer/warehouse/_schema/public.yaml'), 'utf-8')).resolves.toContain('Orders are repeatedly queried by lifecycle status.');
62
+ });
63
+ });
@@ -1,11 +1,8 @@
1
- import type { MemoryAction } from '../../../memory/index.js';
2
- import type { FinalizationOverrideReplay } from '../../types.js';
3
1
  export interface HistoricSqlProjectionInput {
4
2
  workdir: string;
5
3
  connectionId: string;
6
4
  syncId: string;
7
5
  runId: string;
8
- overrideReplay?: FinalizationOverrideReplay;
9
6
  }
10
7
  export interface HistoricSqlProjectionResult {
11
8
  tableUsageMerged: number;
@@ -17,8 +14,6 @@ export interface HistoricSqlProjectionResult {
17
14
  connectionId: string;
18
15
  sourceName: string;
19
16
  }>;
20
- changedWikiPageKeys: string[];
21
- actions: MemoryAction[];
22
17
  warnings: string[];
23
18
  }
24
19
  export declare function projectHistoricSqlEvidence(input: HistoricSqlProjectionInput): Promise<HistoricSqlProjectionResult>;