@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
@@ -0,0 +1,131 @@
1
+ import { createHash } from 'node:crypto';
2
+ import YAML from 'yaml';
3
+ // Semantic-layer source identity lives in the file's `name:` field, which mirrors
4
+ // the warehouse identifier verbatim (Snowflake's uppercase `SIGNED_UP`, `EVENT$LOG`).
5
+ // The filename is a derived label and never participates in identity: reads resolve
6
+ // a source by scanning the connection directory and matching `name:`, and writes
7
+ // reuse the resolved file's path, so files can be freely renamed by humans without
8
+ // changing which source they define.
9
+ function assertSafePathToken(kind, value) {
10
+ if (value.trim().length === 0 ||
11
+ value.includes('..') ||
12
+ value.includes('\\') ||
13
+ value.startsWith('/') ||
14
+ value.startsWith('.') ||
15
+ value.includes('//')) {
16
+ throw new Error(`Unsafe ${kind}: ${value}`);
17
+ }
18
+ return value;
19
+ }
20
+ export function assertSafeConnectionId(connectionId) {
21
+ if (!isSafeConnectionId(connectionId)) {
22
+ throw new Error(`Unsafe connection id: ${connectionId}`);
23
+ }
24
+ return assertSafePathToken('connection id', connectionId);
25
+ }
26
+ export function isSafeConnectionId(connectionId) {
27
+ return typeof connectionId === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(connectionId);
28
+ }
29
+ export function sourceNameFromPath(path) {
30
+ return (path
31
+ .split('/')
32
+ .at(-1)
33
+ ?.replace(/\.ya?ml$/, '') ?? path);
34
+ }
35
+ // The one predicate for "this path is a semantic-layer YAML file". ktx itself
36
+ // always writes `.yaml` (see `slSourceFileName`), but humans rename freely and
37
+ // the dbt ecosystem's habit is `.yml`, so every reader must accept both — a
38
+ // listing that recognizes only one extension makes the same file visible to
39
+ // some entry points and invisible to others.
40
+ export function isSlYamlPath(path) {
41
+ return path.endsWith('.yaml') || path.endsWith('.yml');
42
+ }
43
+ // Windows refuses these basenames regardless of extension — a genuinely universal
44
+ // filesystem invariant, so the static list is acceptable.
45
+ const WINDOWS_RESERVED_BASENAME = /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])$/;
46
+ const SAFE_FILE_BASENAME = /^[a-z0-9][a-z0-9_]{0,63}$/;
47
+ /**
48
+ * Derive the filename for a semantic-layer source. Total over all possible
49
+ * source names — never throws.
50
+ *
51
+ * Names that are already safe lowercase snake_case become `<name>.yaml`;
52
+ * anything else becomes `<slug>-<8 hex of sha256(name)>.yaml`. The two ranges
53
+ * are disjoint and the mapping is injective: safe filenames contain no `-`,
54
+ * hashed filenames always end in `-<8 hex>`, and slugs are lowercased so names
55
+ * differing only by case get distinct hashes instead of colliding paths on
56
+ * case-insensitive filesystems (macOS APFS, Windows).
57
+ *
58
+ * @internal
59
+ */
60
+ export function slSourceFileName(sourceName) {
61
+ if (SAFE_FILE_BASENAME.test(sourceName) && !WINDOWS_RESERVED_BASENAME.test(sourceName)) {
62
+ return `${sourceName}.yaml`;
63
+ }
64
+ const slug = sourceName
65
+ .toLowerCase()
66
+ .replace(/[^a-z0-9_]+/g, '_')
67
+ .replace(/_+/g, '_')
68
+ .replace(/^_+|_+$/g, '')
69
+ .slice(0, 64);
70
+ const hash = createHash('sha256').update(sourceName, 'utf-8').digest('hex').slice(0, 8);
71
+ return `${slug || 'src'}-${hash}.yaml`;
72
+ }
73
+ export function slSourceFilePath(connectionId, sourceName) {
74
+ return `semantic-layer/${assertSafeConnectionId(connectionId)}/${slSourceFileName(sourceName)}`;
75
+ }
76
+ // Same keying as `loadLocalSlSourceRecords`: the in-file `name:` is the identity;
77
+ // the filename is only a fallback for files so broken that even the `name:` is
78
+ // unrecoverable, or genuinely nameless ones. A file left mid-edit with a syntax
79
+ // error below its `name:` line keeps its declared identity (see
80
+ // `slDeclaredSourceName`), so a human-renamed source is still addressed by name
81
+ // while broken instead of silently reverting to its filename.
82
+ export function slSourceNameForFile(path, content) {
83
+ return slDeclaredSourceName(content) ?? sourceNameFromPath(path);
84
+ }
85
+ /**
86
+ * The `name:` a semantic-layer YAML file declares, or null when the file is
87
+ * nameless or so broken even the name is unrecoverable. Null is how
88
+ * `writeSource` tells a genuine name conflict at a derived path apart from the
89
+ * broken remains of the source being written, which a rewrite must repair
90
+ * rather than refuse.
91
+ *
92
+ * Uses `parseDocument`, not `parse`: a file with a syntax error below the
93
+ * `name:` line still parses into a partial tree whose top-level `name:` is
94
+ * intact. `parse` would throw on the same input and drop the source to its
95
+ * filename — wrong for human-renamed files, whose filename is not the name.
96
+ */
97
+ export function slDeclaredSourceName(content) {
98
+ let doc;
99
+ try {
100
+ doc = YAML.parseDocument(content);
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ const name = doc.get('name');
106
+ return typeof name === 'string' && name.length > 0 ? name : null;
107
+ }
108
+ /**
109
+ * Find the standalone/overlay file that defines `sourceName` for a connection.
110
+ * Returns null when no file declares the name (the source may still exist as a
111
+ * manifest entry under `_schema/`). Throws when more than one file declares the
112
+ * same name — that breaks the one-file-per-name invariant and must be repaired
113
+ * by hand rather than silently picking one.
114
+ */
115
+ export async function resolveSlSourceFile(fileStore, connectionId, sourceName) {
116
+ const dir = `semantic-layer/${assertSafeConnectionId(connectionId)}`;
117
+ const schemaDir = `${dir}/_schema`;
118
+ const listed = await fileStore.listFiles(dir);
119
+ const paths = listed.files.filter((file) => isSlYamlPath(file) && !file.startsWith(`${schemaDir}/`)).sort();
120
+ const matches = [];
121
+ for (const path of paths) {
122
+ const raw = await fileStore.readFile(path);
123
+ if (slSourceNameForFile(path, raw.content) === sourceName) {
124
+ matches.push({ path, content: raw.content });
125
+ }
126
+ }
127
+ if (matches.length > 1) {
128
+ throw new Error(`Multiple semantic-layer files declare source "${sourceName}": ${matches.map((match) => match.path).join(', ')}`);
129
+ }
130
+ return matches[0] ?? null;
131
+ }
@@ -1,4 +1,4 @@
1
- import type { ZodType } from 'zod';
1
+ import type { z } from 'zod';
2
2
  import type { GitAuthorResolverPort } from '../../../context/tools/authors.js';
3
3
  import type { ToolContext, ToolOutput } from '../../../context/tools/base-tool.js';
4
4
  import { BaseTool } from '../../../context/tools/base-tool.js';
@@ -21,7 +21,7 @@ export interface BaseSemanticLayerToolDeps {
21
21
  slSearchService: SlSearchService;
22
22
  authorResolver: GitAuthorResolverPort;
23
23
  }
24
- export declare abstract class BaseSemanticLayerTool<TInput extends ZodType = ZodType> extends BaseTool<TInput> {
24
+ export declare abstract class BaseSemanticLayerTool<TInput extends z.ZodObject<z.ZodRawShape> = z.ZodObject<z.ZodRawShape>> extends BaseTool<TInput> {
25
25
  protected readonly semanticLayerService: SemanticLayerService;
26
26
  protected readonly slSearchService: SlSearchService;
27
27
  protected readonly authorResolver: GitAuthorResolverPort;
@@ -14,13 +14,8 @@ export class BaseSemanticLayerTool extends BaseTool {
14
14
  }
15
15
  async readSourceYaml(connectionId, sourceName, context) {
16
16
  const semanticLayerService = context?.session?.semanticLayerService ?? this.semanticLayerService;
17
- try {
18
- const { content } = await semanticLayerService.readSourceFile(connectionId, sourceName);
19
- return content;
20
- }
21
- catch {
22
- return null;
23
- }
17
+ const file = await semanticLayerService.readSourceFile(connectionId, sourceName);
18
+ return file?.content ?? null;
24
19
  }
25
20
  buildMarkdown(success, errors, sourceName, extra) {
26
21
  const parts = [];
@@ -91,14 +91,8 @@ If no source exists yet, use sl_write_source instead — this tool will reject t
91
91
  }
92
92
  }
93
93
  // Read existing source
94
- let currentYaml = null;
95
- try {
96
- const { content } = await semanticLayerService.readSourceFile(connectionId, sourceName);
97
- currentYaml = content;
98
- }
99
- catch {
100
- currentYaml = null;
101
- }
94
+ const currentFile = await semanticLayerService.readSourceFile(connectionId, sourceName);
95
+ const currentYaml = currentFile?.content ?? null;
102
96
  if (!currentYaml) {
103
97
  const manifestBacked = await semanticLayerService.isManifestBacked(connectionId, sourceName);
104
98
  if (manifestBacked) {
@@ -138,6 +132,14 @@ If no source exists yet, use sl_write_source instead — this tool will reject t
138
132
  catch (e) {
139
133
  return this.buildOutput(false, [`YAML parse error after edits: ${e}`], sourceName);
140
134
  }
135
+ // The in-file `name:` is the source's identity — an edited name would make
136
+ // writeSource create a second source instead of updating this one.
137
+ if (source.name !== sourceName) {
138
+ return this.buildOutput(false, [
139
+ `Edits change "name:" from "${sourceName}" to "${source.name ?? '<missing>'}" — renaming is not supported. ` +
140
+ `Delete the source and recreate it under the new name.`,
141
+ ], sourceName);
142
+ }
141
143
  source = normalizeSemanticLayerDescriptions(source, { fillMissing: !!context.session?.ingest });
142
144
  // Re-serialize and write
143
145
  const updatedYaml = YAML.stringify(source, { indent: 2, lineWidth: 0, version: '1.1' });
@@ -2,8 +2,8 @@ import YAML from 'yaml';
2
2
  import { SYSTEM_GIT_AUTHOR } from '../../../context/tools/authors.js';
3
3
  import { sourceOverlaySchema } from '../schemas.js';
4
4
  import { SemanticLayerService } from '../semantic-layer.service.js';
5
+ import { resolveSlSourceFile, slSourceFilePath } from '../source-files.js';
5
6
  import { sourceDefinitionSchema } from './base-semantic-layer.tool.js';
6
- const slSourcePath = (connectionId, sourceName) => `semantic-layer/${connectionId}/${sourceName}.yaml`;
7
7
  function resolveDialect(warehouse) {
8
8
  if (!warehouse) {
9
9
  return null;
@@ -33,32 +33,28 @@ function wrapWithSingleRowQuery(sql, dialect) {
33
33
  export async function validateSingleSource(deps, connectionId, sourceName) {
34
34
  const errors = [];
35
35
  const warnings = [];
36
- let content;
37
- try {
38
- const result = await deps.semanticLayerService.readSourceFile(connectionId, sourceName);
39
- content = result.content;
40
- }
41
- catch {
42
- errors.push(`${sourceName}.yaml: file not found`);
36
+ const file = await deps.semanticLayerService.readSourceFile(connectionId, sourceName);
37
+ if (!file) {
38
+ errors.push(`${sourceName}: no standalone or overlay file found`);
43
39
  return { errors, warnings };
44
40
  }
45
41
  let parsed;
46
42
  try {
47
- parsed = YAML.parse(content);
43
+ parsed = YAML.parse(file.content);
48
44
  }
49
45
  catch (e) {
50
- errors.push(`${sourceName}.yaml: invalid YAML — ${e instanceof Error ? e.message : String(e)}`);
46
+ errors.push(`${sourceName}: invalid YAML — ${e instanceof Error ? e.message : String(e)}`);
51
47
  return { errors, warnings };
52
48
  }
53
49
  if (!parsed || typeof parsed !== 'object') {
54
- errors.push(`${sourceName}.yaml: top-level content is not an object`);
50
+ errors.push(`${sourceName}: top-level content is not an object`);
55
51
  return { errors, warnings };
56
52
  }
57
53
  const isOverlay = !parsed.table && !parsed.sql;
58
54
  if (!isOverlay) {
59
55
  const isManifestBacked = await deps.semanticLayerService.isManifestBacked(connectionId, sourceName);
60
56
  if (isManifestBacked) {
61
- errors.push(`${sourceName}.yaml: standalone source shadows an existing manifest entry — ` +
57
+ errors.push(`${sourceName}: standalone source shadows an existing manifest entry — ` +
62
58
  `writing it as-is drops the manifest's columns and joins. ` +
63
59
  `Remove "sql:", "table:", "grain:", and base-table "columns:" and keep only ` +
64
60
  `"name:" plus overlay fields such as "measures:", "segments:", "descriptions:", ` +
@@ -71,16 +67,16 @@ export async function validateSingleSource(deps, connectionId, sourceName) {
71
67
  const result = schema.safeParse(parsed);
72
68
  if (!result.success) {
73
69
  const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
74
- errors.push(`${sourceName}.yaml: schema — ${issues}`);
70
+ errors.push(`${sourceName}: schema — ${issues}`);
75
71
  const errorPaths = new Set(result.error.issues.map((i) => String(i.path[0])));
76
72
  if (errorPaths.has('joins')) {
77
- warnings.push(`${sourceName}.yaml: hint — join format: {to, on: 'local_col = TARGET.col', relationship: 'many_to_one|one_to_many|one_to_one'}`);
73
+ warnings.push(`${sourceName}: hint — join format: {to, on: 'local_col = TARGET.col', relationship: 'many_to_one|one_to_many|one_to_one'}`);
78
74
  }
79
75
  if (errorPaths.has('columns')) {
80
- warnings.push(`${sourceName}.yaml: hint — overlay columns must be computed: {name, expr, type}. Use column_overrides for manifest column descriptions or metadata.`);
76
+ warnings.push(`${sourceName}: hint — overlay columns must be computed: {name, expr, type}. Use column_overrides for manifest column descriptions or metadata.`);
81
77
  }
82
78
  if (errorPaths.has('measures')) {
83
- warnings.push(`${sourceName}.yaml: hint — measure format: {name, expr, description (optional), filter (optional)}`);
79
+ warnings.push(`${sourceName}: hint — measure format: {name, expr, description (optional), filter (optional)}`);
84
80
  }
85
81
  return { errors, warnings };
86
82
  }
@@ -93,7 +89,7 @@ export async function validateSingleSource(deps, connectionId, sourceName) {
93
89
  const seenMeasures = new Set();
94
90
  for (const m of measures) {
95
91
  if (seenMeasures.has(m.name)) {
96
- errors.push(`${sourceName}.yaml: duplicate measure name "${m.name}"`);
92
+ errors.push(`${sourceName}: duplicate measure name "${m.name}"`);
97
93
  }
98
94
  seenMeasures.add(m.name);
99
95
  }
@@ -125,7 +121,7 @@ export async function validateSingleSource(deps, connectionId, sourceName) {
125
121
  const actual = new Set((probe.headers ?? []).map((h) => h.toLowerCase()));
126
122
  const missing = sourceColumns.map((c) => c.name).filter((n) => !actual.has(n.toLowerCase()));
127
123
  if (missing.length > 0) {
128
- errors.push(`${sourceName}.yaml: declared columns absent from sql result — ${missing.join(', ')} (warehouse returned: ${[...actual].slice(0, 10).join(', ')}${actual.size > 10 ? ', …' : ''})`);
124
+ errors.push(`${sourceName}: declared columns absent from sql result — ${missing.join(', ')} (warehouse returned: ${[...actual].slice(0, 10).join(', ')}${actual.size > 10 ? ', …' : ''})`);
129
125
  }
130
126
  }
131
127
  catch (e) {
@@ -151,7 +147,7 @@ function formatProbeError(args) {
151
147
  const errMsg = error instanceof Error ? error.message : String(error);
152
148
  const refColumns = sourceColumns.filter((c) => referencesColumn(probeSql, c.name));
153
149
  const lines = [
154
- measureName ? `${sourceName}.yaml: measure "${measureName}" ${headline}.` : `${sourceName}.yaml: ${headline}.`,
150
+ measureName ? `${sourceName}: measure "${measureName}" ${headline}.` : `${sourceName}: ${headline}.`,
155
151
  ];
156
152
  if (warehouse) {
157
153
  lines.push(` Warehouse: ${warehouse}`);
@@ -179,7 +175,7 @@ async function probeOverlayMeasures(deps, connectionId, sourceName, warehouse) {
179
175
  composed = all.find((s) => s.name === sourceName);
180
176
  }
181
177
  catch (e) {
182
- errors.push(`${sourceName}.yaml: failed to load composed source for probe — ${e instanceof Error ? e.message : String(e)}`);
178
+ errors.push(`${sourceName}: failed to load composed source for probe — ${e instanceof Error ? e.message : String(e)}`);
183
179
  return errors;
184
180
  }
185
181
  if (!composed?.table || composed.measures.length === 0) {
@@ -214,22 +210,54 @@ async function probeOverlayMeasures(deps, connectionId, sourceName, warehouse) {
214
210
  }
215
211
  return errors;
216
212
  }
213
+ /**
214
+ * A read-only view of the config repo at one commit, shaped for
215
+ * `resolveSlSourceFile` so name→file resolution runs against history exactly as
216
+ * it does against the working tree — one resolver, two backing stores. Used to
217
+ * recover the path a source occupied at `preHead` after the live file is gone.
218
+ */
219
+ function gitCommitFileStore(git, commitHash) {
220
+ return {
221
+ async listFiles(path) {
222
+ return { files: await git.listFilesAtCommit(path, commitHash) };
223
+ },
224
+ async readFile(path) {
225
+ return { content: await git.getFileAtCommit(path, commitHash) };
226
+ },
227
+ };
228
+ }
217
229
  /**
218
230
  * Restore `sourceName` to the content it had at `preHead`, or delete it if it didn't
219
231
  * exist then. Used by sl_rollback (agent-driven) and the pre-squash revert gate
220
232
  * (automatic). Returns a short human-readable description of what happened.
221
233
  */
222
234
  export async function revertSourceToPreHead(deps, connectionId, preHead, sourceName) {
223
- const relPath = slSourcePath(connectionId, sourceName);
235
+ // Find the file that defines this source. While it is still on disk
236
+ // (invalid-but-present) the live resolver finds it by its in-file `name:`.
237
+ // Once the session deleted it, the path is gone too — and humans rename files
238
+ // freely, so it is NOT the writer-derived filename. Recover it from history by
239
+ // resolving the name against the preHead commit instead of guessing.
240
+ const live = await resolveSlSourceFile(deps.configService, connectionId, sourceName);
241
+ let relPath;
224
242
  let preContent = null;
225
- if (preHead) {
226
- try {
227
- preContent = await deps.gitService.getFileAtCommit(relPath, preHead);
228
- }
229
- catch {
230
- preContent = null;
243
+ if (live) {
244
+ relPath = live.path;
245
+ if (preHead) {
246
+ try {
247
+ preContent = await deps.gitService.getFileAtCommit(relPath, preHead);
248
+ }
249
+ catch {
250
+ preContent = null;
251
+ }
231
252
  }
232
253
  }
254
+ else {
255
+ const atPreHead = preHead
256
+ ? await resolveSlSourceFile(gitCommitFileStore(deps.gitService, preHead), connectionId, sourceName)
257
+ : null;
258
+ relPath = atPreHead?.path ?? slSourceFilePath(connectionId, sourceName);
259
+ preContent = atPreHead?.content ?? null;
260
+ }
233
261
  if (preContent !== null) {
234
262
  await deps.configService.writeFile(relPath, preContent, SYSTEM_GIT_AUTHOR.name, SYSTEM_GIT_AUTHOR.email, `Revert SL source to pre-session state: ${sourceName}`, { skipLock: true });
235
263
  return 'restored to pre-session content';
@@ -12,8 +12,10 @@ const slWriteSourceInputSchema = z.object({
12
12
  connectionId: slToolConnectionIdSchema.describe('Data source connection ID'),
13
13
  sourceName: z
14
14
  .string()
15
- .regex(/^[a-z0-9][a-z0-9_]*$/, 'Source name must be snake_case (lowercase alphanumeric and underscores)')
16
- .describe('Name of the source to create, edit, or delete'),
15
+ .min(1)
16
+ .describe("Name of the source to create, edit, or delete. Must equal the source's `name:`. Use the verbatim " +
17
+ 'warehouse identifier when overlaying a manifest source (e.g. SIGNED_UP); snake_case is recommended ' +
18
+ 'for new standalone sources.'),
17
19
  source: sourceInputSchema
18
20
  .optional()
19
21
  .describe('Source definition (standalone with table/sql) or overlay (measures, column_overrides, computed columns, etc.)'),
@@ -122,6 +124,12 @@ Do NOT join back to a table that the SQL already aggregates from if the grain co
122
124
  if (!input.source) {
123
125
  return this.buildOutput(false, ['Provide `source` to create or rewrite. For targeted edits, use sl_edit_source.'], sourceName);
124
126
  }
127
+ // The in-file `name:` is the source's identity; the file is written under
128
+ // source.name while the orphan/shadow checks key on sourceName — a mismatch
129
+ // would validate one source and save another.
130
+ if (input.source.name !== sourceName) {
131
+ return this.buildOutput(false, [`source.name "${input.source.name}" does not match sourceName "${sourceName}" — they must be identical.`], sourceName);
132
+ }
125
133
  return this.writeFullSource(connectionId, input.source, sourceName, author, authorEmail, context, semanticLayerService, skipIndex, rawPathValidation.rawPaths);
126
134
  }
127
135
  async writeFullSource(connectionId, source, sourceName, author, authorEmail, context, semanticLayerService, skipIndex, rawPaths) {
@@ -183,13 +191,8 @@ Do NOT join back to a table that the SQL already aggregates from if the grain co
183
191
  }
184
192
  }
185
193
  async readSourceYamlFromService(service, connectionId, sourceName) {
186
- try {
187
- const { content } = await service.readSourceFile(connectionId, sourceName);
188
- return content;
189
- }
190
- catch {
191
- return null;
192
- }
194
+ const file = await service.readSourceFile(connectionId, sourceName);
195
+ return file?.content ?? null;
193
196
  }
194
197
  async rejectOrphanOverlay(semanticLayerService, connectionId, sourceName, content) {
195
198
  let parsed;
@@ -0,0 +1,2 @@
1
+ import type { SqlAnalysisDialect } from './ports.js';
2
+ export declare function sqlAnalysisDialectForDriver(driver: string | undefined): SqlAnalysisDialect;
@@ -0,0 +1,20 @@
1
+ // One mapping from ktx connection identity to the sqlglot dialect name used by
2
+ // the Python daemon (SQL analysis, read-only validation) and semantic-layer
3
+ // compute. Keys cover both vocabularies that name a connection's engine:
4
+ // ktx.yaml driver names ("postgres", "sqlserver") and the local connection-type
5
+ // spellings exposed by KtxConnectionInfo.connectionType ("POSTGRESQL").
6
+ const SQLGLOT_DIALECTS = {
7
+ postgres: 'postgres',
8
+ postgresql: 'postgres',
9
+ bigquery: 'bigquery',
10
+ snowflake: 'snowflake',
11
+ mysql: 'mysql',
12
+ sqlserver: 'tsql',
13
+ sqlite: 'sqlite',
14
+ duckdb: 'duckdb',
15
+ clickhouse: 'clickhouse',
16
+ databricks: 'databricks',
17
+ };
18
+ export function sqlAnalysisDialectForDriver(driver) {
19
+ return SQLGLOT_DIALECTS[(driver ?? '').toLowerCase()] ?? 'postgres';
20
+ }
@@ -1,4 +1,5 @@
1
- import { z, type ZodType } from 'zod';
1
+ import { type Tool } from 'ai';
2
+ import { z } from 'zod';
2
3
  import { type KtxLogger } from '../../context/core/config.js';
3
4
  import type { KtxRuntimeToolDescriptor } from '../llm/runtime-port.js';
4
5
  import type { IngestToolMetadata, ToolSession } from './tool-session.js';
@@ -56,30 +57,16 @@ interface MethodologyEntry {
56
57
  /**
57
58
  * SECURITY: All tools require authentication. userId must always be provided in ToolContext.
58
59
  */
59
- export declare abstract class BaseTool<TInput extends ZodType = ZodType> {
60
+ export declare abstract class BaseTool<TInput extends z.ZodObject<z.ZodRawShape> = z.ZodObject<z.ZodRawShape>> {
60
61
  protected readonly logger: KtxLogger;
61
62
  abstract readonly name: string;
62
63
  constructor(logger?: KtxLogger);
63
64
  abstract get description(): string;
64
65
  abstract get inputSchema(): TInput;
65
- abstract call(input: z.infer<TInput>, context: ToolContext): Promise<any>;
66
- getParametersSchema(): {
67
- type: 'object';
68
- properties: Record<string, any>;
69
- required?: string[];
70
- };
71
- toAnthropicFormat(): {
72
- name: string;
73
- description: string;
74
- input_schema: {
75
- type: 'object';
76
- properties: Record<string, any>;
77
- required?: string[];
78
- };
79
- };
80
- toAiSdkTool(context: ToolContext): any;
66
+ abstract call(input: z.infer<TInput>, context: ToolContext): Promise<unknown>;
67
+ toAiSdkTool(context: ToolContext): Tool;
81
68
  toRuntimeTool(context: ToolContext): KtxRuntimeToolDescriptor;
82
- parseInput(input: Record<string, any>): z.infer<TInput>;
69
+ parseInput(input: Record<string, unknown>): z.infer<TInput>;
83
70
  protected getCurrentUserQuery(context: ToolContext): string | null;
84
71
  }
85
72
  export {};
@@ -1,5 +1,4 @@
1
1
  import { tool } from 'ai';
2
- import { z } from 'zod';
3
2
  import { noopLogger } from '../../context/core/config.js';
4
3
  import { normalizeKtxRuntimeToolOutput } from '../llm/runtime-tools.js';
5
4
  /**
@@ -10,19 +9,6 @@ export class BaseTool {
10
9
  constructor(logger = noopLogger) {
11
10
  this.logger = logger;
12
11
  }
13
- getParametersSchema() {
14
- const jsonSchema = z.toJSONSchema(this.inputSchema, {
15
- target: 'draft-7',
16
- });
17
- return jsonSchema;
18
- }
19
- toAnthropicFormat() {
20
- return {
21
- name: this.name,
22
- description: this.description,
23
- input_schema: this.getParametersSchema(),
24
- };
25
- }
26
12
  toAiSdkTool(context) {
27
13
  const toolName = this.name;
28
14
  const logger = this.logger;
@@ -260,7 +260,7 @@ export function renderContextBuildView(state, options = {}) {
260
260
  const totalCount = allTargets.length;
261
261
  const hasActive = allTargets.some((t) => t.status === 'running' || t.status === 'queued');
262
262
  const allDone = totalCount > 0 && !hasActive;
263
- const headerParts = [options.title ?? 'Building KTX context'];
263
+ const headerParts = [options.title ?? 'Building ktx context'];
264
264
  if (totalCount > 0) {
265
265
  const progressParts = [`${doneCount}/${totalCount}`];
266
266
  if (state.totalElapsedMs > 0)
@@ -550,7 +550,7 @@ function failedStepDetail(result) {
550
550
  return result.steps.find((step) => step.status === 'failed')?.detail ?? null;
551
551
  }
552
552
  const INTERNAL_FAILURE_LINE_RE = /^(Report|Run|Job|Status|Adapter|Connection|Sync|Mode|Dry run|Diff|Tasks|Work units|Failed tasks|Saved memory|Provenance rows):\s*/;
553
- const ACTIONABLE_FAILURE_LINE_RE = /^(Missing bundled Python runtime manifest|KTX Python runtime is required|KTX daemon HTTP|Error:|Failed\b|Could not\b|Cannot\b)/;
553
+ const ACTIONABLE_FAILURE_LINE_RE = /^(Missing bundled Python runtime manifest|ktx Python runtime is required|ktx daemon HTTP|Error:|Failed\b|Could not\b|Cannot\b)/;
554
554
  function trimErrorPrefix(line) {
555
555
  return line.replace(/^Error:\s*/, '');
556
556
  }
@@ -559,7 +559,7 @@ function firstCapturedFailureLine(output) {
559
559
  .split(/\r?\n/)
560
560
  .map((candidate) => candidate.trim())
561
561
  .filter((candidate) => candidate.length > 0)
562
- .filter((candidate) => !candidate.startsWith('KTX scan completed'))
562
+ .filter((candidate) => !candidate.startsWith('ktx scan completed'))
563
563
  .filter((candidate) => !INTERNAL_FAILURE_LINE_RE.test(candidate));
564
564
  const line = lines.find((candidate) => ACTIONABLE_FAILURE_LINE_RE.test(candidate)) ?? lines.at(-1) ?? null;
565
565
  return line ? trimErrorPrefix(line) : null;
@@ -584,7 +584,7 @@ function failureTextForTarget(input) {
584
584
  const code = networkErrorCode(input.error, input.capturedOutput);
585
585
  if (code && isLocalSqlAnalysisConnectionRefused({ capturedOutput: input.capturedOutput, fallback: input.fallback })) {
586
586
  return [
587
- `KTX could not reach the local SQL analysis runtime while processing query history for ${input.target.connectionId}.`,
587
+ `ktx could not reach the local SQL analysis runtime while processing query history for ${input.target.connectionId}.`,
588
588
  `Reason: ${NETWORK_ERROR_REASONS[code]} (${code}).`,
589
589
  `Retry: ${retryCommand({
590
590
  projectDir: input.projectDir,
@@ -598,7 +598,7 @@ function failureTextForTarget(input) {
598
598
  if (code) {
599
599
  const operation = input.target.operation === 'database-ingest' ? 'reading schema for' : 'ingesting';
600
600
  return [
601
- `KTX lost its connection to ${friendlyDriverName(input.target.driver)} while ${operation} ${input.target.connectionId}.`,
601
+ `ktx lost its connection to ${friendlyDriverName(input.target.driver)} while ${operation} ${input.target.connectionId}.`,
602
602
  `Reason: ${NETWORK_ERROR_REASONS[code]} (${code}).`,
603
603
  `Retry: ${retryCommand({
604
604
  projectDir: input.projectDir,
@@ -1,5 +1,7 @@
1
1
  import { parseDottedTableEntry } from './context/scan/enabled-tables.js';
2
+ import { createStaticCliSpinner } from './clack.js';
2
3
  import { profileMark } from './startup-profile.js';
4
+ import { withSearchableMultiselectNavigation } from './prompt-navigation.js';
3
5
  import { buildInitialState, buildPickerTree, } from './tree-picker-state.js';
4
6
  import { renderTreePickerTui, } from './tree-picker-tui.js';
5
7
  profileMark('module:database-tree-picker');
@@ -167,7 +169,7 @@ export async function pickDatabaseScope(args, io, render = defaultRenderer) {
167
169
  let selectedSchemas = initialStageOneSchemas(args);
168
170
  while (true) {
169
171
  const pickedSchemas = await args.prompts.autocompleteMultiselect({
170
- message: `Choose ${args.schemaNounPlural} to enable for ${args.connectionId}\nType to filter. Space to select. Enter when done.`,
172
+ message: withSearchableMultiselectNavigation(`Choose ${args.schemaNounPlural} to enable for ${args.connectionId}`),
171
173
  placeholder: `Search ${args.schemaNounPlural}`,
172
174
  options: schemaOptions(args),
173
175
  initialValues: selectedSchemas,
@@ -178,7 +180,7 @@ export async function pickDatabaseScope(args, io, render = defaultRenderer) {
178
180
  }
179
181
  selectedSchemas = pickedSchemas;
180
182
  if (selectedSchemas.length === 0) {
181
- io.stderr.write(`Nothing selected - type to filter, or Escape to skip ${args.schemaNoun} scope.\n`);
183
+ io.stderr.write(`Nothing selected - type to search, or Escape to skip ${args.schemaNoun} scope.\n`);
182
184
  continue;
183
185
  }
184
186
  const selectedNoun = selectedSchemas.length === 1 ? args.schemaNoun : args.schemaNounPlural;
@@ -193,7 +195,20 @@ export async function pickDatabaseScope(args, io, render = defaultRenderer) {
193
195
  if (action === 'back') {
194
196
  continue;
195
197
  }
196
- const discovered = await args.listTablesForSchemas(selectedSchemas);
198
+ // Static (stderr-only) spinner: the stage-two table picker below is a raw-mode
199
+ // Ink TUI, and an animated clack spinner would leave stdin dirty so Ink reads a
200
+ // stray Escape and exits immediately.
201
+ const tablesSpinner = createStaticCliSpinner(io);
202
+ tablesSpinner.start(`Listing tables in ${selectedSchemas.length} ${selectedNoun}…`);
203
+ let discovered;
204
+ try {
205
+ discovered = await args.listTablesForSchemas(selectedSchemas);
206
+ }
207
+ catch (error) {
208
+ tablesSpinner.error('Could not list tables');
209
+ throw error;
210
+ }
211
+ tablesSpinner.stop(`Found ${discovered.length} ${discovered.length === 1 ? 'table' : 'tables'}`);
197
212
  if (action === 'save' && args.existing.enabledTables.length === 0) {
198
213
  return {
199
214
  kind: 'selected',
@@ -46,7 +46,6 @@ function demoConfig(databasePath) {
46
46
  ' state: sqlite',
47
47
  ' search: sqlite-fts5',
48
48
  ' git:',
49
- ' auto_commit: true',
50
49
  ' author: ktx <ktx@example.com>',
51
50
  'llm:',
52
51
  ' provider:',
package/dist/doctor.d.ts CHANGED
@@ -67,7 +67,7 @@ interface RenderOptions {
67
67
  }
68
68
  export declare function formatDoctorReport(report: DoctorReport, options?: Partial<RenderOptions>): string;
69
69
  export declare function renderInvalidConfigMessage(projectDir: string, issues: KtxConfigIssue[], outputMode: KtxDoctorOutputMode, io: KtxDoctorIo): void;
70
- export declare function renderValidConfigMessage(projectDir: string, outputMode: KtxDoctorOutputMode, io: KtxDoctorIo): void;
70
+ export declare function renderValidConfigMessage(projectDir: string, outputMode: KtxDoctorOutputMode, io: KtxDoctorIo, warnings?: KtxConfigIssue[]): void;
71
71
  export declare function renderMissingProjectMessage(projectDir: string, outputMode: KtxDoctorOutputMode, io: KtxDoctorIo): void;
72
72
  export declare function runKtxDoctor(args: KtxDoctorArgs, io?: KtxDoctorIo, deps?: KtxDoctorDeps): Promise<number>;
73
73
  export {};