@kaelio/ktx 0.1.0-rc.5 → 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 (180) hide show
  1. package/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl +0 -0
  2. package/assets/python/manifest.json +2 -2
  3. package/dist/clack.d.ts +6 -0
  4. package/dist/clack.js +23 -0
  5. package/dist/cli-program.js +5 -2
  6. package/dist/cli-program.test.js +7 -1
  7. package/dist/cli-runtime.d.ts +4 -0
  8. package/dist/cli-runtime.js +8 -1
  9. package/dist/command-schemas.d.ts +1 -1
  10. package/dist/commands/ingest-commands.js +1 -0
  11. package/dist/commands/knowledge-commands.js +5 -0
  12. package/dist/commands/mcp-commands.js +11 -3
  13. package/dist/commands/mcp-commands.test.js +30 -1
  14. package/dist/commands/sql-commands.d.ts +3 -0
  15. package/dist/commands/sql-commands.js +43 -0
  16. package/dist/commands/sql-commands.test.d.ts +1 -0
  17. package/dist/commands/sql-commands.test.js +68 -0
  18. package/dist/context-build-view.js +5 -1
  19. package/dist/dev.test.js +27 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +1 -0
  22. package/dist/index.test.js +56 -21
  23. package/dist/ingest.js +123 -18
  24. package/dist/ingest.test.js +206 -0
  25. package/dist/io/print-list.d.ts +2 -1
  26. package/dist/io/print-list.js +7 -0
  27. package/dist/io/print-list.test.js +13 -11
  28. package/dist/io/symbols.d.ts +2 -2
  29. package/dist/knowledge.d.ts +1 -0
  30. package/dist/knowledge.js +34 -16
  31. package/dist/knowledge.test.js +27 -0
  32. package/dist/managed-python-command.d.ts +2 -0
  33. package/dist/managed-python-command.js +17 -9
  34. package/dist/managed-python-command.test.js +59 -4
  35. package/dist/next-steps.js +1 -1
  36. package/dist/next-steps.test.js +2 -0
  37. package/dist/print-command-tree.js +7 -1
  38. package/dist/public-ingest.d.ts +9 -1
  39. package/dist/public-ingest.js +50 -7
  40. package/dist/public-ingest.test.js +69 -2
  41. package/dist/release-version.d.ts +5 -0
  42. package/dist/release-version.js +44 -0
  43. package/dist/runtime-requirements.d.ts +23 -0
  44. package/dist/runtime-requirements.js +99 -0
  45. package/dist/runtime-requirements.test.d.ts +1 -0
  46. package/dist/runtime-requirements.test.js +63 -0
  47. package/dist/setup-agents.d.ts +11 -3
  48. package/dist/setup-agents.js +397 -134
  49. package/dist/setup-agents.test.js +359 -61
  50. package/dist/setup-embeddings.js +3 -6
  51. package/dist/setup-embeddings.test.js +18 -2
  52. package/dist/setup-models.js +2 -2
  53. package/dist/setup-models.test.js +5 -3
  54. package/dist/setup-ready-menu.d.ts +1 -1
  55. package/dist/setup-ready-menu.js +2 -0
  56. package/dist/setup-ready-menu.test.js +3 -0
  57. package/dist/setup-runtime.d.ts +45 -0
  58. package/dist/setup-runtime.js +47 -0
  59. package/dist/setup-runtime.test.d.ts +1 -0
  60. package/dist/setup-runtime.test.js +110 -0
  61. package/dist/setup-sources-notion.test.d.ts +1 -0
  62. package/dist/setup-sources-notion.test.js +107 -0
  63. package/dist/setup-sources.js +5 -2
  64. package/dist/setup.d.ts +19 -1
  65. package/dist/setup.js +104 -29
  66. package/dist/setup.test.js +221 -57
  67. package/dist/sl.js +2 -2
  68. package/dist/sl.test.js +10 -0
  69. package/dist/source-mapping.js +9 -1
  70. package/dist/source-mapping.test.d.ts +1 -0
  71. package/dist/source-mapping.test.js +65 -0
  72. package/dist/sql.d.ts +22 -0
  73. package/dist/sql.js +125 -0
  74. package/dist/sql.test.d.ts +1 -0
  75. package/dist/sql.test.js +226 -0
  76. package/node_modules/@ktx/connector-clickhouse/dist/package-exports.test.js +1 -1
  77. package/node_modules/@ktx/context/dist/connections/connection-type.d.ts +4 -4
  78. package/node_modules/@ktx/context/dist/core/git.service.d.ts +3 -0
  79. package/node_modules/@ktx/context/dist/core/git.service.js +47 -1
  80. package/node_modules/@ktx/context/dist/core/git.service.patch.test.d.ts +1 -0
  81. package/node_modules/@ktx/context/dist/core/git.service.patch.test.js +40 -0
  82. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/types.d.ts +5 -5
  83. package/node_modules/@ktx/context/dist/ingest/adapters/looker/looker.adapter.d.ts +2 -2
  84. package/node_modules/@ktx/context/dist/ingest/adapters/looker/tools/looker-query-to-sl.tool.d.ts +2 -2
  85. package/node_modules/@ktx/context/dist/ingest/adapters/looker/types.d.ts +16 -16
  86. package/node_modules/@ktx/context/dist/ingest/adapters/lookml/pull-config.d.ts +1 -1
  87. package/node_modules/@ktx/context/dist/ingest/adapters/metabase/fetch.js +16 -0
  88. package/node_modules/@ktx/context/dist/ingest/adapters/metabase/fetch.test.js +41 -0
  89. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/metricflow.adapter.d.ts +2 -1
  90. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/metricflow.adapter.js +40 -0
  91. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/metricflow.adapter.test.js +116 -1
  92. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/projection-config.d.ts +29 -0
  93. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/projection-config.js +40 -0
  94. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/pull-config.d.ts +1 -1
  95. package/node_modules/@ktx/context/dist/ingest/artifact-gates.d.ts +25 -0
  96. package/node_modules/@ktx/context/dist/ingest/artifact-gates.js +149 -0
  97. package/node_modules/@ktx/context/dist/ingest/artifact-gates.test.d.ts +1 -0
  98. package/node_modules/@ktx/context/dist/ingest/artifact-gates.test.js +167 -0
  99. package/node_modules/@ktx/context/dist/ingest/final-gate-repair.d.ts +29 -0
  100. package/node_modules/@ktx/context/dist/ingest/final-gate-repair.js +178 -0
  101. package/node_modules/@ktx/context/dist/ingest/final-gate-repair.test.d.ts +1 -0
  102. package/node_modules/@ktx/context/dist/ingest/final-gate-repair.test.js +109 -0
  103. package/node_modules/@ktx/context/dist/ingest/index.d.ts +8 -1
  104. package/node_modules/@ktx/context/dist/ingest/index.js +7 -0
  105. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.d.ts +18 -2
  106. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.isolated-diff.test.d.ts +1 -0
  107. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.isolated-diff.test.js +1761 -0
  108. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.js +1695 -901
  109. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.test.js +135 -118
  110. package/node_modules/@ktx/context/dist/ingest/ingest-trace.d.ts +50 -0
  111. package/node_modules/@ktx/context/dist/ingest/ingest-trace.js +88 -0
  112. package/node_modules/@ktx/context/dist/ingest/ingest-trace.test.d.ts +1 -0
  113. package/node_modules/@ktx/context/dist/ingest/ingest-trace.test.js +76 -0
  114. package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.d.ts +16 -0
  115. package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.js +78 -0
  116. package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.test.d.ts +1 -0
  117. package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.test.js +76 -0
  118. package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.d.ts +58 -0
  119. package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.js +223 -0
  120. package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.test.d.ts +1 -0
  121. package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.test.js +369 -0
  122. package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.d.ts +23 -0
  123. package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.js +190 -0
  124. package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.test.d.ts +1 -0
  125. package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.test.js +101 -0
  126. package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.d.ts +15 -0
  127. package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.js +61 -0
  128. package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.test.d.ts +1 -0
  129. package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.test.js +137 -0
  130. package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +7 -0
  131. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +54 -10
  132. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.test.js +65 -0
  133. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +23 -5
  134. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.js +17 -0
  135. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.test.js +1 -0
  136. package/node_modules/@ktx/context/dist/ingest/memory-flow/types.d.ts +6 -0
  137. package/node_modules/@ktx/context/dist/ingest/parsed-target-table.d.ts +1 -1
  138. package/node_modules/@ktx/context/dist/ingest/ports.d.ts +3 -0
  139. package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +32 -7
  140. package/node_modules/@ktx/context/dist/ingest/report-snapshot.js +25 -0
  141. package/node_modules/@ktx/context/dist/ingest/report-snapshot.test.js +124 -0
  142. package/node_modules/@ktx/context/dist/ingest/reports.d.ts +23 -0
  143. package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.d.ts +11 -0
  144. package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.js +26 -0
  145. package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.test.d.ts +1 -0
  146. package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.test.js +25 -0
  147. package/node_modules/@ktx/context/dist/ingest/stages/stage-3-work-units.d.ts +4 -0
  148. package/node_modules/@ktx/context/dist/ingest/stages/stage-3-work-units.js +4 -0
  149. package/node_modules/@ktx/context/dist/ingest/stages/stage-3-work-units.test.js +29 -0
  150. package/node_modules/@ktx/context/dist/ingest/tools/emit-unmapped-fallback.tool.d.ts +1 -1
  151. package/node_modules/@ktx/context/dist/ingest/types.d.ts +24 -0
  152. package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.d.ts +24 -0
  153. package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.js +111 -0
  154. package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.test.d.ts +1 -0
  155. package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.test.js +138 -0
  156. package/node_modules/@ktx/context/dist/llm/claude-code-runtime.js +19 -2
  157. package/node_modules/@ktx/context/dist/llm/claude-code-runtime.test.js +33 -0
  158. package/node_modules/@ktx/context/dist/project/setup-config.d.ts +1 -1
  159. package/node_modules/@ktx/context/dist/project/setup-config.js +10 -1
  160. package/node_modules/@ktx/context/dist/project/setup-config.test.js +3 -2
  161. package/node_modules/@ktx/context/dist/sl/tools/sl-edit-source.tool.js +5 -1
  162. package/node_modules/@ktx/context/dist/sl/tools/sl-edit-source.tool.test.js +15 -0
  163. package/node_modules/@ktx/context/dist/sl/tools/sl-write-source.tool.js +5 -1
  164. package/node_modules/@ktx/context/dist/sl/tools/sl-write-source.tool.test.js +22 -0
  165. package/node_modules/@ktx/context/dist/tools/action-target-connection.d.ts +9 -0
  166. package/node_modules/@ktx/context/dist/tools/action-target-connection.js +14 -0
  167. package/node_modules/@ktx/context/dist/tools/context-candidate-write.tool.d.ts +4 -4
  168. package/node_modules/@ktx/context/dist/tools/index.d.ts +1 -0
  169. package/node_modules/@ktx/context/dist/tools/index.js +1 -0
  170. package/node_modules/@ktx/context/dist/wiki/local-knowledge.js +4 -1
  171. package/node_modules/@ktx/context/dist/wiki/local-knowledge.test.js +44 -0
  172. package/node_modules/@ktx/context/dist/wiki/tools/wiki-write.tool.js +3 -48
  173. package/node_modules/@ktx/context/dist/wiki/tools/wiki-write.tool.test.js +28 -0
  174. package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.d.ts +17 -0
  175. package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.js +79 -0
  176. package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.test.d.ts +1 -0
  177. package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.test.js +64 -0
  178. package/node_modules/@ktx/context/prompts/memory_agent_bundle_ingest_work_unit.md +23 -4
  179. package/node_modules/@ktx/context/skills/ingest_triage/SKILL.md +7 -3
  180. package/package.json +4 -4
@@ -0,0 +1,101 @@
1
+ import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { FileIngestTraceWriter } from '../ingest-trace.js';
6
+ import { resolveTextualConflict } from './textual-conflict-resolver.js';
7
+ async function makeHarness() {
8
+ const root = await mkdtemp(join(tmpdir(), 'ktx-textual-resolver-'));
9
+ const workdir = join(root, 'workdir');
10
+ const patchPath = join(root, 'failed.patch');
11
+ const trace = new FileIngestTraceWriter({
12
+ tracePath: join(root, 'trace.jsonl'),
13
+ jobId: 'job-1',
14
+ connectionId: 'warehouse',
15
+ sourceKey: 'metabase',
16
+ runId: 'run-1',
17
+ syncId: 'sync-1',
18
+ level: 'trace',
19
+ });
20
+ await mkdir(join(workdir, 'wiki/global'), { recursive: true });
21
+ await writeFile(join(workdir, 'wiki/global/account.md'), 'accepted line\n', 'utf-8');
22
+ await writeFile(patchPath, [
23
+ 'diff --git a/wiki/global/account.md b/wiki/global/account.md',
24
+ 'index 8877391..6f63f4d 100644',
25
+ '--- a/wiki/global/account.md',
26
+ '+++ b/wiki/global/account.md',
27
+ '@@ -1 +1 @@',
28
+ '-base line',
29
+ '+proposal line',
30
+ '',
31
+ ].join('\n'), 'utf-8');
32
+ return { root, workdir, patchPath, trace };
33
+ }
34
+ describe('resolveTextualConflict', () => {
35
+ it('lets the repair agent read the failed patch and write only touched paths', async () => {
36
+ const { workdir, patchPath, trace } = await makeHarness();
37
+ const agentRunner = {
38
+ runLoop: vi.fn(async (params) => {
39
+ const current = await params.toolSet.read_integration_file.execute({ path: 'wiki/global/account.md' });
40
+ expect(current.structured).toEqual({ path: 'wiki/global/account.md', exists: true });
41
+ expect(current.markdown).toContain('accepted line');
42
+ const patch = await params.toolSet.read_failed_patch.execute({});
43
+ expect(patch.markdown).toContain('proposal line');
44
+ await expect(params.toolSet.write_integration_file.execute({
45
+ path: 'wiki/global/not-allowed.md',
46
+ content: 'bad\n',
47
+ })).rejects.toThrow(/resolver path not allowed/);
48
+ await params.toolSet.write_integration_file.execute({
49
+ path: 'wiki/global/account.md',
50
+ content: 'accepted line\nproposal line\n',
51
+ });
52
+ return { stopReason: 'natural' };
53
+ }),
54
+ };
55
+ const result = await resolveTextualConflict({
56
+ agentRunner,
57
+ workdir,
58
+ unitKey: 'wu-a',
59
+ patchPath,
60
+ touchedPaths: ['wiki/global/account.md'],
61
+ trace,
62
+ reason: 'patch failed: wiki/global/account.md',
63
+ maxAttempts: 1,
64
+ stepBudget: 8,
65
+ });
66
+ expect(result).toEqual({
67
+ status: 'repaired',
68
+ attempts: 1,
69
+ changedPaths: ['wiki/global/account.md'],
70
+ });
71
+ await expect(readFile(join(workdir, 'wiki/global/account.md'), 'utf-8')).resolves.toBe('accepted line\nproposal line\n');
72
+ expect(agentRunner.runLoop).toHaveBeenCalledWith(expect.objectContaining({
73
+ modelRole: 'repair',
74
+ stepBudget: 8,
75
+ telemetryTags: expect.objectContaining({
76
+ operationName: 'ingest-isolated-diff-textual-resolver',
77
+ jobId: 'job-1',
78
+ unitKey: 'wu-a',
79
+ }),
80
+ }));
81
+ });
82
+ it('fails when the repair agent completes without editing any touched path', async () => {
83
+ const { workdir, patchPath, trace } = await makeHarness();
84
+ const result = await resolveTextualConflict({
85
+ agentRunner: { runLoop: vi.fn(async () => ({ stopReason: 'natural' })) },
86
+ workdir,
87
+ unitKey: 'wu-a',
88
+ patchPath,
89
+ touchedPaths: ['wiki/global/account.md'],
90
+ trace,
91
+ reason: 'patch failed: wiki/global/account.md',
92
+ maxAttempts: 1,
93
+ stepBudget: 8,
94
+ });
95
+ expect(result).toEqual({
96
+ status: 'failed',
97
+ attempts: 1,
98
+ reason: 'resolver completed without editing an allowed path',
99
+ });
100
+ });
101
+ });
@@ -0,0 +1,15 @@
1
+ import type { IngestSessionWorktree, IngestSessionWorktreePort } from '../ports.js';
2
+ import type { WorkUnit } from '../types.js';
3
+ import type { IngestTraceWriter } from '../ingest-trace.js';
4
+ import type { WorkUnitOutcome } from '../stages/stage-3-work-units.js';
5
+ export interface RunIsolatedWorkUnitInput {
6
+ unitIndex: number;
7
+ ingestionBaseSha: string;
8
+ sessionWorktreeService: IngestSessionWorktreePort;
9
+ patchDir: string;
10
+ trace: IngestTraceWriter;
11
+ workUnit: WorkUnit;
12
+ run(child: IngestSessionWorktree): Promise<WorkUnitOutcome>;
13
+ afterSuccess?(child: IngestSessionWorktree): Promise<void>;
14
+ }
15
+ export declare function runIsolatedWorkUnit(input: RunIsolatedWorkUnitInput): Promise<WorkUnitOutcome>;
@@ -0,0 +1,61 @@
1
+ import { mkdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { parsePatchTouchedPaths } from './git-patch.js';
4
+ function patchFileName(unitIndex, unitKey) {
5
+ const safeKey = unitKey.replace(/[^a-zA-Z0-9_.-]+/g, '-');
6
+ return `${String(unitIndex).padStart(4, '0')}-${safeKey}.patch`;
7
+ }
8
+ export async function runIsolatedWorkUnit(input) {
9
+ const sessionKey = `${input.trace.context.jobId}-${input.workUnit.unitKey}`;
10
+ let cleanupOutcome = 'crash';
11
+ const child = await input.sessionWorktreeService.create(sessionKey, input.ingestionBaseSha);
12
+ await input.trace.event('debug', 'work_unit', 'work_unit_child_created', {
13
+ unitKey: input.workUnit.unitKey,
14
+ unitIndex: input.unitIndex,
15
+ worktreePath: child.workdir,
16
+ baseSha: input.ingestionBaseSha,
17
+ });
18
+ try {
19
+ const outcome = await input.run(child);
20
+ if (outcome.status !== 'success') {
21
+ cleanupOutcome = 'success';
22
+ await input.trace.event('error', 'work_unit', 'work_unit_failed_before_patch', {
23
+ unitKey: input.workUnit.unitKey,
24
+ reason: outcome.reason ?? 'unknown failure',
25
+ });
26
+ return { ...outcome, childWorktreePath: child.workdir };
27
+ }
28
+ await input.afterSuccess?.(child);
29
+ await mkdir(input.patchDir, { recursive: true });
30
+ const patchPath = join(input.patchDir, patchFileName(input.unitIndex, input.workUnit.unitKey));
31
+ await child.git.writeBinaryNoRenamePatch(input.ingestionBaseSha, 'HEAD', patchPath);
32
+ const patch = await readFile(patchPath, 'utf-8');
33
+ const touched = parsePatchTouchedPaths(patch);
34
+ cleanupOutcome = 'success';
35
+ await input.trace.event('debug', 'work_unit', 'work_unit_patch_collected', {
36
+ unitKey: input.workUnit.unitKey,
37
+ patchPath,
38
+ touchedPaths: touched.map((entry) => entry.path),
39
+ patchBytes: Buffer.byteLength(patch),
40
+ });
41
+ return {
42
+ ...outcome,
43
+ patchPath,
44
+ patchTouchedPaths: touched.map((entry) => entry.path),
45
+ childWorktreePath: child.workdir,
46
+ };
47
+ }
48
+ catch (error) {
49
+ await input.trace.event('error', 'work_unit', 'work_unit_child_failed', { unitKey: input.workUnit.unitKey, worktreePath: child.workdir }, error);
50
+ cleanupOutcome = 'success';
51
+ throw error;
52
+ }
53
+ finally {
54
+ await input.sessionWorktreeService.cleanup(child, cleanupOutcome);
55
+ await input.trace.event('trace', 'work_unit', 'work_unit_child_cleanup', {
56
+ unitKey: input.workUnit.unitKey,
57
+ outcome: cleanupOutcome,
58
+ worktreePath: child.workdir,
59
+ });
60
+ }
61
+ }
@@ -0,0 +1,137 @@
1
+ import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { GitService } from '../../core/index.js';
6
+ import { FileIngestTraceWriter } from '../ingest-trace.js';
7
+ import { runIsolatedWorkUnit } from './work-unit-executor.js';
8
+ async function makeGit() {
9
+ const homeDir = await mkdtemp(join(tmpdir(), 'ktx-isolated-wu-'));
10
+ const configDir = join(homeDir, 'config');
11
+ const git = new GitService({
12
+ storage: { configDir, homeDir },
13
+ git: {
14
+ userName: 'System User',
15
+ userEmail: 'system@example.com',
16
+ bootstrapMessage: 'init',
17
+ bootstrapAuthor: 'system',
18
+ bootstrapAuthorEmail: 'system@example.com',
19
+ },
20
+ });
21
+ await git.onModuleInit();
22
+ await mkdir(join(configDir, 'raw-sources/c1/fake/s'), { recursive: true });
23
+ await writeFile(join(configDir, 'raw-sources/c1/fake/s/a.json'), '{}\n');
24
+ await git.commitFiles(['raw-sources/c1/fake/s/a.json'], 'raw snapshot', 'System User', 'system@example.com');
25
+ return { homeDir, configDir, git, baseSha: await git.revParseHead() };
26
+ }
27
+ describe('runIsolatedWorkUnit', () => {
28
+ it('creates a child worktree at the ingestion base and persists a patch proposal', async () => {
29
+ const { homeDir, git, baseSha } = await makeGit();
30
+ const childDir = join(homeDir, '.worktrees/session-job-1-wu-1');
31
+ const sessionWorktreeService = {
32
+ create: vi.fn(async (_key, startSha) => {
33
+ await mkdir(join(homeDir, '.worktrees'), { recursive: true });
34
+ await git.addWorktree(childDir, 'session/job-1-wu-1', startSha);
35
+ const childGit = git.forWorktree(childDir);
36
+ return {
37
+ chatId: 'job-1-wu-1',
38
+ workdir: childDir,
39
+ branch: 'session/job-1-wu-1',
40
+ baseSha: startSha,
41
+ createdAt: new Date(),
42
+ git: childGit,
43
+ config: {},
44
+ };
45
+ }),
46
+ cleanup: vi.fn(async () => undefined),
47
+ };
48
+ const tracePath = join(homeDir, '.ktx/ingest-traces/job-1/trace.jsonl');
49
+ const trace = new FileIngestTraceWriter({
50
+ tracePath,
51
+ jobId: 'job-1',
52
+ connectionId: 'c1',
53
+ sourceKey: 'fake',
54
+ level: 'trace',
55
+ });
56
+ const result = await runIsolatedWorkUnit({
57
+ unitIndex: 0,
58
+ ingestionBaseSha: baseSha,
59
+ sessionWorktreeService: sessionWorktreeService,
60
+ patchDir: join(homeDir, '.ktx/ingest-patches/job-1'),
61
+ trace,
62
+ run: async (child) => {
63
+ await mkdir(join(child.workdir, 'wiki/global'), { recursive: true });
64
+ await writeFile(join(child.workdir, 'wiki/global/a.md'), '---\nsummary: A\nusage_mode: auto\n---\n\nBody\n');
65
+ await child.git.commitFiles(['wiki/global/a.md'], 'test: write wiki', 'KTX Test', 'system@ktx.local');
66
+ return {
67
+ unitKey: 'wu-1',
68
+ status: 'success',
69
+ preSha: baseSha,
70
+ postSha: await child.git.revParseHead(),
71
+ actions: [{ target: 'wiki', type: 'created', key: 'a', detail: 'A' }],
72
+ touchedSlSources: [],
73
+ };
74
+ },
75
+ workUnit: { unitKey: 'wu-1', rawFiles: ['a.json'], peerFileIndex: [], dependencyPaths: [] },
76
+ });
77
+ expect(sessionWorktreeService.create).toHaveBeenCalledWith('job-1-wu-1', baseSha);
78
+ expect(sessionWorktreeService.cleanup).toHaveBeenCalledWith(expect.any(Object), 'success');
79
+ expect(result.status).toBe('success');
80
+ if (result.status !== 'success') {
81
+ throw new Error('expected successful work unit');
82
+ }
83
+ const patchPath = result.patchPath;
84
+ if (!patchPath) {
85
+ throw new Error('expected patch path');
86
+ }
87
+ expect(patchPath).toContain('0000-wu-1.patch');
88
+ await expect(readFile(patchPath, 'utf-8')).resolves.toContain('wiki/global/a.md');
89
+ await expect(readFile(tracePath, 'utf-8')).resolves.toContain('work_unit_child_created');
90
+ });
91
+ it('removes child worktrees after failed WorkUnit outcomes are traced', async () => {
92
+ const { homeDir, git, baseSha } = await makeGit();
93
+ const childDir = join(homeDir, '.worktrees/session-job-1-wu-fail');
94
+ const sessionWorktreeService = {
95
+ create: vi.fn(async (_key, startSha) => {
96
+ await mkdir(join(homeDir, '.worktrees'), { recursive: true });
97
+ await git.addWorktree(childDir, 'session/job-1-wu-fail', startSha);
98
+ return {
99
+ chatId: 'job-1-wu-fail',
100
+ workdir: childDir,
101
+ branch: 'session/job-1-wu-fail',
102
+ baseSha: startSha,
103
+ createdAt: new Date(),
104
+ git: git.forWorktree(childDir),
105
+ config: {},
106
+ };
107
+ }),
108
+ cleanup: vi.fn(async () => undefined),
109
+ };
110
+ const trace = new FileIngestTraceWriter({
111
+ tracePath: join(homeDir, '.ktx/ingest-traces/job-1/trace.jsonl'),
112
+ jobId: 'job-1',
113
+ connectionId: 'c1',
114
+ sourceKey: 'fake',
115
+ level: 'trace',
116
+ });
117
+ const result = await runIsolatedWorkUnit({
118
+ unitIndex: 0,
119
+ ingestionBaseSha: baseSha,
120
+ sessionWorktreeService: sessionWorktreeService,
121
+ patchDir: join(homeDir, '.ktx/ingest-patches/job-1'),
122
+ trace,
123
+ run: async () => ({
124
+ unitKey: 'wu-fail',
125
+ status: 'failed',
126
+ reason: 'agent loop errored',
127
+ preSha: baseSha,
128
+ postSha: baseSha,
129
+ actions: [],
130
+ touchedSlSources: [],
131
+ }),
132
+ workUnit: { unitKey: 'wu-fail', rawFiles: ['a.json'], peerFileIndex: [], dependencyPaths: [] },
133
+ });
134
+ expect(result.status).toBe('failed');
135
+ expect(sessionWorktreeService.cleanup).toHaveBeenCalledWith(expect.any(Object), 'success');
136
+ });
137
+ });
@@ -596,6 +596,13 @@ describe('canonical local ingest', () => {
596
596
  ],
597
597
  },
598
598
  });
599
+ expect(result.report.body.isolatedDiff).toMatchObject({
600
+ enabled: true,
601
+ acceptedPatches: 0,
602
+ projectionSha: expect.any(String),
603
+ });
604
+ const projectedSourcePath = join(metricflowProject.projectDir, 'semantic-layer/warehouse/orders.yaml');
605
+ await expect(readFile(projectedSourcePath, 'utf-8')).resolves.toContain('name: orders');
599
606
  const stagedRawPath = join(metricflowProject.projectDir, 'raw-sources', 'warehouse', 'metricflow', result.report.body.syncId, 'models', 'orders.yml');
600
607
  await expect(readFile(stagedRawPath, 'utf-8')).resolves.toContain('semantic_models:');
601
608
  });
@@ -16,6 +16,7 @@ import { createEmitHistoricSqlEvidenceTool } from './adapters/historic-sql/evide
16
16
  import { HistoricSqlProjectionPostProcessor } from './adapters/historic-sql/post-processor.js';
17
17
  import { ContextEvidenceIndexService, SqliteContextEvidenceStore } from './context-evidence/index.js';
18
18
  import { DiffSetService } from './diff-set.service.js';
19
+ import { ingestTracePathForJob } from './ingest-trace.js';
19
20
  import { IngestBundleRunner } from './ingest-bundle.runner.js';
20
21
  import { PageTriageService } from './page-triage/index.js';
21
22
  import { createWarehouseVerificationTools } from './tools/warehouse-verification/index.js';
@@ -25,6 +26,11 @@ const promptsDir = fileURLToPath(new URL('../../prompts', import.meta.url));
25
26
  const skillsDir = fileURLToPath(new URL('../../skills', import.meta.url));
26
27
  const LOCAL_AUTHOR = { name: 'KTX Local', email: 'local@ktx.local' };
27
28
  const LOCAL_SHAPE_WARNING = 'Local ingest validates semantic-layer YAML shape only.';
29
+ const INGEST_TRACE_LEVELS = new Set(['error', 'info', 'debug', 'trace']);
30
+ function ingestTraceLevelFromEnv(env = process.env) {
31
+ const raw = env.KTX_INGEST_TRACE_LEVEL;
32
+ return raw && INGEST_TRACE_LEVELS.has(raw) ? raw : 'debug';
33
+ }
28
34
  class NoopEmbeddingPort {
29
35
  maxBatchSize = 64;
30
36
  async computeEmbedding() {
@@ -51,6 +57,9 @@ class LocalIngestStorage {
51
57
  resolveTranscriptDir(jobId) {
52
58
  return join(this.project.projectDir, '.ktx/ingest-transcripts', jobId);
53
59
  }
60
+ resolveTracePath(jobId) {
61
+ return ingestTracePathForJob(this.homeDir, jobId);
62
+ }
54
63
  }
55
64
  class LocalIngestLock {
56
65
  async withLock(_key, fn) {
@@ -131,21 +140,55 @@ class LocalSlPythonPort {
131
140
  }
132
141
  }
133
142
  class LocalShapeOnlySlValidator {
143
+ validateParsedSource(sourceName, parsed) {
144
+ const isOverlay = parsed.table == null && parsed.sql == null;
145
+ const result = (isOverlay ? sourceOverlaySchema : sourceDefinitionSchema).safeParse(parsed);
146
+ return result.success
147
+ ? { errors: [], warnings: [LOCAL_SHAPE_WARNING] }
148
+ : {
149
+ errors: result.error.issues.map((issue) => `${sourceName}: ${issue.path.join('.') || 'source'} ${issue.message}`),
150
+ warnings: [],
151
+ };
152
+ }
153
+ async validateComposedSource(deps, connectionId, sourceName, readError) {
154
+ try {
155
+ const { sources, loadErrors } = await deps.semanticLayerService.loadAllSources(connectionId);
156
+ const source = sources.find((candidate) => candidate.name === sourceName);
157
+ if (source) {
158
+ return this.validateParsedSource(sourceName, source);
159
+ }
160
+ const detail = loadErrors.length > 0
161
+ ? loadErrors.join('; ')
162
+ : readError instanceof Error
163
+ ? readError.message
164
+ : String(readError);
165
+ return { errors: [`${sourceName}: ${detail}`], warnings: [] };
166
+ }
167
+ catch (fallbackError) {
168
+ return {
169
+ errors: [`${sourceName}: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`],
170
+ warnings: [],
171
+ };
172
+ }
173
+ }
134
174
  async validateSingleSource(deps, connectionId, sourceName) {
175
+ let content;
135
176
  try {
136
177
  const file = await deps.semanticLayerService.readSourceFile(connectionId, sourceName);
137
- const parsed = YAML.parse(file.content);
138
- const isOverlay = parsed.table == null && parsed.sql == null;
139
- const result = (isOverlay ? sourceOverlaySchema : sourceDefinitionSchema).safeParse(parsed);
140
- return result.success
141
- ? { errors: [], warnings: [LOCAL_SHAPE_WARNING] }
142
- : {
143
- errors: result.error.issues.map((issue) => `${sourceName}: ${issue.path.join('.') || 'source'} ${issue.message}`),
144
- warnings: [],
145
- };
178
+ content = file.content;
179
+ }
180
+ catch (error) {
181
+ return this.validateComposedSource(deps, connectionId, sourceName, error);
182
+ }
183
+ try {
184
+ const parsed = YAML.parse(content);
185
+ return this.validateParsedSource(sourceName, parsed);
146
186
  }
147
187
  catch (error) {
148
- return { errors: [`${sourceName}: ${error instanceof Error ? error.message : String(error)}`], warnings: [] };
188
+ return {
189
+ errors: [`${sourceName}: invalid YAML — ${error instanceof Error ? error.message : String(error)}`],
190
+ warnings: [],
191
+ };
149
192
  }
150
193
  }
151
194
  }
@@ -494,6 +537,7 @@ export function createLocalBundleIngestRuntime(options) {
494
537
  workUnitMaxConcurrency: options.project.config.ingest.workUnits.maxConcurrency,
495
538
  workUnitStepBudget: options.project.config.ingest.workUnits.stepBudget,
496
539
  workUnitFailureMode: options.project.config.ingest.workUnits.failureMode,
540
+ ingestTraceLevel: ingestTraceLevelFromEnv(),
497
541
  },
498
542
  skillsRegistry: new SkillsRegistryService({ skillsDir, logger }),
499
543
  promptService,
@@ -102,6 +102,53 @@ describe('createLocalBundleIngestRuntime', () => {
102
102
  { id: 'bq', name: 'bq', connectionType: 'BIGQUERY' },
103
103
  ]);
104
104
  });
105
+ it('validates manifest-backed scan sources during local ingest gates', async () => {
106
+ await project.fileStore.writeFile('semantic-layer/warehouse/_schema/public.yaml', [
107
+ 'tables:',
108
+ ' payments:',
109
+ ' table: public.payments',
110
+ ' columns:',
111
+ ' - name: payment_id',
112
+ ' type: string',
113
+ ' - name: amount',
114
+ ' type: number',
115
+ '',
116
+ ].join('\n'), 'ktx', 'ktx@example.com', 'Add warehouse manifest');
117
+ const agentRunner = testAgentRunner();
118
+ const runtime = createLocalBundleIngestRuntime({
119
+ project,
120
+ adapters: [new FakeSourceAdapter()],
121
+ agentRunner,
122
+ });
123
+ const deps = runtime.runner.deps;
124
+ await expect(deps.slValidator.validateSingleSource(deps, 'warehouse', 'payments')).resolves.toEqual({
125
+ errors: [],
126
+ warnings: expect.any(Array),
127
+ });
128
+ });
129
+ it('does not mask malformed direct overlays with manifest-backed fallback validation', async () => {
130
+ await project.fileStore.writeFile('semantic-layer/warehouse/_schema/public.yaml', [
131
+ 'tables:',
132
+ ' payments:',
133
+ ' table: public.payments',
134
+ ' columns:',
135
+ ' - name: payment_id',
136
+ ' type: string',
137
+ '',
138
+ ].join('\n'), 'ktx', 'ktx@example.com', 'Add warehouse manifest');
139
+ await project.fileStore.writeFile('semantic-layer/warehouse/payments.yaml', ['name: payments', 'columns:', ' - [', ''].join('\n'), 'ktx', 'ktx@example.com', 'Add malformed overlay');
140
+ const agentRunner = testAgentRunner();
141
+ const runtime = createLocalBundleIngestRuntime({
142
+ project,
143
+ adapters: [new FakeSourceAdapter()],
144
+ agentRunner,
145
+ });
146
+ const deps = runtime.runner.deps;
147
+ await expect(deps.slValidator.validateSingleSource(deps, 'warehouse', 'payments')).resolves.toEqual({
148
+ errors: [expect.stringContaining('invalid YAML')],
149
+ warnings: [],
150
+ });
151
+ });
105
152
  it('passes project connection config to local ingest query executors', async () => {
106
153
  const agentRunner = testAgentRunner();
107
154
  const queryExecutor = {
@@ -130,6 +177,24 @@ describe('createLocalBundleIngestRuntime', () => {
130
177
  sql: 'select 1',
131
178
  });
132
179
  });
180
+ it('defaults local bundle ingest to isolated diffs without a shared-worktree fallback setting', () => {
181
+ const runtime = createLocalBundleIngestRuntime({
182
+ project,
183
+ adapters: [new FakeSourceAdapter()],
184
+ agentRunner: testAgentRunner(),
185
+ });
186
+ const settings = runtime.runner.deps.settings;
187
+ const fallbackSettingKey = ['sharedWorktree', 'SourceKeys'].join('');
188
+ expect(settings).not.toHaveProperty(fallbackSettingKey);
189
+ expect(Object.keys(settings).sort()).toEqual([
190
+ 'ingestTraceLevel',
191
+ 'memoryIngestionModel',
192
+ 'probeRowCount',
193
+ 'workUnitFailureMode',
194
+ 'workUnitMaxConcurrency',
195
+ 'workUnitStepBudget',
196
+ ]);
197
+ });
133
198
  it('accepts a debug LLM request file when constructing the default agent runner', async () => {
134
199
  await writeFile(join(project.projectDir, 'ktx.yaml'), [
135
200
  'connections:',
@@ -2,8 +2,8 @@ import * as z from 'zod';
2
2
  import type { MemoryFlowReplayInput } from './types.js';
3
3
  export declare const memoryFlowRunStatusSchema: z.ZodEnum<{
4
4
  error: "error";
5
- running: "running";
6
5
  done: "done";
6
+ running: "running";
7
7
  }>;
8
8
  export declare const memoryFlowEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
9
9
  type: z.ZodLiteral<"source_acquired">;
@@ -42,15 +42,33 @@ export declare const memoryFlowEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject
42
42
  type: z.ZodLiteral<"stage_skipped">;
43
43
  stage: z.ZodEnum<{
44
44
  source: "source";
45
- chunks: "chunks";
46
45
  workUnits: "workUnits";
47
46
  actions: "actions";
47
+ chunks: "chunks";
48
48
  gates: "gates";
49
49
  saved: "saved";
50
50
  }>;
51
51
  reason: z.ZodString;
52
52
  } & {
53
53
  emittedAt: z.ZodOptional<z.ZodString>;
54
+ }, z.core.$strip>, z.ZodObject<{
55
+ type: z.ZodLiteral<"stage_progress">;
56
+ stage: z.ZodEnum<{
57
+ source: "source";
58
+ report: "report";
59
+ integration: "integration";
60
+ reconciliation: "reconciliation";
61
+ post_processor: "post_processor";
62
+ wiki_sl_ref_repair: "wiki_sl_ref_repair";
63
+ final_gates: "final_gates";
64
+ save: "save";
65
+ provenance: "provenance";
66
+ }>;
67
+ percent: z.ZodNumber;
68
+ message: z.ZodString;
69
+ transient: z.ZodOptional<z.ZodBoolean>;
70
+ } & {
71
+ emittedAt: z.ZodOptional<z.ZodString>;
54
72
  }, z.core.$strip>, z.ZodObject<{
55
73
  type: z.ZodLiteral<"work_unit_started">;
56
74
  unitKey: z.ZodString;
@@ -125,8 +143,8 @@ export declare const memoryFlowActionDetailSchema: z.ZodObject<{
125
143
  }>;
126
144
  action: z.ZodEnum<{
127
145
  created: "created";
128
- updated: "updated";
129
146
  removed: "removed";
147
+ updated: "updated";
130
148
  }>;
131
149
  key: z.ZodString;
132
150
  summary: z.ZodString;
@@ -145,8 +163,8 @@ export declare const memoryFlowDetailSectionsSchema: z.ZodObject<{
145
163
  }>;
146
164
  action: z.ZodEnum<{
147
165
  created: "created";
148
- updated: "updated";
149
166
  removed: "removed";
167
+ updated: "updated";
150
168
  }>;
151
169
  key: z.ZodString;
152
170
  summary: z.ZodString;
@@ -181,8 +199,8 @@ export declare const memoryFlowStreamEventSchema: z.ZodDiscriminatedUnion<[z.Zod
181
199
  type: z.ZodLiteral<"closed">;
182
200
  status: z.ZodEnum<{
183
201
  error: "error";
184
- running: "running";
185
202
  done: "done";
203
+ running: "running";
186
204
  }>;
187
205
  errors: z.ZodArray<z.ZodString>;
188
206
  }, z.core.$strip>], "type">;
@@ -47,6 +47,23 @@ export const memoryFlowEventSchema = z.discriminatedUnion('type', [
47
47
  stage: z.enum(['source', 'chunks', 'workUnits', 'actions', 'gates', 'saved']),
48
48
  reason: z.string().min(1),
49
49
  }),
50
+ eventSchema({
51
+ type: z.literal('stage_progress'),
52
+ stage: z.enum([
53
+ 'source',
54
+ 'integration',
55
+ 'reconciliation',
56
+ 'post_processor',
57
+ 'wiki_sl_ref_repair',
58
+ 'final_gates',
59
+ 'save',
60
+ 'provenance',
61
+ 'report',
62
+ ]),
63
+ percent: z.number().min(0).max(100),
64
+ message: z.string().min(1),
65
+ transient: z.boolean().optional(),
66
+ }),
50
67
  eventSchema({
51
68
  type: z.literal('work_unit_started'),
52
69
  unitKey: z.string().min(1),
@@ -15,6 +15,7 @@ function snapshot(overrides = {}) {
15
15
  { type: 'raw_snapshot_written', syncId: 'sync-1', rawFileCount: 2 },
16
16
  { type: 'diff_computed', added: 1, modified: 1, deleted: 0, unchanged: 0 },
17
17
  { type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
18
+ { type: 'stage_progress', stage: 'integration', percent: 80, message: 'Integrating 1/1 patches: orders' },
18
19
  { type: 'work_unit_started', unitKey: 'orders', skills: ['wiki_capture'], stepBudget: 40 },
19
20
  { type: 'work_unit_step', unitKey: 'orders', stepIndex: 1, stepBudget: 40 },
20
21
  { type: 'candidate_action', unitKey: 'orders', target: 'wiki', action: 'created', key: 'wiki/orders.md' },
@@ -38,6 +38,12 @@ type MemoryFlowEventPayload = {
38
38
  type: 'stage_skipped';
39
39
  stage: MemoryFlowColumnId;
40
40
  reason: string;
41
+ } | {
42
+ type: 'stage_progress';
43
+ stage: 'source' | 'integration' | 'reconciliation' | 'post_processor' | 'wiki_sl_ref_repair' | 'final_gates' | 'save' | 'provenance' | 'report';
44
+ percent: number;
45
+ message: string;
46
+ transient?: boolean;
41
47
  } | {
42
48
  type: 'work_unit_started';
43
49
  unitKey: string;
@@ -8,9 +8,9 @@ export declare const parsedTargetTableSchema: z.ZodDiscriminatedUnion<[z.ZodObje
8
8
  }, z.core.$strip>, z.ZodObject<{
9
9
  ok: z.ZodLiteral<false>;
10
10
  reason: z.ZodEnum<{
11
+ no_connection_mapping: "no_connection_mapping";
11
12
  looker_template_unresolved: "looker_template_unresolved";
12
13
  derived_table_not_supported: "derived_table_not_supported";
13
- no_connection_mapping: "no_connection_mapping";
14
14
  no_physical_table: "no_physical_table";
15
15
  multiple_table_references: "multiple_table_references";
16
16
  unsupported_dialect: "unsupported_dialect";