@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
@@ -9,6 +9,7 @@ import type { SemanticLayerService, SlConnectionCatalogPort, SlSearchService, Sl
9
9
  import type { ToolContext, ToolSession, TouchedSlSource } from '../tools/index.js';
10
10
  import type { KnowledgeIndexPort, KnowledgeWikiService } from '../wiki/index.js';
11
11
  import type { CanonicalPin } from './canonical-pins.js';
12
+ import type { IngestTraceLevel } from './ingest-trace.js';
12
13
  import type { IngestReportSnapshot } from './reports.js';
13
14
  import type { ReconcileCandidateForPrompt, ReconcileCandidateSummary, ReconcilePromptRunState } from './stages/build-reconcile-context.js';
14
15
  import type { ReconciliationOutcome } from './stages/stage-4-reconciliation.js';
@@ -101,6 +102,7 @@ export interface IngestSettingsPort {
101
102
  workUnitMaxConcurrency?: number;
102
103
  workUnitStepBudget?: number;
103
104
  workUnitFailureMode?: 'abort' | 'continue';
105
+ ingestTraceLevel?: IngestTraceLevel;
104
106
  }
105
107
  export interface IngestGitAuthor {
106
108
  name: string;
@@ -112,6 +114,7 @@ export interface IngestStoragePort {
112
114
  resolveUploadDir(uploadId: string): string;
113
115
  resolvePullDir(jobId: string): string;
114
116
  resolveTranscriptDir(jobId: string): string;
117
+ resolveTracePath(jobId: string): string;
115
118
  }
116
119
  export interface IngestCommitMessagePort {
117
120
  enqueueForExternalCommit(args: {
@@ -8,6 +8,10 @@ export declare const ingestReportSnapshotSchema: z.ZodObject<{
8
8
  sourceKey: z.ZodString;
9
9
  createdAt: z.ZodString;
10
10
  body: z.ZodObject<{
11
+ status: z.ZodOptional<z.ZodEnum<{
12
+ failed: "failed";
13
+ completed: "completed";
14
+ }>>;
11
15
  syncId: z.ZodString;
12
16
  diffSummary: z.ZodObject<{
13
17
  added: z.ZodNumber;
@@ -34,10 +38,10 @@ export declare const ingestReportSnapshotSchema: z.ZodObject<{
34
38
  retryRecommended: z.ZodBoolean;
35
39
  kind: z.ZodOptional<z.ZodEnum<{
36
40
  lookml_connection_mismatch: "lookml_connection_mismatch";
37
- unmapped_looker_connection: "unmapped_looker_connection";
38
- unparseable_sql_table_name: "unparseable_sql_table_name";
39
41
  looker_template_unresolved: "looker_template_unresolved";
40
42
  derived_table_not_supported: "derived_table_not_supported";
43
+ unmapped_looker_connection: "unmapped_looker_connection";
44
+ unparseable_sql_table_name: "unparseable_sql_table_name";
41
45
  }>>;
42
46
  details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
43
47
  }, z.core.$strip>>>;
@@ -54,15 +58,36 @@ export declare const ingestReportSnapshotSchema: z.ZodObject<{
54
58
  retryRecommended: z.ZodBoolean;
55
59
  kind: z.ZodOptional<z.ZodEnum<{
56
60
  lookml_connection_mismatch: "lookml_connection_mismatch";
57
- unmapped_looker_connection: "unmapped_looker_connection";
58
- unparseable_sql_table_name: "unparseable_sql_table_name";
59
61
  looker_template_unresolved: "looker_template_unresolved";
60
62
  derived_table_not_supported: "derived_table_not_supported";
63
+ unmapped_looker_connection: "unmapped_looker_connection";
64
+ unparseable_sql_table_name: "unparseable_sql_table_name";
61
65
  }>>;
62
66
  details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
63
67
  }, z.core.$strip>>>;
64
68
  }, z.core.$strip>>;
65
69
  commitSha: z.ZodNullable<z.ZodString>;
70
+ tracePath: z.ZodOptional<z.ZodString>;
71
+ failure: z.ZodOptional<z.ZodObject<{
72
+ phase: z.ZodString;
73
+ message: z.ZodString;
74
+ details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
75
+ }, z.core.$strip>>;
76
+ isolatedDiff: z.ZodOptional<z.ZodObject<{
77
+ enabled: z.ZodBoolean;
78
+ integrationWorktreePath: z.ZodOptional<z.ZodString>;
79
+ ingestionBaseSha: z.ZodOptional<z.ZodString>;
80
+ projectionSha: z.ZodOptional<z.ZodNullable<z.ZodString>>;
81
+ acceptedPatches: z.ZodNumber;
82
+ textualConflicts: z.ZodNumber;
83
+ semanticConflicts: z.ZodNumber;
84
+ resolverAttempts: z.ZodDefault<z.ZodNumber>;
85
+ resolverRepairs: z.ZodDefault<z.ZodNumber>;
86
+ resolverFailures: z.ZodDefault<z.ZodNumber>;
87
+ gateRepairAttempts: z.ZodDefault<z.ZodNumber>;
88
+ gateRepairs: z.ZodDefault<z.ZodNumber>;
89
+ gateRepairFailures: z.ZodDefault<z.ZodNumber>;
90
+ }, z.core.$strip>>;
66
91
  workUnits: z.ZodArray<z.ZodObject<{
67
92
  unitKey: z.ZodString;
68
93
  rawFiles: z.ZodArray<z.ZodString>;
@@ -78,8 +103,8 @@ export declare const ingestReportSnapshotSchema: z.ZodObject<{
78
103
  }>;
79
104
  type: z.ZodEnum<{
80
105
  created: "created";
81
- updated: "updated";
82
106
  removed: "removed";
107
+ updated: "updated";
83
108
  }>;
84
109
  key: z.ZodString;
85
110
  detail: z.ZodString;
@@ -104,8 +129,8 @@ export declare const ingestReportSnapshotSchema: z.ZodObject<{
104
129
  }>;
105
130
  type: z.ZodEnum<{
106
131
  created: "created";
107
- updated: "updated";
108
132
  removed: "removed";
133
+ updated: "updated";
109
134
  }>;
110
135
  key: z.ZodString;
111
136
  detail: z.ZodString;
@@ -138,9 +163,9 @@ export declare const ingestReportSnapshotSchema: z.ZodObject<{
138
163
  unmappedFallbacks: z.ZodDefault<z.ZodArray<z.ZodObject<{
139
164
  rawPath: z.ZodString;
140
165
  reason: z.ZodEnum<{
166
+ no_connection_mapping: "no_connection_mapping";
141
167
  looker_template_unresolved: "looker_template_unresolved";
142
168
  derived_table_not_supported: "derived_table_not_supported";
143
- no_connection_mapping: "no_connection_mapping";
144
169
  no_physical_table: "no_physical_table";
145
170
  multiple_table_references: "multiple_table_references";
146
171
  unsupported_dialect: "unsupported_dialect";
@@ -109,6 +109,11 @@ const sourceFetchReportSchema = z.object({
109
109
  skipped: z.array(sourceFetchIssueSchema).default([]),
110
110
  warnings: z.array(sourceFetchIssueSchema).default([]),
111
111
  });
112
+ const ingestReportFailureSchema = z.object({
113
+ phase: z.string().min(1),
114
+ message: z.string().min(1),
115
+ details: z.record(z.string(), z.unknown()).optional(),
116
+ });
112
117
  export const ingestReportSnapshotSchema = z
113
118
  .object({
114
119
  id: z.string().min(1),
@@ -119,10 +124,30 @@ export const ingestReportSnapshotSchema = z
119
124
  createdAt: z.string().min(1),
120
125
  body: z
121
126
  .object({
127
+ status: z.enum(['completed', 'failed']).optional(),
122
128
  syncId: z.string().min(1),
123
129
  diffSummary: ingestDiffSummarySchema,
124
130
  fetch: sourceFetchReportSchema.optional(),
125
131
  commitSha: z.string().nullable(),
132
+ tracePath: z.string().optional(),
133
+ failure: ingestReportFailureSchema.optional(),
134
+ isolatedDiff: z
135
+ .object({
136
+ enabled: z.boolean(),
137
+ integrationWorktreePath: z.string().optional(),
138
+ ingestionBaseSha: z.string().optional(),
139
+ projectionSha: z.string().nullable().optional(),
140
+ acceptedPatches: z.number().int().min(0),
141
+ textualConflicts: z.number().int().min(0),
142
+ semanticConflicts: z.number().int().min(0),
143
+ resolverAttempts: z.number().int().min(0).default(0),
144
+ resolverRepairs: z.number().int().min(0).default(0),
145
+ resolverFailures: z.number().int().min(0).default(0),
146
+ gateRepairAttempts: z.number().int().min(0).default(0),
147
+ gateRepairs: z.number().int().min(0).default(0),
148
+ gateRepairFailures: z.number().int().min(0).default(0),
149
+ })
150
+ .optional(),
126
151
  workUnits: z.array(z.object({
127
152
  unitKey: z.string().min(1),
128
153
  rawFiles: z.array(z.string()),
@@ -194,6 +194,45 @@ describe('parseIngestReportSnapshot', () => {
194
194
  expect(snapshot.body.provenanceRows).toEqual([]);
195
195
  expect(snapshot.body.toolTranscripts).toEqual([]);
196
196
  });
197
+ it('parses failed ingest reports with trace and failure details', () => {
198
+ const snapshot = parseIngestReportSnapshot({
199
+ id: 'report-failed',
200
+ runId: 'run-failed',
201
+ jobId: 'job-failed',
202
+ connectionId: 'warehouse',
203
+ sourceKey: 'metabase',
204
+ createdAt: '2026-05-17T12:00:00.000Z',
205
+ body: {
206
+ status: 'failed',
207
+ syncId: 'sync-failed',
208
+ diffSummary: { added: 1, modified: 0, deleted: 0, unchanged: 0 },
209
+ commitSha: null,
210
+ tracePath: '/project/.ktx/ingest-traces/job-failed/trace.jsonl',
211
+ failure: {
212
+ phase: 'final_gates',
213
+ message: 'final artifact gates failed',
214
+ },
215
+ workUnits: [],
216
+ failedWorkUnits: [],
217
+ reconciliationSkipped: true,
218
+ conflictsResolved: [],
219
+ evictionsApplied: [],
220
+ unmappedFallbacks: [],
221
+ evictionInputs: [],
222
+ unresolvedCards: [],
223
+ supersededBy: null,
224
+ overrideOf: null,
225
+ provenanceRows: [],
226
+ toolTranscripts: [],
227
+ },
228
+ });
229
+ expect(snapshot.body.status).toBe('failed');
230
+ expect(snapshot.body.failure).toEqual({
231
+ phase: 'final_gates',
232
+ message: 'final artifact gates failed',
233
+ });
234
+ expect(snapshot.body.tracePath).toContain('trace.jsonl');
235
+ });
197
236
  it('rejects malformed report snapshots with a concise message', () => {
198
237
  const report = validReportSnapshot();
199
238
  report.body.workUnits[0] = {
@@ -202,4 +241,89 @@ describe('parseIngestReportSnapshot', () => {
202
241
  };
203
242
  expect(() => parseIngestReportSnapshot(report)).toThrow('Invalid ingest report snapshot');
204
243
  });
244
+ it('parses isolated-diff textual resolver counters', () => {
245
+ const snapshot = parseIngestReportSnapshot({
246
+ id: 'report-1',
247
+ runId: 'run-1',
248
+ jobId: 'job-1',
249
+ connectionId: 'warehouse',
250
+ sourceKey: 'metabase',
251
+ createdAt: '2026-05-18T00:00:00.000Z',
252
+ body: {
253
+ status: 'completed',
254
+ syncId: 'sync-1',
255
+ diffSummary: { added: 0, modified: 1, deleted: 0, unchanged: 0 },
256
+ commitSha: 'abc123',
257
+ isolatedDiff: {
258
+ enabled: true,
259
+ acceptedPatches: 2,
260
+ textualConflicts: 1,
261
+ semanticConflicts: 0,
262
+ resolverAttempts: 1,
263
+ resolverRepairs: 1,
264
+ resolverFailures: 0,
265
+ },
266
+ workUnits: [],
267
+ failedWorkUnits: [],
268
+ reconciliationSkipped: true,
269
+ conflictsResolved: [],
270
+ evictionsApplied: [],
271
+ unmappedFallbacks: [],
272
+ artifactResolutions: [],
273
+ evictionInputs: [],
274
+ unresolvedCards: [],
275
+ supersededBy: null,
276
+ overrideOf: null,
277
+ provenanceRows: [],
278
+ toolTranscripts: [],
279
+ },
280
+ });
281
+ expect(snapshot.body.isolatedDiff).toMatchObject({
282
+ resolverAttempts: 1,
283
+ resolverRepairs: 1,
284
+ resolverFailures: 0,
285
+ });
286
+ });
287
+ it('parses isolated-diff gate repair counters', () => {
288
+ const snapshot = parseIngestReportSnapshot({
289
+ id: 'report-1',
290
+ runId: 'run-1',
291
+ jobId: 'job-1',
292
+ connectionId: 'warehouse',
293
+ sourceKey: 'metabase',
294
+ createdAt: '2026-05-18T00:00:00.000Z',
295
+ body: {
296
+ status: 'completed',
297
+ syncId: 'sync-1',
298
+ diffSummary: { added: 1, modified: 0, deleted: 0, unchanged: 0 },
299
+ commitSha: 'abc123',
300
+ isolatedDiff: {
301
+ enabled: true,
302
+ acceptedPatches: 1,
303
+ textualConflicts: 0,
304
+ semanticConflicts: 1,
305
+ gateRepairAttempts: 1,
306
+ gateRepairs: 1,
307
+ gateRepairFailures: 0,
308
+ },
309
+ workUnits: [],
310
+ failedWorkUnits: [],
311
+ reconciliationSkipped: true,
312
+ conflictsResolved: [],
313
+ evictionsApplied: [],
314
+ unmappedFallbacks: [],
315
+ evictionInputs: [],
316
+ unresolvedCards: [],
317
+ supersededBy: null,
318
+ overrideOf: null,
319
+ provenanceRows: [],
320
+ toolTranscripts: [],
321
+ },
322
+ });
323
+ expect(snapshot.body.isolatedDiff).toMatchObject({
324
+ gateRepairAttempts: 1,
325
+ gateRepairs: 1,
326
+ gateRepairFailures: 0,
327
+ });
328
+ });
205
329
  });
@@ -37,11 +37,34 @@ export interface IngestReportPostProcessorOutcome {
37
37
  warnings: string[];
38
38
  touchedSources: TouchedSlSource[];
39
39
  }
40
+ export interface IngestReportFailure {
41
+ phase: string;
42
+ message: string;
43
+ details?: Record<string, unknown>;
44
+ }
40
45
  export interface IngestReportBody {
46
+ status?: 'completed' | 'failed';
41
47
  syncId: string;
42
48
  diffSummary: IngestDiffSummary;
43
49
  fetch?: SourceFetchReport;
44
50
  commitSha: string | null;
51
+ tracePath?: string;
52
+ failure?: IngestReportFailure;
53
+ isolatedDiff?: {
54
+ enabled: boolean;
55
+ integrationWorktreePath?: string;
56
+ ingestionBaseSha?: string;
57
+ projectionSha?: string | null;
58
+ acceptedPatches: number;
59
+ textualConflicts: number;
60
+ semanticConflicts: number;
61
+ resolverAttempts?: number;
62
+ resolverRepairs?: number;
63
+ resolverFailures?: number;
64
+ gateRepairAttempts?: number;
65
+ gateRepairs?: number;
66
+ gateRepairFailures?: number;
67
+ };
45
68
  workUnits: IngestReportWorkUnit[];
46
69
  failedWorkUnits: string[];
47
70
  reconciliationSkipped: boolean;
@@ -0,0 +1,11 @@
1
+ export interface SemanticLayerTargetPolicyInput {
2
+ paths: readonly string[];
3
+ allowedConnectionIds: ReadonlySet<string>;
4
+ }
5
+ export interface SemanticLayerTargetPolicyViolation {
6
+ path: string;
7
+ connectionId: string;
8
+ }
9
+ export declare function semanticLayerConnectionIdFromPath(path: string): string | null;
10
+ export declare function findDisallowedSemanticLayerTargetPaths(input: SemanticLayerTargetPolicyInput): SemanticLayerTargetPolicyViolation[];
11
+ export declare function assertSemanticLayerTargetPathsAllowed(input: SemanticLayerTargetPolicyInput): void;
@@ -0,0 +1,26 @@
1
+ export function semanticLayerConnectionIdFromPath(path) {
2
+ const normalized = path.replace(/^[ab]\//, '');
3
+ const match = /^semantic-layer\/([^/]+)\//.exec(normalized);
4
+ return match?.[1] ?? null;
5
+ }
6
+ export function findDisallowedSemanticLayerTargetPaths(input) {
7
+ return input.paths
8
+ .map((path) => ({ path, connectionId: semanticLayerConnectionIdFromPath(path) }))
9
+ .filter((entry) => {
10
+ return entry.connectionId !== null && !input.allowedConnectionIds.has(entry.connectionId);
11
+ })
12
+ .sort((left, right) => {
13
+ const byConnection = left.connectionId.localeCompare(right.connectionId);
14
+ return byConnection === 0 ? left.path.localeCompare(right.path) : byConnection;
15
+ });
16
+ }
17
+ export function assertSemanticLayerTargetPathsAllowed(input) {
18
+ const violations = findDisallowedSemanticLayerTargetPaths(input);
19
+ if (violations.length === 0) {
20
+ return;
21
+ }
22
+ const allowed = [...input.allowedConnectionIds].sort();
23
+ throw new Error(`semantic-layer target connection not allowed: ${violations
24
+ .map((violation) => `${violation.path} (${violation.connectionId})`)
25
+ .join(', ')}; allowed: ${allowed.length > 0 ? allowed.join(', ') : '(none)'}`);
26
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { assertSemanticLayerTargetPathsAllowed, findDisallowedSemanticLayerTargetPaths, semanticLayerConnectionIdFromPath, } from './semantic-layer-target-policy.js';
3
+ describe('semantic-layer target policy', () => {
4
+ it('extracts connection ids from semantic-layer paths', () => {
5
+ expect(semanticLayerConnectionIdFromPath('semantic-layer/warehouse/orders.yaml')).toBe('warehouse');
6
+ expect(semanticLayerConnectionIdFromPath('a/semantic-layer/finance/orders.yaml')).toBe('finance');
7
+ expect(semanticLayerConnectionIdFromPath('wiki/global/orders.md')).toBeNull();
8
+ });
9
+ it('finds semantic-layer paths outside the allowed target connections', () => {
10
+ expect(findDisallowedSemanticLayerTargetPaths({
11
+ paths: [
12
+ 'semantic-layer/warehouse/orders.yaml',
13
+ 'semantic-layer/finance/orders.yaml',
14
+ 'wiki/global/orders.md',
15
+ ],
16
+ allowedConnectionIds: new Set(['warehouse']),
17
+ })).toEqual([{ path: 'semantic-layer/finance/orders.yaml', connectionId: 'finance' }]);
18
+ });
19
+ it('throws a deterministic error for unauthorized semantic-layer targets', () => {
20
+ expect(() => assertSemanticLayerTargetPathsAllowed({
21
+ paths: ['semantic-layer/finance/orders.yaml', 'semantic-layer/marketing/accounts.yaml'],
22
+ allowedConnectionIds: new Set(['warehouse']),
23
+ })).toThrow(/semantic-layer target connection not allowed: semantic-layer\/finance\/orders\.yaml \(finance\), semantic-layer\/marketing\/accounts\.yaml \(marketing\); allowed: warehouse/);
24
+ });
25
+ });
@@ -12,6 +12,7 @@ export interface WorkUnitExecutionDeps {
12
12
  revParseHead(): Promise<string | null>;
13
13
  };
14
14
  agentRunner: AgentRunnerPort;
15
+ validateWikiRefs?: (actions: MemoryAction[]) => Promise<string[]>;
15
16
  validateTouchedSources: (touched: TouchedSlSource[]) => Promise<TouchedValidationResult>;
16
17
  resetHardTo: (targetSha: string) => Promise<void>;
17
18
  buildSystemPrompt: (wu: WorkUnit) => string;
@@ -40,5 +41,8 @@ export interface WorkUnitOutcome {
40
41
  touchedSlSources: TouchedSlSource[];
41
42
  slDisallowed?: boolean;
42
43
  slDisallowedReason?: 'lookml_connection_mismatch';
44
+ patchPath?: string;
45
+ patchTouchedPaths?: string[];
46
+ childWorktreePath?: string;
43
47
  }
44
48
  export declare function executeWorkUnit(deps: WorkUnitExecutionDeps, wu: WorkUnit): Promise<WorkUnitOutcome>;
@@ -81,6 +81,10 @@ export async function executeWorkUnit(deps, wu) {
81
81
  if (toolFailureCount > 0) {
82
82
  return failWithReset(`${toolFailureCount} tool call(s) failed during WorkUnit ${wu.unitKey}`);
83
83
  }
84
+ const danglingWikiRefs = (await deps.validateWikiRefs?.(deps.sessionActions)) ?? [];
85
+ if (danglingWikiRefs.length > 0) {
86
+ return failWithReset(`wiki references target missing page(s): ${danglingWikiRefs.join(', ')}`);
87
+ }
84
88
  const touched = listTouchedSlSources(deps.captureSession.touchedSlSources);
85
89
  if (touched.length > 0) {
86
90
  const validation = await deps.validateTouchedSources(touched);
@@ -108,6 +108,35 @@ describe('Stage 3 — executeWorkUnit', () => {
108
108
  expect(outcome.touchedSlSources).toEqual([]);
109
109
  expect(deps.resetHardTo).toHaveBeenCalledWith('pre');
110
110
  });
111
+ it('dangling wiki refs reset to the pre-WU SHA and mark WU failed after the agent loop', async () => {
112
+ const deps = makeDeps();
113
+ deps.sessionWorktreeGit.revParseHead = vi.fn().mockResolvedValueOnce('pre').mockResolvedValueOnce('post');
114
+ deps.agentRunner.runLoop = vi.fn().mockImplementation(() => {
115
+ deps.sessionActions.push({ target: 'wiki', type: 'created', key: 'page-a', detail: 'Page A' });
116
+ return Promise.resolve({ stopReason: 'natural' });
117
+ });
118
+ deps.validateWikiRefs = vi.fn().mockResolvedValue(['page-a -> page-b']);
119
+ const outcome = await executeWorkUnit(deps, makeWu());
120
+ expect(outcome.status).toBe('failed');
121
+ expect(outcome.reason).toContain('wiki references target missing page(s): page-a -> page-b');
122
+ expect(outcome.actions).toEqual([]);
123
+ expect(outcome.touchedSlSources).toEqual([]);
124
+ expect(deps.resetHardTo).toHaveBeenCalledWith('pre');
125
+ });
126
+ it('resolved wiki refs pass post-WU validation and preserve actions', async () => {
127
+ const deps = makeDeps();
128
+ deps.sessionWorktreeGit.revParseHead = vi.fn().mockResolvedValueOnce('pre').mockResolvedValueOnce('post');
129
+ deps.agentRunner.runLoop = vi.fn().mockImplementation(() => {
130
+ deps.sessionActions.push({ target: 'wiki', type: 'created', key: 'page-a', detail: 'Page A' });
131
+ deps.sessionActions.push({ target: 'wiki', type: 'created', key: 'page-b', detail: 'Page B' });
132
+ return Promise.resolve({ stopReason: 'natural' });
133
+ });
134
+ deps.validateWikiRefs = vi.fn().mockResolvedValue([]);
135
+ const outcome = await executeWorkUnit(deps, makeWu());
136
+ expect(outcome.status).toBe('success');
137
+ expect(outcome.actions.map((action) => action.key)).toEqual(['page-a', 'page-b']);
138
+ expect(deps.resetHardTo).not.toHaveBeenCalled();
139
+ });
111
140
  it('runner loop thrown exception resets to the pre-WU SHA and marks WU failed', async () => {
112
141
  const deps = makeDeps();
113
142
  deps.sessionWorktreeGit.revParseHead = vi.fn().mockResolvedValueOnce('pre').mockResolvedValueOnce('post');
@@ -6,7 +6,7 @@ interface EmitUnmappedFallbackDeps {
6
6
  }
7
7
  export declare function createEmitUnmappedFallbackTool(deps: EmitUnmappedFallbackDeps): import("ai").Tool<{
8
8
  rawPath: string;
9
- reason: "looker_template_unresolved" | "derived_table_not_supported" | "no_connection_mapping" | "no_physical_table" | "multiple_table_references" | "unsupported_dialect" | "parse_error" | "missing_target_table" | "cumulative_metric_unsupported" | "conversion_metric_unsupported";
9
+ reason: "no_connection_mapping" | "looker_template_unresolved" | "derived_table_not_supported" | "no_physical_table" | "multiple_table_references" | "unsupported_dialect" | "parse_error" | "missing_target_table" | "cumulative_metric_unsupported" | "conversion_metric_unsupported";
10
10
  fallback: "sql_standalone" | "wiki_only" | "flagged";
11
11
  tableRef?: string | undefined;
12
12
  clarification?: string | undefined;
@@ -1,4 +1,5 @@
1
1
  import type { KtxEmbeddingPort } from '../core/embedding.js';
2
+ import type { SemanticLayerService } from '../sl/index.js';
2
3
  import type { MemoryFlowEventSink } from './memory-flow/types.js';
3
4
  export type IngestTrigger = 'upload' | 'scheduled_pull' | 'manual_resync' | 'manual_override';
4
5
  export interface DiffSet {
@@ -40,6 +41,7 @@ export interface ChunkResult {
40
41
  export interface FetchContext {
41
42
  connectionId: string;
42
43
  sourceKey: string;
44
+ memoryFlow?: MemoryFlowEventSink;
43
45
  }
44
46
  type SourceFetchIssueKind = 'unmapped_looker_connection' | 'unparseable_sql_table_name' | 'looker_template_unresolved' | 'derived_table_not_supported' | 'lookml_connection_mismatch';
45
47
  export interface SourceFetchIssue {
@@ -76,6 +78,27 @@ export interface ClusterWorkUnitsContext {
76
78
  stagedDir: string;
77
79
  embedding: KtxEmbeddingPort;
78
80
  }
81
+ export interface DeterministicProjectionContext {
82
+ connectionId: string;
83
+ sourceKey: string;
84
+ syncId: string;
85
+ jobId: string;
86
+ runId: string;
87
+ stagedDir: string;
88
+ workdir: string;
89
+ parseArtifacts?: unknown;
90
+ semanticLayerService: SemanticLayerService;
91
+ }
92
+ export interface ProjectionResult {
93
+ warnings: string[];
94
+ errors: string[];
95
+ touchedSources: Array<{
96
+ connectionId: string;
97
+ sourceName: string;
98
+ }>;
99
+ changedWikiPageKeys: string[];
100
+ result?: unknown;
101
+ }
79
102
  export interface SourceAdapter {
80
103
  readonly source: string;
81
104
  readonly skillNames: string[];
@@ -89,6 +112,7 @@ export interface SourceAdapter {
89
112
  listTargetConnectionIds?(stagedDir: string): Promise<string[]>;
90
113
  chunk(stagedDir: string, diffSet?: DiffSet): Promise<ChunkResult>;
91
114
  clusterWorkUnits?(ctx: ClusterWorkUnitsContext): Promise<WorkUnit[]>;
115
+ project?(ctx: DeterministicProjectionContext): Promise<ProjectionResult>;
92
116
  describeScope?(stagedDir: string): Promise<ScopeDescriptor>;
93
117
  onPullSucceeded?(ctx: {
94
118
  connectionId: string;
@@ -0,0 +1,24 @@
1
+ import type { SemanticLayerSource } from '../sl/index.js';
2
+ export type WikiBodyRef = {
3
+ kind: 'sl_entity';
4
+ connectionId: string | null;
5
+ sourceName: string;
6
+ entityName: string;
7
+ } | {
8
+ kind: 'sl_source';
9
+ connectionId: string | null;
10
+ sourceName: string;
11
+ } | {
12
+ kind: 'table';
13
+ connectionId: string | null;
14
+ tableRef: string;
15
+ };
16
+ export interface WikiBodyRefValidationInput {
17
+ pageKey: string;
18
+ body: string;
19
+ visibleConnectionIds: string[];
20
+ loadSources(connectionId: string): Promise<SemanticLayerSource[]>;
21
+ tableExists(connectionId: string, tableRef: string): Promise<boolean>;
22
+ }
23
+ export declare function parseWikiBodyRefs(body: string): WikiBodyRef[];
24
+ export declare function findInvalidWikiBodyRefs(input: WikiBodyRefValidationInput): Promise<string[]>;
@@ -0,0 +1,111 @@
1
+ const inlineCodePattern = /`([^`\n]+)`/g;
2
+ function visibleLinesOutsideFences(body) {
3
+ const lines = [];
4
+ let fenced = false;
5
+ for (const line of body.split('\n')) {
6
+ if (/^\s*```/.test(line)) {
7
+ fenced = !fenced;
8
+ continue;
9
+ }
10
+ if (!fenced) {
11
+ lines.push(line);
12
+ }
13
+ }
14
+ return lines;
15
+ }
16
+ function parseConnectionScoped(value) {
17
+ const slash = value.indexOf('/');
18
+ if (slash <= 0) {
19
+ return { connectionId: null, body: value };
20
+ }
21
+ return { connectionId: value.slice(0, slash), body: value.slice(slash + 1) };
22
+ }
23
+ function isIdentifierToken(value) {
24
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
25
+ }
26
+ export function parseWikiBodyRefs(body) {
27
+ const refs = [];
28
+ for (const line of visibleLinesOutsideFences(body)) {
29
+ for (const match of line.matchAll(inlineCodePattern)) {
30
+ const token = (match[1] ?? '').trim();
31
+ if (!token) {
32
+ continue;
33
+ }
34
+ const scoped = parseConnectionScoped(token);
35
+ if (scoped.body.startsWith('source:')) {
36
+ const sourceName = scoped.body.slice('source:'.length).trim();
37
+ if (sourceName) {
38
+ refs.push({ kind: 'sl_source', connectionId: scoped.connectionId, sourceName });
39
+ }
40
+ continue;
41
+ }
42
+ if (scoped.body.startsWith('table:')) {
43
+ const tableRef = scoped.body.slice('table:'.length).trim();
44
+ if (tableRef) {
45
+ refs.push({ kind: 'table', connectionId: scoped.connectionId, tableRef });
46
+ }
47
+ continue;
48
+ }
49
+ const parts = scoped.body.split('.');
50
+ if (parts.length === 2 && isIdentifierToken(parts[0] ?? '') && isIdentifierToken(parts[1] ?? '')) {
51
+ refs.push({
52
+ kind: 'sl_entity',
53
+ connectionId: scoped.connectionId,
54
+ sourceName: parts[0],
55
+ entityName: parts[1],
56
+ });
57
+ }
58
+ }
59
+ }
60
+ return refs;
61
+ }
62
+ function entityNames(source) {
63
+ return new Set([
64
+ ...(source.measures ?? []).map((measure) => measure.name),
65
+ ...(source.columns ?? []).map((column) => column.name),
66
+ ...(source.segments ?? []).map((segment) => segment.name),
67
+ ]);
68
+ }
69
+ export async function findInvalidWikiBodyRefs(input) {
70
+ const errors = [];
71
+ const sourceCache = new Map();
72
+ const loadSources = async (connectionId) => {
73
+ const cached = sourceCache.get(connectionId);
74
+ if (cached) {
75
+ return cached;
76
+ }
77
+ const sources = await input.loadSources(connectionId);
78
+ sourceCache.set(connectionId, sources);
79
+ return sources;
80
+ };
81
+ const findSource = async (connectionIds, sourceName) => {
82
+ for (const connectionId of connectionIds) {
83
+ const source = (await loadSources(connectionId)).find((candidate) => candidate.name === sourceName);
84
+ if (source) {
85
+ return { connectionId, source };
86
+ }
87
+ }
88
+ return null;
89
+ };
90
+ for (const ref of parseWikiBodyRefs(input.body)) {
91
+ const connectionIds = ref.connectionId ? [ref.connectionId] : input.visibleConnectionIds;
92
+ if (ref.kind === 'table') {
93
+ const found = await Promise.all(connectionIds.map((connectionId) => input.tableExists(connectionId, ref.tableRef)));
94
+ if (!found.some(Boolean)) {
95
+ errors.push(`${input.pageKey}: unknown raw table ${ref.connectionId ? `${ref.connectionId}/` : ''}${ref.tableRef}`);
96
+ }
97
+ continue;
98
+ }
99
+ const found = await findSource(connectionIds, ref.sourceName);
100
+ if (!found) {
101
+ if (ref.kind === 'sl_source') {
102
+ errors.push(`${input.pageKey}: unknown semantic-layer source ${ref.connectionId ? `${ref.connectionId}/` : ''}${ref.sourceName}`);
103
+ }
104
+ continue;
105
+ }
106
+ if (ref.kind === 'sl_entity' && !entityNames(found.source).has(ref.entityName)) {
107
+ errors.push(`${input.pageKey}: unknown semantic-layer entity ${ref.sourceName}.${ref.entityName}`);
108
+ }
109
+ }
110
+ return errors;
111
+ }