@kaelio/ktx 0.11.0 → 0.12.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 (181) hide show
  1. package/assets/python/{kaelio_ktx-0.11.0-py3-none-any.whl → kaelio_ktx-0.12.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/admin.js +1 -1
  5. package/dist/clack.d.ts +16 -0
  6. package/dist/clack.js +37 -6
  7. package/dist/claude-code-prompt-caching.js +1 -1
  8. package/dist/cli-program.js +3 -3
  9. package/dist/cli-runtime.js +2 -2
  10. package/dist/commands/connection-commands.js +1 -1
  11. package/dist/commands/ingest-commands.js +4 -4
  12. package/dist/commands/mcp-commands.js +12 -12
  13. package/dist/commands/runtime-commands.js +4 -4
  14. package/dist/commands/setup-commands.js +6 -5
  15. package/dist/commands/sl-commands.js +1 -1
  16. package/dist/commands/sql-commands.js +1 -1
  17. package/dist/commands/status-commands.js +1 -1
  18. package/dist/connection.js +1 -1
  19. package/dist/connectors/clickhouse/connector.js +1 -1
  20. package/dist/connectors/mysql/connector.js +1 -1
  21. package/dist/connectors/snowflake/connector.d.ts +1 -1
  22. package/dist/connectors/sqlite/connector.js +2 -25
  23. package/dist/connectors/sqlserver/connector.js +3 -3
  24. package/dist/context/connections/connection-type.d.ts +1 -1
  25. package/dist/context/connections/read-only-sql.d.ts +1 -0
  26. package/dist/context/connections/read-only-sql.js +116 -2
  27. package/dist/context/core/git.service.d.ts +23 -0
  28. package/dist/context/core/git.service.js +71 -8
  29. package/dist/context/ingest/adapters/historic-sql/projection.js +2 -1
  30. package/dist/context/ingest/adapters/looker/client.js +7 -2
  31. package/dist/context/ingest/adapters/looker/factory.d.ts +8 -1
  32. package/dist/context/ingest/adapters/looker/factory.js +9 -0
  33. package/dist/context/ingest/adapters/looker/mapping.js +1 -1
  34. package/dist/context/ingest/adapters/looker/types.d.ts +1 -1
  35. package/dist/context/ingest/adapters/metabase/client.d.ts +1 -1
  36. package/dist/context/ingest/adapters/metabase/client.js +1 -1
  37. package/dist/context/ingest/adapters/metabase/local-metabase.adapter.js +1 -1
  38. package/dist/context/ingest/adapters/metabase/mapping.js +6 -6
  39. package/dist/context/ingest/artifact-gates.d.ts +2 -6
  40. package/dist/context/ingest/artifact-gates.js +5 -47
  41. package/dist/context/ingest/constrained-repair.d.ts +55 -0
  42. package/dist/context/ingest/constrained-repair.js +167 -0
  43. package/dist/context/ingest/final-gate-repair.d.ts +9 -11
  44. package/dist/context/ingest/final-gate-repair.js +40 -128
  45. package/dist/context/ingest/finalization-scope.d.ts +1 -1
  46. package/dist/context/ingest/finalization-scope.js +15 -15
  47. package/dist/context/ingest/ingest-bundle.runner.d.ts +1 -0
  48. package/dist/context/ingest/ingest-bundle.runner.js +101 -67
  49. package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +6 -13
  50. package/dist/context/ingest/isolated-diff/patch-integrator.js +32 -109
  51. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +8 -9
  52. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +63 -141
  53. package/dist/context/ingest/local-bundle-runtime.d.ts +2 -0
  54. package/dist/context/ingest/local-bundle-runtime.js +9 -10
  55. package/dist/context/ingest/local-ingest.d.ts +2 -0
  56. package/dist/context/ingest/local-ingest.js +2 -0
  57. package/dist/context/ingest/memory-flow/view-model.js +1 -1
  58. package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -6
  59. package/dist/context/ingest/stages/stage-3-work-units.js +2 -1
  60. package/dist/context/ingest/stages/validate-wu-sources.d.ts +7 -1
  61. package/dist/context/ingest/stages/validate-wu-sources.js +109 -4
  62. package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.d.ts +2 -0
  63. package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.js +1 -1
  64. package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +3 -3
  65. package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.d.ts +3 -1
  66. package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +15 -1
  67. package/dist/context/llm/ai-sdk-runtime.js +2 -2
  68. package/dist/context/llm/claude-code-runtime.js +1 -1
  69. package/dist/context/llm/local-config.js +1 -1
  70. package/dist/context/llm/runtime-tools.js +2 -2
  71. package/dist/context/mcp/context-tools.js +7 -7
  72. package/dist/context/mcp/local-project-ports.js +23 -54
  73. package/dist/context/memory/local-memory.js +4 -1
  74. package/dist/context/memory/memory-agent.service.js +1 -1
  75. package/dist/context/project/config.d.ts +11 -4
  76. package/dist/context/project/config.js +85 -30
  77. package/dist/context/project/driver-schemas.js +1 -1
  78. package/dist/context/project/mappings-yaml-schema.js +2 -2
  79. package/dist/context/project/project.js +12 -4
  80. package/dist/context/scan/description-generation.js +4 -4
  81. package/dist/context/scan/local-enrichment-artifacts.js +2 -1
  82. package/dist/context/scan/local-scan.js +2 -2
  83. package/dist/context/scan/local-structural-artifacts.js +5 -5
  84. package/dist/context/scan/relationship-benchmark-report.js +1 -1
  85. package/dist/context/scan/relationship-discovery.js +3 -3
  86. package/dist/context/scan/relationship-llm-proposal.js +3 -3
  87. package/dist/context/sl/local-query.js +3 -33
  88. package/dist/context/sl/local-sl.d.ts +0 -8
  89. package/dist/context/sl/local-sl.js +44 -69
  90. package/dist/context/sl/semantic-layer.service.d.ts +25 -8
  91. package/dist/context/sl/semantic-layer.service.js +109 -56
  92. package/dist/context/sl/source-files.d.ts +46 -0
  93. package/dist/context/sl/source-files.js +131 -0
  94. package/dist/context/sl/tools/base-semantic-layer.tool.d.ts +2 -2
  95. package/dist/context/sl/tools/base-semantic-layer.tool.js +2 -7
  96. package/dist/context/sl/tools/sl-edit-source.tool.js +10 -8
  97. package/dist/context/sl/tools/sl-warehouse-validation.js +55 -27
  98. package/dist/context/sl/tools/sl-write-source.tool.js +12 -9
  99. package/dist/context/sql-analysis/dialect.d.ts +2 -0
  100. package/dist/context/sql-analysis/dialect.js +20 -0
  101. package/dist/context/tools/base-tool.d.ts +6 -19
  102. package/dist/context/tools/base-tool.js +0 -14
  103. package/dist/context-build-view.js +5 -5
  104. package/dist/database-tree-picker.js +18 -3
  105. package/dist/demo-assets.js +0 -1
  106. package/dist/doctor.d.ts +1 -1
  107. package/dist/doctor.js +31 -23
  108. package/dist/errors.d.ts +31 -0
  109. package/dist/errors.js +44 -0
  110. package/dist/ingest.d.ts +1 -1
  111. package/dist/ingest.js +8 -2
  112. package/dist/io/symbols.d.ts +2 -0
  113. package/dist/io/symbols.js +2 -0
  114. package/dist/io/tty.d.ts +8 -0
  115. package/dist/io/tty.js +16 -0
  116. package/dist/llm/embedding-health.js +1 -1
  117. package/dist/llm/embedding-provider.js +3 -3
  118. package/dist/llm/model-provider.js +1 -1
  119. package/dist/local-adapters.d.ts +1 -0
  120. package/dist/local-adapters.js +2 -2
  121. package/dist/local-scan-connectors.js +1 -1
  122. package/dist/managed-local-embeddings.js +17 -8
  123. package/dist/managed-mcp-daemon.js +3 -3
  124. package/dist/managed-python-command.d.ts +7 -0
  125. package/dist/managed-python-command.js +34 -8
  126. package/dist/managed-python-daemon.js +2 -2
  127. package/dist/managed-python-http.js +3 -3
  128. package/dist/managed-python-runtime.d.ts +30 -1
  129. package/dist/managed-python-runtime.js +134 -18
  130. package/dist/managed-uv-release.d.ts +7 -0
  131. package/dist/managed-uv-release.js +11 -0
  132. package/dist/mcp-http-server.js +4 -4
  133. package/dist/mcp-server-factory.js +3 -3
  134. package/dist/mcp-stdio-server.js +1 -1
  135. package/dist/memory-flow-hud.js +2 -2
  136. package/dist/next-steps.js +2 -2
  137. package/dist/prompt-navigation.d.ts +17 -0
  138. package/dist/prompt-navigation.js +49 -3
  139. package/dist/prompts/memory_agent_bundle_ingest_work_unit.md +2 -2
  140. package/dist/prompts/memory_agent_external_ingest.md +2 -2
  141. package/dist/public-ingest-copy.js +1 -1
  142. package/dist/public-ingest.js +3 -3
  143. package/dist/release-version.js +1 -1
  144. package/dist/runtime-requirements.js +1 -1
  145. package/dist/runtime.js +9 -9
  146. package/dist/scan.js +1 -1
  147. package/dist/setup-agents.js +21 -30
  148. package/dist/setup-banner.d.ts +20 -0
  149. package/dist/setup-banner.js +39 -0
  150. package/dist/setup-context.js +24 -15
  151. package/dist/setup-databases.js +31 -59
  152. package/dist/setup-demo-tour.js +12 -8
  153. package/dist/setup-embeddings.js +9 -9
  154. package/dist/setup-interrupt.js +1 -1
  155. package/dist/setup-models.d.ts +4 -1
  156. package/dist/setup-models.js +54 -28
  157. package/dist/setup-project.js +29 -5
  158. package/dist/setup-prompts.js +16 -1
  159. package/dist/setup-ready-menu.js +1 -1
  160. package/dist/setup-sources.js +27 -7
  161. package/dist/setup.js +13 -13
  162. package/dist/skills/analytics/SKILL.md +3 -3
  163. package/dist/skills/dbt_ingest/SKILL.md +3 -3
  164. package/dist/skills/looker_ingest/SKILL.md +3 -3
  165. package/dist/skills/lookml_ingest/SKILL.md +7 -7
  166. package/dist/skills/metabase_ingest/SKILL.md +4 -4
  167. package/dist/skills/metricflow_ingest/SKILL.md +15 -15
  168. package/dist/skills/notion_synthesize/SKILL.md +1 -1
  169. package/dist/skills/sl/SKILL.md +3 -3
  170. package/dist/skills/sl_capture/SKILL.md +1 -1
  171. package/dist/skills/wiki_capture/SKILL.md +1 -1
  172. package/dist/source-mapping.js +1 -1
  173. package/dist/startup-profile.js +1 -1
  174. package/dist/status-project.d.ts +0 -2
  175. package/dist/status-project.js +4 -6
  176. package/dist/telemetry/events.d.ts +1 -1
  177. package/dist/telemetry/exception.js +14 -0
  178. package/dist/text-ingest.js +1 -1
  179. package/dist/tree-picker-tui.d.ts +0 -1
  180. package/dist/tree-picker-tui.js +2 -3
  181. package/package.json +1 -1
@@ -1,13 +1,118 @@
1
+ import { findMissingJoinTargets, formatMissingJoinTarget } from '../../../context/sl/semantic-layer.service.js';
2
+ export function formatInvalidWuSources(invalid) {
3
+ return invalid.map((entry) => `${entry.source} (${entry.errors.join('; ')})`).join(', ');
4
+ }
5
+ function uniqueTouchedSources(sources) {
6
+ const seen = new Set();
7
+ const unique = [];
8
+ for (const source of sources) {
9
+ const key = `${source.connectionId}:${source.sourceName}`;
10
+ if (seen.has(key)) {
11
+ continue;
12
+ }
13
+ seen.add(key);
14
+ unique.push(source);
15
+ }
16
+ return unique.sort((left, right) => {
17
+ const byConnection = left.connectionId.localeCompare(right.connectionId);
18
+ return byConnection === 0 ? left.sourceName.localeCompare(right.sourceName) : byConnection;
19
+ });
20
+ }
21
+ /**
22
+ * Expand the touched set with direct join neighbors that exist: targets the
23
+ * touched sources join to, and existing sources that join to a touched one.
24
+ * Missing targets are not added here — they are reported as join-target
25
+ * errors on the source that declares them, so the failure names the file
26
+ * that must change instead of the phantom neighbor.
27
+ */
28
+ function expandWithExistingJoinNeighbors(touched, sourcesByConnection) {
29
+ const expanded = [...touched];
30
+ const touchedByConnection = new Map();
31
+ for (const source of touched) {
32
+ const bucket = touchedByConnection.get(source.connectionId) ?? new Set();
33
+ bucket.add(source.sourceName);
34
+ touchedByConnection.set(source.connectionId, bucket);
35
+ }
36
+ for (const [connectionId, sources] of sourcesByConnection) {
37
+ const touchedNames = touchedByConnection.get(connectionId);
38
+ if (!touchedNames || touchedNames.size === 0) {
39
+ continue;
40
+ }
41
+ const existingNames = new Set(sources.map((source) => source.name));
42
+ for (const source of sources) {
43
+ if (touchedNames.has(source.name)) {
44
+ for (const join of source.joins ?? []) {
45
+ if (existingNames.has(join.to)) {
46
+ expanded.push({ connectionId, sourceName: join.to });
47
+ }
48
+ }
49
+ }
50
+ if ((source.joins ?? []).some((join) => touchedNames.has(join.to))) {
51
+ expanded.push({ connectionId, sourceName: source.name });
52
+ }
53
+ }
54
+ }
55
+ return uniqueTouchedSources(expanded);
56
+ }
57
+ /**
58
+ * Join-target errors attributable to this change set: every join declared by
59
+ * a touched source must resolve, and no source may be left joining to a name
60
+ * this change set removed. Pre-existing dangling joins on untouched sources
61
+ * are out of scope — they must not block unrelated work. Resolution is the
62
+ * Python engine's: exact source-name match within the connection.
63
+ */
64
+ function findJoinTargetErrors(touched, sourcesByConnection) {
65
+ const errorsBySource = new Map();
66
+ const touchedByConnection = new Map();
67
+ for (const source of touched) {
68
+ const bucket = touchedByConnection.get(source.connectionId) ?? new Set();
69
+ bucket.add(source.sourceName);
70
+ touchedByConnection.set(source.connectionId, bucket);
71
+ }
72
+ for (const [connectionId, sources] of sourcesByConnection) {
73
+ const touchedNames = touchedByConnection.get(connectionId);
74
+ if (!touchedNames || touchedNames.size === 0) {
75
+ continue;
76
+ }
77
+ const existingNames = sources.map((source) => source.name);
78
+ for (const source of sources) {
79
+ const sourceIsTouched = touchedNames.has(source.name);
80
+ const candidateJoins = sourceIsTouched
81
+ ? source.joins
82
+ : (source.joins ?? []).filter((join) => touchedNames.has(join.to));
83
+ const missing = findMissingJoinTargets(candidateJoins, existingNames);
84
+ if (missing.length === 0) {
85
+ continue;
86
+ }
87
+ const key = `${connectionId}:${source.name}`;
88
+ const messages = missing.map(formatMissingJoinTarget);
89
+ errorsBySource.set(key, [...(errorsBySource.get(key) ?? []), ...messages]);
90
+ }
91
+ }
92
+ return errorsBySource;
93
+ }
1
94
  export async function validateWuTouchedSources(deps, touched) {
95
+ if (touched.length === 0) {
96
+ return { validSources: [], invalidSources: [] };
97
+ }
98
+ const sourcesByConnection = new Map();
99
+ for (const connectionId of new Set(touched.map((source) => source.connectionId))) {
100
+ const { sources } = await deps.semanticLayerService.loadAllSources(connectionId);
101
+ sourcesByConnection.set(connectionId, sources);
102
+ }
103
+ const expanded = expandWithExistingJoinNeighbors(touched, sourcesByConnection);
104
+ const joinTargetErrors = findJoinTargetErrors(touched, sourcesByConnection);
2
105
  const valid = [];
3
106
  const invalid = [];
4
- for (const source of touched) {
107
+ for (const source of expanded) {
108
+ const key = `${source.connectionId}:${source.sourceName}`;
5
109
  const result = await deps.slValidator.validateSingleSource(deps, source.connectionId, source.sourceName);
6
- if (result.errors.length === 0) {
7
- valid.push(`${source.connectionId}:${source.sourceName}`);
110
+ const errors = [...result.errors, ...(joinTargetErrors.get(key) ?? [])];
111
+ if (errors.length === 0) {
112
+ valid.push(key);
8
113
  }
9
114
  else {
10
- invalid.push(`${source.connectionId}:${source.sourceName}`);
115
+ invalid.push({ source: key, errors });
11
116
  }
12
117
  }
13
118
  return { validSources: valid, invalidSources: invalid };
@@ -1,8 +1,10 @@
1
1
  import type { KtxFileStorePort } from '../../../core/file-store.js';
2
2
  import type { SlConnectionCatalogPort } from '../../../sl/ports.js';
3
+ import type { SqlAnalysisPort } from '../../../sql-analysis/ports.js';
3
4
  import type { BaseTool } from '../../../tools/base-tool.js';
4
5
  export declare function createWarehouseVerificationTools(deps: {
5
6
  connections: SlConnectionCatalogPort;
7
+ sqlAnalysis?: SqlAnalysisPort;
6
8
  fallbackFileStore: KtxFileStorePort;
7
9
  wikiSearchTool: BaseTool;
8
10
  slDiscoverTool: BaseTool;
@@ -8,7 +8,7 @@ export function createWarehouseVerificationTools(deps) {
8
8
  });
9
9
  return [
10
10
  new EntityDetailsTool(catalogFactory),
11
- new SqlExecutionTool(deps.connections),
11
+ new SqlExecutionTool(deps.connections, deps.sqlAnalysis),
12
12
  new DiscoverDataTool({
13
13
  wikiSearchTool: deps.wikiSearchTool,
14
14
  slDiscoverTool: deps.slDiscoverTool,
@@ -47,7 +47,7 @@ export class DiscoverDataTool extends BaseTool {
47
47
  };
48
48
  }
49
49
  if (input.sourceName) {
50
- const sl = await this.deps.slDiscoverTool.call({ sourceName: input.sourceName, connectionId: input.connectionId }, context);
50
+ const sl = (await this.deps.slDiscoverTool.call({ sourceName: input.sourceName, connectionId: input.connectionId }, context));
51
51
  return { markdown: sl.markdown, structured: { wiki: null, sl: sl.structured, raw: null } };
52
52
  }
53
53
  const query = input.query?.trim() || '';
@@ -57,13 +57,13 @@ export class DiscoverDataTool extends BaseTool {
57
57
  let sl = null;
58
58
  let raw = null;
59
59
  if (query) {
60
- const wikiResult = await this.deps.wikiSearchTool.call({ query, limit }, context);
60
+ const wikiResult = (await this.deps.wikiSearchTool.call({ query, limit }, context));
61
61
  if (totalFound(wikiResult.structured) > 0) {
62
62
  parts.push('## Wiki Pages', '> use `wiki_read(blockKey)` for full content', wikiResult.markdown, '');
63
63
  wiki = wikiResult.structured;
64
64
  }
65
65
  }
66
- const slResult = await this.deps.slDiscoverTool.call({ query: query || undefined, connectionId: input.connectionId }, context);
66
+ const slResult = (await this.deps.slDiscoverTool.call({ query: query || undefined, connectionId: input.connectionId }, context));
67
67
  if (totalSources(slResult.structured) > 0) {
68
68
  parts.push('## Semantic Layer Sources', '> use `sl_read_source(sourceName)` for the YAML, or `entity_details` for warehouse-shape details', slResult.markdown, '');
69
69
  sl = slResult.structured;
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { SlConnectionCatalogPort } from '../../../../context/sl/ports.js';
3
+ import type { SqlAnalysisPort } from '../../../../context/sql-analysis/ports.js';
3
4
  import { BaseTool, type ToolContext, type ToolOutput } from '../../../../context/tools/base-tool.js';
4
5
  declare const sqlExecutionInputSchema: z.ZodObject<{
5
6
  connectionId: z.ZodString;
@@ -18,8 +19,9 @@ export interface SqlExecutionStructured {
18
19
  }
19
20
  export declare class SqlExecutionTool extends BaseTool<typeof sqlExecutionInputSchema> {
20
21
  private readonly connections;
22
+ private readonly sqlAnalysis?;
21
23
  readonly name = "sql_execution";
22
- constructor(connections: SlConnectionCatalogPort);
24
+ constructor(connections: SlConnectionCatalogPort, sqlAnalysis?: SqlAnalysisPort | undefined);
23
25
  get description(): string;
24
26
  get inputSchema(): z.ZodObject<{
25
27
  connectionId: z.ZodString;
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { assertReadOnlySql, limitSqlForExecution } from '../../../../context/connections/read-only-sql.js';
3
+ import { sqlAnalysisDialectForDriver } from '../../../../context/sql-analysis/dialect.js';
3
4
  import { BaseTool } from '../../../../context/tools/base-tool.js';
4
5
  const sqlExecutionInputSchema = z.object({
5
6
  connectionId: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/),
@@ -23,10 +24,12 @@ function markdownTable(headers, rows, totalRows) {
23
24
  }
24
25
  export class SqlExecutionTool extends BaseTool {
25
26
  connections;
27
+ sqlAnalysis;
26
28
  name = 'sql_execution';
27
- constructor(connections) {
29
+ constructor(connections, sqlAnalysis) {
28
30
  super();
29
31
  this.connections = connections;
32
+ this.sqlAnalysis = sqlAnalysis;
30
33
  }
31
34
  get description() {
32
35
  return 'Run a single read-only SELECT or WITH probe against an allowed warehouse connection and return a capped markdown table or the warehouse error.';
@@ -50,9 +53,20 @@ export class SqlExecutionTool extends BaseTool {
50
53
  },
51
54
  };
52
55
  }
56
+ if (!this.sqlAnalysis) {
57
+ throw new Error('sql_execution requires parser-backed SQL validation.');
58
+ }
53
59
  let sql;
54
60
  let wrappedSql;
55
61
  try {
62
+ const connection = await this.connections.getConnectionById(input.connectionId);
63
+ if (!connection) {
64
+ throw new Error(`Connection not found: ${input.connectionId}`);
65
+ }
66
+ const validation = await this.sqlAnalysis.validateReadOnly(input.sql, sqlAnalysisDialectForDriver(connection.connectionType));
67
+ if (!validation.ok) {
68
+ throw new Error(validation.error ?? 'SQL is not read-only.');
69
+ }
56
70
  sql = assertReadOnlySql(input.sql);
57
71
  wrappedSql = limitSqlForExecution(sql, input.rowLimit);
58
72
  }
@@ -189,7 +189,7 @@ export class AiSdkKtxLlmRuntime {
189
189
  const result = await this.generateTextWithRateLimitRetry(modelProviderName(model), input.abortSignal, () => generateText(request));
190
190
  input.onMetrics?.({ totalMs: Date.now() - startedAt, usage: toLlmTokenUsage(result.totalUsage ?? result.usage) });
191
191
  if (typeof result.text !== 'string') {
192
- throw new Error('KTX LLM text generation returned no text');
192
+ throw new Error('ktx LLM text generation returned no text');
193
193
  }
194
194
  return result.text;
195
195
  }
@@ -223,7 +223,7 @@ export class AiSdkKtxLlmRuntime {
223
223
  const result = await this.generateTextWithRateLimitRetry(modelProviderName(model), input.abortSignal, () => generateText(request));
224
224
  input.onMetrics?.({ totalMs: Date.now() - startedAt, usage: toLlmTokenUsage(result.totalUsage ?? result.usage) });
225
225
  if (result.output == null) {
226
- throw new Error('KTX LLM object generation returned no output');
226
+ throw new Error('ktx LLM object generation returned no output');
227
227
  }
228
228
  return result.output;
229
229
  }
@@ -176,7 +176,7 @@ function baseOptions(input) {
176
176
  ? { behavior: 'allow', toolUseID: options.toolUseID }
177
177
  : {
178
178
  behavior: 'deny',
179
- message: `KTX claude-code runtime only permits current KTX MCP tools; denied ${toolName}.`,
179
+ message: `ktx claude-code runtime only permits current ktx MCP tools; denied ${toolName}.`,
180
180
  toolUseID: options.toolUseID,
181
181
  },
182
182
  permissionMode: 'dontAsk',
@@ -143,7 +143,7 @@ export function resolveLocalKtxEmbeddingConfig(config, env) {
143
143
  batchSize: config.batchSize,
144
144
  };
145
145
  }
146
- throw new Error(`Unsupported KTX embedding backend: ${String(config.backend)}`);
146
+ throw new Error(`Unsupported ktx embedding backend: ${String(config.backend)}`);
147
147
  }
148
148
  /** @internal */
149
149
  export function createLocalKtxEmbeddingProviderFromConfig(config, deps = {}) {
@@ -21,7 +21,7 @@ export function normalizeKtxRuntimeToolOutput(value) {
21
21
  }
22
22
  function assertObjectSchema(name, schema) {
23
23
  if (!(schema instanceof z.ZodObject)) {
24
- throw new Error(`KTX runtime tool "${name}" must use z.object input schema for claude-code`);
24
+ throw new Error(`ktx runtime tool "${name}" must use z.object input schema for claude-code`);
25
25
  }
26
26
  }
27
27
  export function createAiSdkToolSet(tools = {}) {
@@ -57,7 +57,7 @@ export function createRuntimeToolDescriptorFromAiTool(name, aiSdkTool) {
57
57
  inputSchema: aiSdkTool.inputSchema,
58
58
  execute: async (input) => {
59
59
  if (typeof aiSdkTool.execute !== 'function') {
60
- throw new Error(`KTX runtime tool "${name}" has no execute function`);
60
+ throw new Error(`ktx runtime tool "${name}" has no execute function`);
61
61
  }
62
62
  return normalizeKtxRuntimeToolOutput(await aiSdkTool.execute(input, { toolCallId: `runtime-${name}` }));
63
63
  },
@@ -24,16 +24,16 @@ const toolAnnotations = {
24
24
  memory_ingest_status: { title: 'Memory Ingest Status', readOnlyHint: true, openWorldHint: false },
25
25
  };
26
26
  const toolDescriptions = {
27
- connection_list: 'List configured read-only data connections available to this KTX project. Use this before connection-scoped tools when the project may have multiple warehouses.',
28
- discover_data: 'Search across KTX wiki pages, semantic-layer sources, measures, dimensions, raw tables, and columns. Example: discover_data({ query: "monthly orders by customer", connectionId: "warehouse", kinds: ["sl_source", "table"] }).',
29
- wiki_search: 'Search KTX wiki pages for reusable business context. Example: wiki_search({ query: "revenue recognition", limit: 5 }).',
30
- wiki_read: 'Read a KTX wiki page by key returned from wiki_search. Example: wiki_read({ key: "global/revenue" }).',
27
+ connection_list: 'List configured read-only data connections available to this ktx project. Use this before connection-scoped tools when the project may have multiple warehouses.',
28
+ discover_data: 'Search across ktx wiki pages, semantic-layer sources, measures, dimensions, raw tables, and columns. Example: discover_data({ query: "monthly orders by customer", connectionId: "warehouse", kinds: ["sl_source", "table"] }).',
29
+ wiki_search: 'Search ktx wiki pages for reusable business context. Example: wiki_search({ query: "revenue recognition", limit: 5 }).',
30
+ wiki_read: 'Read a ktx wiki page by key returned from wiki_search. Example: wiki_read({ key: "global/revenue" }).',
31
31
  entity_details: 'Read table and column metadata from the latest live-database scan snapshot. Example: entity_details({ connectionId: "warehouse", entities: [{ table: { catalog: null, db: "public", name: "orders" }, columns: ["id"] }] }).',
32
32
  dictionary_search: 'Search profile-sampled warehouse values to locate likely source columns for business values. Example: dictionary_search({ values: ["Acme Corp"], connectionId: "warehouse" }).',
33
33
  sl_read_source: 'Read a semantic-layer YAML source by connection id and source name. Example: sl_read_source({ connectionId: "warehouse", sourceName: "orders" }).',
34
34
  sl_query: 'Execute a semantic-layer query and return headers, rows, and total row count, plus correctness notes (e.g. compile-only or fan-out) when relevant. The generated SQL and full query plan are omitted by default; request them with include: ["sql"] and/or include: ["plan"]. Example: sl_query({ connectionId: "warehouse", measures: ["orders.order_count"], dimensions: [{ field: "orders.created_at", granularity: "month" }], include: ["sql"] }).',
35
- sql_execution: 'Execute one parser-validated read-only SQL query against a configured KTX connection. Example: sql_execution({ connectionId: "warehouse", sql: "select count(*) from public.orders", maxRows: 100 }).',
36
- memory_ingest: 'Ingest free-form markdown knowledge into durable KTX memory. Use this for business rules, metric definitions, schema gotchas, recurring findings, or explicit user requests to remember something. Example: memory_ingest({ connectionId: "warehouse", content: "ARR is reported in cents in this warehouse." }).',
35
+ sql_execution: 'Execute one parser-validated read-only SQL query against a configured ktx connection. Example: sql_execution({ connectionId: "warehouse", sql: "select count(*) from public.orders", maxRows: 100 }).',
36
+ memory_ingest: 'Ingest free-form markdown knowledge into durable ktx memory. Use this for business rules, metric definitions, schema gotchas, recurring findings, or explicit user requests to remember something. Example: memory_ingest({ connectionId: "warehouse", content: "ARR is reported in cents in this warehouse." }).',
37
37
  memory_ingest_status: 'Read the current or final status for a memory ingest run. Example: memory_ingest_status({ runId: "memory-run-1" }).',
38
38
  };
39
39
  const connectionListSchema = z.object({});
@@ -641,7 +641,7 @@ export function registerKtxContextTools(deps) {
641
641
  const ingestInput = {
642
642
  userId: userContext.userId,
643
643
  chatId: `mcp-${randomUUID()}`,
644
- userMessage: 'Ingest external knowledge into KTX memory.',
644
+ userMessage: 'Ingest external knowledge into ktx memory.',
645
645
  assistantMessage: input.content,
646
646
  connectionId: input.connectionId,
647
647
  sourceType: 'external_ingest',
@@ -1,58 +1,18 @@
1
+ import { KtxQueryError, isNativeProgrammingFault } from '../../errors.js';
1
2
  import { localConnectionInfoFromConfig } from '../../context/connections/local-warehouse-descriptor.js';
2
3
  import { createKtxEntityDetailsService } from '../../context/scan/entity-details.js';
3
4
  import { createKtxDiscoverDataService } from '../../context/search/discover.js';
5
+ import { sqlAnalysisDialectForDriver } from '../../context/sql-analysis/dialect.js';
4
6
  import { compileLocalSlQuery } from '../../context/sl/local-query.js';
5
7
  import { createKtxDictionarySearchService } from '../../context/sl/dictionary-search.js';
8
+ import { readLocalSlSource } from '../../context/sl/local-sl.js';
9
+ import { assertSafeConnectionId } from '../../context/sl/source-files.js';
6
10
  import { readLocalKnowledgePage, searchLocalKnowledgePages } from '../wiki/local-knowledge.js';
7
- function dialectForDriver(driver) {
8
- const normalized = (driver ?? 'postgres').toUpperCase();
9
- const map = {
10
- POSTGRES: 'postgres',
11
- BIGQUERY: 'bigquery',
12
- SNOWFLAKE: 'snowflake',
13
- MYSQL: 'mysql',
14
- SQLSERVER: 'tsql',
15
- SQLITE: 'sqlite',
16
- DUCKDB: 'duckdb',
17
- CLICKHOUSE: 'clickhouse',
18
- DATABRICKS: 'databricks',
19
- };
20
- return map[normalized] ?? 'postgres';
21
- }
22
- function sqlAnalysisDialectForDriver(driver) {
23
- return dialectForDriver(driver);
24
- }
25
- function assertSafePathToken(kind, value) {
26
- if (value.trim().length === 0 ||
27
- value.includes('..') ||
28
- value.includes('\\') ||
29
- value.startsWith('/') ||
30
- value.startsWith('.') ||
31
- value.includes('//')) {
32
- throw new Error(`Unsafe ${kind}: ${value}`);
33
- }
34
- return value;
35
- }
36
- function assertSafeConnectionId(connectionId) {
37
- if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(connectionId)) {
38
- throw new Error(`Unsafe connection id: ${connectionId}`);
39
- }
40
- return assertSafePathToken('connection id', connectionId);
41
- }
42
- function assertSafeSourceName(sourceName) {
43
- if (!/^[a-z0-9][a-z0-9_]*$/.test(sourceName)) {
44
- throw new Error(`Unsafe semantic-layer source name: ${sourceName}`);
45
- }
46
- return assertSafePathToken('semantic-layer source name', sourceName);
47
- }
48
11
  async function cleanupConnector(connector) {
49
12
  if (connector?.cleanup) {
50
13
  await connector.cleanup();
51
14
  }
52
15
  }
53
- function slPath(connectionId, sourceName) {
54
- return `semantic-layer/${assertSafeConnectionId(connectionId)}/${assertSafeSourceName(sourceName)}.yaml`;
55
- }
56
16
  async function executeValidatedReadOnlySql(project, options, input, onProgress) {
57
17
  await onProgress?.({ progress: 0, message: 'Validating SQL' });
58
18
  const connectionId = assertSafeConnectionId(input.connectionId);
@@ -78,11 +38,23 @@ async function executeValidatedReadOnlySql(project, options, input, onProgress)
78
38
  throw new Error(`Connection "${connectionId}" does not support read-only SQL execution.`);
79
39
  }
80
40
  await onProgress?.({ progress: 0.3, message: 'Executing' });
81
- const result = await connector.executeReadOnly({
41
+ const result = await connector
42
+ .executeReadOnly({
82
43
  connectionId,
83
44
  sql: input.sql,
84
45
  maxRows: input.maxRows,
85
- }, { runId: 'mcp-sql-execution' });
46
+ }, { runId: 'mcp-sql-execution' })
47
+ .catch((error) => {
48
+ // A warehouse/driver rejection (e.g. the agent's SQL failed to compile)
49
+ // is a surfaced operational outcome, not a ktx fault: mark it expected
50
+ // while preserving the warehouse's own diagnostics. A native JS error
51
+ // (TypeError, etc.) signals a bug in connector code — let it propagate
52
+ // unchanged so Error Tracking still sees it.
53
+ if (isNativeProgrammingFault(error)) {
54
+ throw error;
55
+ }
56
+ throw new KtxQueryError(error instanceof Error ? error.message : String(error), { cause: error });
57
+ });
86
58
  const response = {
87
59
  headers: result.headers,
88
60
  ...(result.headerTypes ? { headerTypes: result.headerTypes } : {}),
@@ -148,14 +120,11 @@ export function createLocalProjectMcpContextPorts(project, options) {
148
120
  },
149
121
  semanticLayer: {
150
122
  async readSource(input) {
151
- const path = slPath(input.connectionId, input.sourceName);
152
- try {
153
- const result = await project.fileStore.readFile(path);
154
- return { sourceName: input.sourceName, yaml: result.content };
155
- }
156
- catch {
157
- return null;
158
- }
123
+ const source = await readLocalSlSource(project, {
124
+ connectionId: input.connectionId,
125
+ sourceName: input.sourceName,
126
+ });
127
+ return source ? { sourceName: source.name, yaml: source.yaml } : null;
159
128
  },
160
129
  async query(input, executionOptions) {
161
130
  if (!options.semanticLayerCompute) {
@@ -31,7 +31,7 @@ import { MemoryAgentService } from './memory-agent.service.js';
31
31
  import { MemoryIngestService } from './memory-runs.js';
32
32
  const promptsDir = fileURLToPath(new URL('../../prompts', import.meta.url));
33
33
  const skillsDir = fileURLToPath(new URL('../../skills', import.meta.url));
34
- const LOCAL_AUTHOR = { name: 'KTX Local', email: 'local@ktx.local' };
34
+ const LOCAL_AUTHOR = { name: 'ktx Local', email: 'local@ktx.local' };
35
35
  const LOCAL_SHAPE_WARNING = 'Local memory ingest validates semantic-layer YAML shape only.';
36
36
  export function createLocalProjectMemoryIngest(project, options = {}) {
37
37
  const logger = options.logger ?? noopLogger;
@@ -307,6 +307,9 @@ class LocalShapeOnlySlValidator {
307
307
  async validateSingleSource(deps, connectionId, sourceName) {
308
308
  try {
309
309
  const file = await deps.semanticLayerService.readSourceFile(connectionId, sourceName);
310
+ if (!file) {
311
+ return { errors: [`${sourceName}: no standalone or overlay file found`], warnings: [] };
312
+ }
310
313
  const parsed = YAML.parse(file.content);
311
314
  const isOverlay = parsed.table == null && parsed.sql == null;
312
315
  const result = (isOverlay ? sourceOverlaySchema : sourceDefinitionSchema).safeParse(parsed);
@@ -391,7 +391,7 @@ export class MemoryAgentService {
391
391
  if (session.connectionId) {
392
392
  for (const { connectionId, sourceName } of listTouchedSlSources(session.touchedSlSources)) {
393
393
  try {
394
- const file = await this.deps.semanticLayerService.readSourceFile(connectionId, sourceName).catch(() => null);
394
+ const file = await this.deps.semanticLayerService.readSourceFile(connectionId, sourceName);
395
395
  if (file?.content) {
396
396
  const parsed = this.parseYamlOrNull(file.content);
397
397
  if (parsed) {
@@ -317,7 +317,6 @@ declare const ktxProjectConfigSchema: z.ZodObject<{
317
317
  "postgres-hybrid": "postgres-hybrid";
318
318
  }>>;
319
319
  git: z.ZodPrefault<z.ZodObject<{
320
- auto_commit: z.ZodDefault<z.ZodBoolean>;
321
320
  author: z.ZodDefault<z.ZodString>;
322
321
  }, z.core.$strict>>;
323
322
  }, z.core.$strict>>;
@@ -418,9 +417,6 @@ declare const ktxProjectConfigSchema: z.ZodObject<{
418
417
  default_toolset: z.ZodDefault<z.ZodArray<z.ZodString>>;
419
418
  }, z.core.$strict>>;
420
419
  }, z.core.$strict>>;
421
- memory: z.ZodPrefault<z.ZodObject<{
422
- auto_commit: z.ZodDefault<z.ZodBoolean>;
423
- }, z.core.$strict>>;
424
420
  scan: z.ZodPrefault<z.ZodObject<{
425
421
  enrichment: z.ZodPrefault<z.ZodObject<{
426
422
  mode: z.ZodDefault<z.ZodEnum<{
@@ -472,12 +468,23 @@ export interface KtxConfigIssue {
472
468
  path: string;
473
469
  message: string;
474
470
  fix?: string;
471
+ /**
472
+ * 'error' blocks the project (bad value on a recognized field); 'warning' is
473
+ * a condition the loader recovers from on its own (an ignored unknown key).
474
+ */
475
+ severity: 'error' | 'warning';
475
476
  }
476
477
  export interface KtxConfigValidation {
477
478
  ok: boolean;
478
479
  issues: KtxConfigIssue[];
479
480
  }
480
481
  export declare function buildDefaultKtxProjectConfig(): KtxProjectConfig;
482
+ /**
483
+ * Parse and validate a ktx.yaml document. Keys this ktx version does not
484
+ * recognize are stripped from the returned config — never from the file, which
485
+ * a load must not rewrite — so a config written by a different ktx version
486
+ * still loads. Malformed values on recognized fields still throw.
487
+ */
481
488
  export declare function parseKtxProjectConfig(raw: string): KtxProjectConfig;
482
489
  export declare function validateKtxProjectConfig(raw: string): KtxConfigValidation;
483
490
  export declare function generateKtxProjectConfigJsonSchema(): Record<string, unknown>;