@kaelio/ktx 0.10.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 (193) hide show
  1. package/assets/python/{kaelio_ktx-0.10.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 +7 -3
  9. package/dist/cli-runtime.d.ts +2 -0
  10. package/dist/cli-runtime.js +14 -8
  11. package/dist/commands/connection-commands.js +1 -1
  12. package/dist/commands/ingest-commands.js +4 -4
  13. package/dist/commands/mcp-commands.js +12 -12
  14. package/dist/commands/runtime-commands.js +4 -4
  15. package/dist/commands/setup-commands.js +6 -5
  16. package/dist/commands/sl-commands.js +1 -1
  17. package/dist/commands/sql-commands.js +1 -1
  18. package/dist/commands/status-commands.js +1 -1
  19. package/dist/community-cta.d.ts +11 -0
  20. package/dist/community-cta.js +19 -0
  21. package/dist/connection.js +1 -1
  22. package/dist/connectors/clickhouse/connector.js +1 -1
  23. package/dist/connectors/mysql/connector.js +1 -1
  24. package/dist/connectors/snowflake/connector.d.ts +1 -1
  25. package/dist/connectors/sqlite/connector.js +2 -25
  26. package/dist/connectors/sqlserver/connector.js +3 -3
  27. package/dist/context/connections/connection-type.d.ts +1 -1
  28. package/dist/context/connections/read-only-sql.d.ts +1 -0
  29. package/dist/context/connections/read-only-sql.js +116 -2
  30. package/dist/context/core/git-env.d.ts +12 -1
  31. package/dist/context/core/git-env.js +17 -2
  32. package/dist/context/core/git.service.d.ts +23 -0
  33. package/dist/context/core/git.service.js +86 -15
  34. package/dist/context/ingest/adapters/historic-sql/projection.js +2 -1
  35. package/dist/context/ingest/adapters/looker/client.js +7 -2
  36. package/dist/context/ingest/adapters/looker/factory.d.ts +8 -1
  37. package/dist/context/ingest/adapters/looker/factory.js +9 -0
  38. package/dist/context/ingest/adapters/looker/mapping.js +1 -1
  39. package/dist/context/ingest/adapters/looker/types.d.ts +1 -1
  40. package/dist/context/ingest/adapters/metabase/client.d.ts +1 -1
  41. package/dist/context/ingest/adapters/metabase/client.js +1 -1
  42. package/dist/context/ingest/adapters/metabase/local-metabase.adapter.js +1 -1
  43. package/dist/context/ingest/adapters/metabase/mapping.js +6 -6
  44. package/dist/context/ingest/artifact-gates.d.ts +2 -6
  45. package/dist/context/ingest/artifact-gates.js +5 -47
  46. package/dist/context/ingest/constrained-repair.d.ts +55 -0
  47. package/dist/context/ingest/constrained-repair.js +167 -0
  48. package/dist/context/ingest/final-gate-repair.d.ts +9 -11
  49. package/dist/context/ingest/final-gate-repair.js +40 -128
  50. package/dist/context/ingest/finalization-scope.d.ts +1 -1
  51. package/dist/context/ingest/finalization-scope.js +15 -15
  52. package/dist/context/ingest/ingest-bundle.runner.d.ts +1 -0
  53. package/dist/context/ingest/ingest-bundle.runner.js +101 -67
  54. package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +6 -13
  55. package/dist/context/ingest/isolated-diff/patch-integrator.js +32 -109
  56. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +8 -9
  57. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +63 -141
  58. package/dist/context/ingest/local-bundle-runtime.d.ts +2 -0
  59. package/dist/context/ingest/local-bundle-runtime.js +9 -10
  60. package/dist/context/ingest/local-ingest.d.ts +2 -0
  61. package/dist/context/ingest/local-ingest.js +2 -0
  62. package/dist/context/ingest/memory-flow/view-model.js +1 -1
  63. package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -6
  64. package/dist/context/ingest/stages/stage-3-work-units.js +2 -1
  65. package/dist/context/ingest/stages/validate-wu-sources.d.ts +7 -1
  66. package/dist/context/ingest/stages/validate-wu-sources.js +109 -4
  67. package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.d.ts +2 -0
  68. package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.js +1 -1
  69. package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +3 -3
  70. package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.d.ts +3 -1
  71. package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +15 -1
  72. package/dist/context/llm/ai-sdk-runtime.js +2 -2
  73. package/dist/context/llm/claude-code-runtime.js +1 -1
  74. package/dist/context/llm/local-config.js +1 -1
  75. package/dist/context/llm/runtime-tools.js +2 -2
  76. package/dist/context/mcp/context-tools.js +7 -7
  77. package/dist/context/mcp/local-project-ports.js +23 -54
  78. package/dist/context/memory/local-memory.js +4 -1
  79. package/dist/context/memory/memory-agent.service.js +1 -1
  80. package/dist/context/project/config.d.ts +11 -4
  81. package/dist/context/project/config.js +85 -30
  82. package/dist/context/project/driver-schemas.js +1 -1
  83. package/dist/context/project/mappings-yaml-schema.js +2 -2
  84. package/dist/context/project/project.js +12 -4
  85. package/dist/context/scan/description-generation.js +4 -4
  86. package/dist/context/scan/local-enrichment-artifacts.js +2 -1
  87. package/dist/context/scan/local-scan.js +2 -2
  88. package/dist/context/scan/local-structural-artifacts.js +5 -5
  89. package/dist/context/scan/relationship-benchmark-report.js +1 -1
  90. package/dist/context/scan/relationship-discovery.js +3 -3
  91. package/dist/context/scan/relationship-llm-proposal.js +3 -3
  92. package/dist/context/sl/local-query.js +3 -33
  93. package/dist/context/sl/local-sl.d.ts +0 -8
  94. package/dist/context/sl/local-sl.js +44 -69
  95. package/dist/context/sl/semantic-layer.service.d.ts +25 -8
  96. package/dist/context/sl/semantic-layer.service.js +109 -56
  97. package/dist/context/sl/source-files.d.ts +46 -0
  98. package/dist/context/sl/source-files.js +131 -0
  99. package/dist/context/sl/tools/base-semantic-layer.tool.d.ts +2 -2
  100. package/dist/context/sl/tools/base-semantic-layer.tool.js +2 -7
  101. package/dist/context/sl/tools/sl-edit-source.tool.js +10 -8
  102. package/dist/context/sl/tools/sl-warehouse-validation.js +55 -27
  103. package/dist/context/sl/tools/sl-write-source.tool.js +12 -9
  104. package/dist/context/sql-analysis/dialect.d.ts +2 -0
  105. package/dist/context/sql-analysis/dialect.js +20 -0
  106. package/dist/context/tools/base-tool.d.ts +6 -19
  107. package/dist/context/tools/base-tool.js +0 -14
  108. package/dist/context-build-view.js +5 -5
  109. package/dist/database-tree-picker.js +18 -3
  110. package/dist/demo-assets.js +0 -1
  111. package/dist/doctor.d.ts +1 -1
  112. package/dist/doctor.js +31 -23
  113. package/dist/errors.d.ts +31 -0
  114. package/dist/errors.js +44 -0
  115. package/dist/ingest.d.ts +1 -1
  116. package/dist/ingest.js +8 -2
  117. package/dist/io/symbols.d.ts +2 -0
  118. package/dist/io/symbols.js +2 -0
  119. package/dist/io/tty.d.ts +17 -0
  120. package/dist/io/tty.js +21 -0
  121. package/dist/links.d.ts +1 -0
  122. package/dist/links.js +1 -0
  123. package/dist/llm/embedding-health.js +1 -1
  124. package/dist/llm/embedding-provider.js +3 -3
  125. package/dist/llm/model-provider.js +1 -1
  126. package/dist/local-adapters.d.ts +1 -0
  127. package/dist/local-adapters.js +2 -2
  128. package/dist/local-scan-connectors.js +1 -1
  129. package/dist/managed-local-embeddings.js +17 -8
  130. package/dist/managed-mcp-daemon.js +3 -3
  131. package/dist/managed-python-command.d.ts +7 -0
  132. package/dist/managed-python-command.js +34 -8
  133. package/dist/managed-python-daemon.js +2 -2
  134. package/dist/managed-python-http.js +3 -3
  135. package/dist/managed-python-runtime.d.ts +30 -1
  136. package/dist/managed-python-runtime.js +134 -18
  137. package/dist/managed-uv-release.d.ts +7 -0
  138. package/dist/managed-uv-release.js +11 -0
  139. package/dist/mcp-http-server.js +4 -4
  140. package/dist/mcp-server-factory.js +3 -3
  141. package/dist/mcp-stdio-server.js +1 -1
  142. package/dist/memory-flow-hud.js +2 -2
  143. package/dist/next-steps.js +2 -2
  144. package/dist/prompt-navigation.d.ts +17 -0
  145. package/dist/prompt-navigation.js +49 -3
  146. package/dist/prompts/memory_agent_bundle_ingest_work_unit.md +2 -2
  147. package/dist/prompts/memory_agent_external_ingest.md +2 -2
  148. package/dist/public-ingest-copy.js +1 -1
  149. package/dist/public-ingest.js +3 -3
  150. package/dist/release-version.js +1 -1
  151. package/dist/runtime-requirements.js +1 -1
  152. package/dist/runtime.js +9 -9
  153. package/dist/scan.js +1 -1
  154. package/dist/setup-agents.js +22 -35
  155. package/dist/setup-banner.d.ts +20 -0
  156. package/dist/setup-banner.js +39 -0
  157. package/dist/setup-context.js +24 -15
  158. package/dist/setup-databases.js +31 -59
  159. package/dist/setup-demo-tour.js +12 -8
  160. package/dist/setup-embeddings.js +9 -9
  161. package/dist/setup-interrupt.js +1 -1
  162. package/dist/setup-models.d.ts +4 -1
  163. package/dist/setup-models.js +54 -28
  164. package/dist/setup-project.js +29 -5
  165. package/dist/setup-prompts.js +16 -5
  166. package/dist/setup-ready-menu.js +1 -1
  167. package/dist/setup-sources.js +27 -7
  168. package/dist/setup.d.ts +25 -0
  169. package/dist/setup.js +90 -19
  170. package/dist/skills/analytics/SKILL.md +3 -3
  171. package/dist/skills/dbt_ingest/SKILL.md +3 -3
  172. package/dist/skills/looker_ingest/SKILL.md +3 -3
  173. package/dist/skills/lookml_ingest/SKILL.md +7 -7
  174. package/dist/skills/metabase_ingest/SKILL.md +4 -4
  175. package/dist/skills/metricflow_ingest/SKILL.md +15 -15
  176. package/dist/skills/notion_synthesize/SKILL.md +1 -1
  177. package/dist/skills/sl/SKILL.md +3 -3
  178. package/dist/skills/sl_capture/SKILL.md +1 -1
  179. package/dist/skills/wiki_capture/SKILL.md +1 -1
  180. package/dist/source-mapping.js +1 -1
  181. package/dist/startup-profile.js +1 -1
  182. package/dist/status-project.d.ts +0 -2
  183. package/dist/status-project.js +4 -6
  184. package/dist/telemetry/command-hook.d.ts +24 -0
  185. package/dist/telemetry/command-hook.js +37 -3
  186. package/dist/telemetry/events.d.ts +1 -1
  187. package/dist/telemetry/exception.js +14 -0
  188. package/dist/telemetry/index.d.ts +2 -2
  189. package/dist/telemetry/index.js +2 -2
  190. package/dist/text-ingest.js +1 -1
  191. package/dist/tree-picker-tui.d.ts +0 -1
  192. package/dist/tree-picker-tui.js +2 -3
  193. package/package.json +1 -1
@@ -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>;
@@ -53,7 +53,7 @@ const llmSchema = z
53
53
  models: z
54
54
  .partialRecord(z.enum(KTX_MODEL_ROLES), z.string().min(1))
55
55
  .default({})
56
- .describe('Per-role model overrides keyed by KTX model role (e.g. "default", "triage"). Values are provider-specific model identifiers.'),
56
+ .describe('Per-role model overrides keyed by ktx model role (e.g. "default", "triage"). Values are provider-specific model identifiers.'),
57
57
  promptCaching: promptCachingSchema.optional().describe('Optional prompt-caching tunables.'),
58
58
  })
59
59
  .describe('LLM provider, per-role model overrides, and prompt-caching tunables.');
@@ -205,27 +205,26 @@ const setupSchema = z
205
205
  .describe('Setup-wizard state captured during `ktx setup`.');
206
206
  const storageGitSchema = z
207
207
  .strictObject({
208
- auto_commit: z.boolean().default(true).describe('When true, KTX automatically commits state changes to the local Git-backed store.'),
209
208
  author: z
210
209
  .string()
211
210
  .min(1)
212
211
  .default('ktx <ktx@example.com>')
213
- .describe('Git author identity used for auto-commits, in standard "Name <email>" form.'),
212
+ .describe('Git author identity used for commits, in standard "Name <email>" form.'),
214
213
  })
215
- .describe('Git-backed storage commit policy.');
214
+ .describe('Git-backed storage author policy.');
216
215
  const storageSchema = z
217
216
  .strictObject({
218
217
  state: z
219
218
  .enum(KTX_STORAGE_STATES)
220
219
  .default('sqlite')
221
- .describe('Backend for KTX state storage. "sqlite" uses .ktx/db.sqlite; "postgres" expects a configured Postgres connection.'),
220
+ .describe('Backend for ktx state storage. "sqlite" uses .ktx/db.sqlite; "postgres" expects a configured Postgres connection.'),
222
221
  search: z
223
222
  .enum(KTX_SEARCH_BACKENDS)
224
223
  .default('sqlite-fts5')
225
224
  .describe('Backend for search indexes. "sqlite-fts5" uses SQLite FTS5; "postgres-hybrid" uses Postgres lexical + vector hybrid search.'),
226
225
  git: storageGitSchema.prefault({}).describe('Git-backed storage commit policy.'),
227
226
  })
228
- .describe('Storage backends and commit policy for KTX state and search indexes.');
227
+ .describe('Storage backends and commit policy for ktx state and search indexes.');
229
228
  const connectionSchema = connectionConfigSchema;
230
229
  const agentSchema = z
231
230
  .strictObject({
@@ -247,11 +246,6 @@ const agentSchema = z
247
246
  .describe('Research-agent configuration.'),
248
247
  })
249
248
  .describe('Agent feature configuration.');
250
- const memorySchema = z
251
- .strictObject({
252
- auto_commit: z.boolean().default(true).describe('When true, KTX automatically commits memory updates to the Git-backed store.'),
253
- })
254
- .describe('Memory subsystem configuration.');
255
249
  const ktxProjectConfigSchema = z
256
250
  .strictObject({
257
251
  setup: setupSchema.optional().describe('Setup-wizard state. Written by `ktx setup`; may be omitted.'),
@@ -259,14 +253,13 @@ const ktxProjectConfigSchema = z
259
253
  .record(z.string(), connectionSchema)
260
254
  .default({})
261
255
  .describe('Map of connection ID to connector configuration. Keys are user-chosen names referenced elsewhere in the config.'),
262
- storage: storageSchema.prefault({}).describe('Storage backends and commit policy for KTX state and search indexes.'),
256
+ storage: storageSchema.prefault({}).describe('Storage backends and commit policy for ktx state and search indexes.'),
263
257
  llm: llmSchema.prefault({}).describe('LLM provider, per-role model overrides, and prompt-caching tunables.'),
264
258
  ingest: ingestSchema.prefault({}).describe('Ingest pipeline configuration.'),
265
259
  agent: agentSchema.prefault({}).describe('Agent feature configuration.'),
266
- memory: memorySchema.prefault({}).describe('Memory subsystem configuration.'),
267
260
  scan: scanSchema.prefault({}).describe('Schema-scan configuration: enrichment and relationship discovery.'),
268
261
  })
269
- .describe('Configuration schema for KTX project files (ktx.yaml).');
262
+ .describe('Configuration schema for ktx project files (ktx.yaml).');
270
263
  function isRecord(value) {
271
264
  return typeof value === 'object' && value !== null && !Array.isArray(value);
272
265
  }
@@ -282,21 +275,53 @@ function valueAtPath(root, path) {
282
275
  }
283
276
  return cursor;
284
277
  }
285
- function formatIssue(issue, input) {
286
- const basePath = dottedPath(issue.path);
278
+ /**
279
+ * Zod reports unknown keys in two shapes: strict objects emit
280
+ * `unrecognized_keys` (path → container, `keys` → offenders), enum-keyed
281
+ * records (`llm.models`) emit one `invalid_key` per offender (path ends with
282
+ * the key). Normalize both so the warning report and the strip always agree.
283
+ */
284
+ function unknownKeyLocations(issue) {
287
285
  if (issue.code === 'unrecognized_keys') {
288
- const keys = issue.keys ?? [];
289
- return keys.map((key) => {
290
- const fullPath = basePath.length > 0 ? `${basePath}.${key}` : key;
291
- return { path: fullPath, message: `Unsupported ${fullPath}: unknown field` };
286
+ return issue.keys.map((key) => ({ containerPath: issue.path, key }));
287
+ }
288
+ if (issue.code === 'invalid_key' && issue.path.length > 0) {
289
+ return [
290
+ {
291
+ containerPath: issue.path.slice(0, -1),
292
+ key: String(issue.path[issue.path.length - 1]),
293
+ },
294
+ ];
295
+ }
296
+ return [];
297
+ }
298
+ function formatIssue(issue, input) {
299
+ const unknownKeys = unknownKeyLocations(issue);
300
+ if (unknownKeys.length > 0) {
301
+ return unknownKeys.map(({ containerPath, key }) => {
302
+ const base = dottedPath(containerPath);
303
+ const fullPath = base.length > 0 ? `${base}.${key}` : key;
304
+ return {
305
+ path: fullPath,
306
+ message: `Unsupported ${fullPath}: unknown field (ignored)`,
307
+ fix: 'Unknown to this ktx version; it is ignored. Delete it from ktx.yaml when convenient.',
308
+ severity: 'warning',
309
+ };
292
310
  });
293
311
  }
312
+ const basePath = dottedPath(issue.path);
294
313
  const lastSegment = issue.path[issue.path.length - 1];
295
314
  if (lastSegment === 'backend' && (issue.code === 'invalid_value' || issue.code === 'invalid_type')) {
296
315
  const value = valueAtPath(input, issue.path);
297
- return [{ path: basePath, message: `Unsupported ${basePath}: ${String(value)}` }];
316
+ return [{ path: basePath, message: `Unsupported ${basePath}: ${String(value)}`, severity: 'error' }];
298
317
  }
299
- return [{ path: basePath, message: basePath.length > 0 ? `${basePath}: ${issue.message}` : issue.message }];
318
+ return [
319
+ {
320
+ path: basePath,
321
+ message: basePath.length > 0 ? `${basePath}: ${issue.message}` : issue.message,
322
+ severity: 'error',
323
+ },
324
+ ];
300
325
  }
301
326
  function collectIssues(error, input) {
302
327
  return error.issues.flatMap((issue) => formatIssue(issue, input));
@@ -309,16 +334,44 @@ function formatZodError(error, input) {
309
334
  export function buildDefaultKtxProjectConfig() {
310
335
  return ktxProjectConfigSchema.parse({});
311
336
  }
337
+ function stripUnrecognizedKeys(input) {
338
+ const result = ktxProjectConfigSchema.safeParse(input);
339
+ if (result.success) {
340
+ return input;
341
+ }
342
+ const unknownKeys = result.error.issues.flatMap(unknownKeyLocations);
343
+ if (unknownKeys.length === 0) {
344
+ return input;
345
+ }
346
+ const value = structuredClone(input);
347
+ for (const { containerPath, key } of unknownKeys) {
348
+ const container = valueAtPath(value, containerPath);
349
+ if (container === null || typeof container !== 'object')
350
+ continue;
351
+ delete container[key];
352
+ }
353
+ return value;
354
+ }
355
+ function parseTolerant(input) {
356
+ const value = stripUnrecognizedKeys(input);
357
+ const result = ktxProjectConfigSchema.safeParse(value);
358
+ if (!result.success) {
359
+ throw new Error(formatZodError(result.error, value));
360
+ }
361
+ return result.data;
362
+ }
363
+ /**
364
+ * Parse and validate a ktx.yaml document. Keys this ktx version does not
365
+ * recognize are stripped from the returned config — never from the file, which
366
+ * a load must not rewrite — so a config written by a different ktx version
367
+ * still loads. Malformed values on recognized fields still throw.
368
+ */
312
369
  export function parseKtxProjectConfig(raw) {
313
370
  const parsed = YAML.parse(raw);
314
371
  if (!isRecord(parsed)) {
315
372
  throw new Error('ktx.yaml must contain a YAML object');
316
373
  }
317
- const result = ktxProjectConfigSchema.safeParse(parsed);
318
- if (!result.success) {
319
- throw new Error(formatZodError(result.error, parsed));
320
- }
321
- return result.data;
374
+ return parseTolerant(parsed);
322
375
  }
323
376
  export function validateKtxProjectConfig(raw) {
324
377
  let parsed;
@@ -327,16 +380,18 @@ export function validateKtxProjectConfig(raw) {
327
380
  }
328
381
  catch (error) {
329
382
  const message = error instanceof Error ? error.message : String(error);
330
- return { ok: false, issues: [{ path: '', message: `ktx.yaml parse error: ${message}` }] };
383
+ return { ok: false, issues: [{ path: '', message: `ktx.yaml parse error: ${message}`, severity: 'error' }] };
331
384
  }
332
385
  if (!isRecord(parsed)) {
333
- return { ok: false, issues: [{ path: '', message: 'ktx.yaml must contain a YAML object' }] };
386
+ return { ok: false, issues: [{ path: '', message: 'ktx.yaml must contain a YAML object', severity: 'error' }] };
334
387
  }
335
388
  const result = ktxProjectConfigSchema.safeParse(parsed);
336
389
  if (result.success) {
337
390
  return { ok: true, issues: [] };
338
391
  }
339
- return { ok: false, issues: collectIssues(result.error, parsed) };
392
+ const issues = collectIssues(result.error, parsed);
393
+ const ok = !issues.some((issue) => issue.severity === 'error');
394
+ return { ok, issues };
340
395
  }
341
396
  export function generateKtxProjectConfigJsonSchema() {
342
397
  const schema = z.toJSONSchema(ktxProjectConfigSchema, {
@@ -84,7 +84,7 @@ const lookerConnectionSchema = z
84
84
  .min(1)
85
85
  .optional()
86
86
  .describe('Reference to Looker OAuth client secret (e.g. env:LOOKER_CLIENT_SECRET).'),
87
- mappings: lookerMappingsSchema.optional().describe('Looker connection-name to KTX warehouse mappings.'),
87
+ mappings: lookerMappingsSchema.optional().describe('Looker connection-name to ktx warehouse mappings.'),
88
88
  })
89
89
  .describe('Looker context-source connection.');
90
90
  const lookmlConnectionSchema = z
@@ -12,7 +12,7 @@ export const metabaseMappingsSchema = z
12
12
  databaseMappings: z
13
13
  .record(z.string(), stringTargetSchema)
14
14
  .default({})
15
- .describe('Map of Metabase database ID (positive integer string) to KTX connection ID. Use null to explicitly unmap.'),
15
+ .describe('Map of Metabase database ID (positive integer string) to ktx connection ID. Use null to explicitly unmap.'),
16
16
  syncEnabled: z
17
17
  .record(z.string(), z.boolean())
18
18
  .default({})
@@ -34,7 +34,7 @@ export const lookerMappingsSchema = z
34
34
  connectionMappings: z
35
35
  .record(z.string().min(1), stringTargetSchema)
36
36
  .default({})
37
- .describe('Map of Looker connection name to KTX connection ID. Use null to explicitly unmap.'),
37
+ .describe('Map of Looker connection name to ktx connection ID. Use null to explicitly unmap.'),
38
38
  })
39
39
  .describe('Looker connection-to-warehouse mapping configuration.');
40
40
  export const lookmlMappingsSchema = z
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import { basename, dirname, join, resolve } from 'node:path';
3
- import { GitService } from '../../context/core/git.service.js';
3
+ import { classifyKtxRepoOwnership, GitService, KtxForeignGitRepositoryError } from '../../context/core/git.service.js';
4
4
  import { noopLogger } from '../../context/core/config.js';
5
5
  import { buildDefaultKtxProjectConfig, parseKtxProjectConfig, serializeKtxProjectConfig } from './config.js';
6
6
  import { LocalGitFileStore } from './local-git-file-store.js';
@@ -69,14 +69,22 @@ export async function initKtxProject(options) {
69
69
  if (!options.force && (await fileExists(configPath))) {
70
70
  throw new Error(`Project already contains ktx.yaml: ${configPath}`);
71
71
  }
72
+ // Must run before ktx.yaml is written: once that file exists the directory
73
+ // classifies as ktx-managed, so a foreign repo would be silently adopted.
74
+ if ((await classifyKtxRepoOwnership(projectDir)) === 'foreign') {
75
+ throw new KtxForeignGitRepositoryError(projectDir);
76
+ }
72
77
  const config = buildDefaultKtxProjectConfig();
73
- const runtime = await createRuntime(projectDir, config, authorName, authorEmail, logger);
74
- await writeProjectFile(projectDir, 'ktx.yaml', serializeKtxProjectConfig(config));
78
+ // ktx.yaml (the ownership signal) is written before git init, so an
79
+ // interrupted init can never leave a bare `.git` without it — residue that
80
+ // would classify as a foreign repo and be unrecoverable.
75
81
  await fs.mkdir(join(projectDir, '.ktx/cache'), { recursive: true });
76
82
  for (const file of TRACKED_SCAFFOLD_FILES) {
77
83
  await writeProjectFile(projectDir, file.path, file.content);
78
84
  }
79
- const commit = await runtime.git.commitFiles(['ktx.yaml', ...TRACKED_SCAFFOLD_FILES.map((file) => file.path)], `Initialize KTX project: ${projectName}`, authorName, authorEmail);
85
+ await writeProjectFile(projectDir, 'ktx.yaml', serializeKtxProjectConfig(config));
86
+ const runtime = await createRuntime(projectDir, config, authorName, authorEmail, logger);
87
+ const commit = await runtime.git.commitFiles(['ktx.yaml', ...TRACKED_SCAFFOLD_FILES.map((file) => file.path)], `Initialize ktx project: ${projectName}`, authorName, authorEmail);
80
88
  return {
81
89
  ...runtime,
82
90
  commitHash: commit.commitHash,
@@ -330,7 +330,7 @@ export class KtxDescriptionGenerator {
330
330
  let fallbackReason = null;
331
331
  if (!connector.sampleTable) {
332
332
  fallbackReason = 'capability_missing';
333
- this.logger?.warn('KTX scan connector does not support table sampling; falling back to metadata-only prompt', {
333
+ this.logger?.warn('ktx scan connector does not support table sampling; falling back to metadata-only prompt', {
334
334
  connectorId: input.connector.id,
335
335
  table: input.table.name,
336
336
  });
@@ -440,7 +440,7 @@ export class KtxDescriptionGenerator {
440
440
  let fallbackReason = null;
441
441
  if (!input.connector.sampleTable) {
442
442
  fallbackReason = 'capability_missing';
443
- this.logger?.warn('KTX scan connector does not support table sampling; falling back to metadata-only prompt', {
443
+ this.logger?.warn('ktx scan connector does not support table sampling; falling back to metadata-only prompt', {
444
444
  connectorId: input.connector.id,
445
445
  table: input.table.name,
446
446
  });
@@ -579,7 +579,7 @@ export class KtxDescriptionGenerator {
579
579
  }
580
580
  }
581
581
  if (!input.connector.sampleTable) {
582
- this.logger?.warn('KTX scan connector does not support table sampling for data-source description generation', {
582
+ this.logger?.warn('ktx scan connector does not support table sampling for data-source description generation', {
583
583
  connectorId: input.connector.id,
584
584
  });
585
585
  return 'No accessible tables found in database';
@@ -647,7 +647,7 @@ export class KtxDescriptionGenerator {
647
647
  let columnValues = column.sampleValues;
648
648
  if (!columnValues || columnValues.length === 0) {
649
649
  if (!input.connector.sampleColumn) {
650
- this.logger?.warn('KTX scan connector does not support column sampling; using available metadata only', {
650
+ this.logger?.warn('ktx scan connector does not support column sampling; using available metadata only', {
651
651
  connectorId: input.connector.id,
652
652
  table: input.table.name,
653
653
  column: column.name,
@@ -1,5 +1,6 @@
1
1
  import YAML from 'yaml';
2
2
  import { buildLiveDatabaseManifestShards } from '../../context/ingest/adapters/live-database/manifest.js';
3
+ import { isSlYamlPath } from '../../context/sl/source-files.js';
3
4
  import { buildKtxRelationshipArtifacts, buildKtxRelationshipDiagnostics, emptyKtxRelationshipProfileArtifact, } from './relationship-diagnostics.js';
4
5
  const LIVE_DATABASE_ADAPTER = 'live-database';
5
6
  const LOCAL_AUTHOR = 'ktx';
@@ -120,7 +121,7 @@ async function loadExistingManifestState(project, connectionId, snapshot) {
120
121
  const columnsByTable = validColumns(snapshot);
121
122
  let files;
122
123
  try {
123
- files = (await project.fileStore.listFiles(schemaDir(connectionId))).files.filter((file) => file.endsWith('.yaml'));
124
+ files = (await project.fileStore.listFiles(schemaDir(connectionId))).files.filter(isSlYamlPath);
124
125
  }
125
126
  catch {
126
127
  return { descriptions, preservedJoins, usage };
@@ -63,7 +63,7 @@ function scanReportPath(connectionId, syncId) {
63
63
  }
64
64
  function assertSupportedMode(mode) {
65
65
  if (mode !== 'structural' && mode !== 'relationships' && mode !== 'enriched') {
66
- throw new Error(`Unsupported KTX scan mode: ${mode}`);
66
+ throw new Error(`Unsupported ktx scan mode: ${mode}`);
67
67
  }
68
68
  }
69
69
  async function resolveScanConnector(options, mode) {
@@ -381,7 +381,7 @@ export async function runLocalScan(options) {
381
381
  }
382
382
  report.warnings.push({
383
383
  code: 'enrichment_failed',
384
- message: `KTX scan enrichment failed after structural scan completed: ${message}`,
384
+ message: `ktx scan enrichment failed after structural scan completed: ${message}`,
385
385
  recoverable: true,
386
386
  metadata: { mode, detectRelationships: options.detectRelationships ?? false },
387
387
  });