@kaelio/ktx 0.13.1 → 0.15.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 (261) hide show
  1. package/assets/python/{kaelio_ktx-0.13.1-py3-none-any.whl → kaelio_ktx-0.15.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/cli-program.js +1 -1
  5. package/dist/commands/ingest-commands.d.ts +9 -0
  6. package/dist/commands/ingest-commands.js +37 -1
  7. package/dist/commands/knowledge-commands.js +3 -0
  8. package/dist/commands/setup-commands.js +15 -1
  9. package/dist/connection-drivers.d.ts +2 -0
  10. package/dist/connection-drivers.js +7 -3
  11. package/dist/connection.d.ts +3 -0
  12. package/dist/connection.js +27 -0
  13. package/dist/connectors/bigquery/connector.d.ts +17 -6
  14. package/dist/connectors/bigquery/connector.js +152 -76
  15. package/dist/connectors/bigquery/dialect.d.ts +2 -2
  16. package/dist/connectors/clickhouse/connector.d.ts +1 -0
  17. package/dist/connectors/clickhouse/connector.js +45 -16
  18. package/dist/connectors/clickhouse/dialect.d.ts +2 -2
  19. package/dist/connectors/mongodb/connector.d.ts +69 -0
  20. package/dist/connectors/mongodb/connector.js +307 -0
  21. package/dist/connectors/mongodb/dialect.d.ts +17 -0
  22. package/dist/connectors/mongodb/dialect.js +50 -0
  23. package/dist/connectors/mongodb/live-database-introspection.d.ts +10 -0
  24. package/dist/connectors/mongodb/live-database-introspection.js +24 -0
  25. package/dist/connectors/mongodb/schema-inference.d.ts +20 -0
  26. package/dist/connectors/mongodb/schema-inference.js +110 -0
  27. package/dist/connectors/mysql/connector.d.ts +1 -0
  28. package/dist/connectors/mysql/connector.js +18 -2
  29. package/dist/connectors/mysql/dialect.d.ts +2 -2
  30. package/dist/connectors/postgres/connector.d.ts +1 -0
  31. package/dist/connectors/postgres/connector.js +20 -3
  32. package/dist/connectors/postgres/dialect.d.ts +2 -2
  33. package/dist/connectors/snowflake/connector.d.ts +1 -0
  34. package/dist/connectors/snowflake/connector.js +27 -3
  35. package/dist/connectors/snowflake/dialect.d.ts +2 -2
  36. package/dist/connectors/sqlite/connector.d.ts +11 -1
  37. package/dist/connectors/sqlite/connector.js +120 -17
  38. package/dist/connectors/sqlite/dialect.d.ts +2 -2
  39. package/dist/connectors/sqlite/read-query-child.d.ts +1 -0
  40. package/dist/connectors/sqlite/read-query-child.js +23 -0
  41. package/dist/connectors/sqlserver/connector.d.ts +2 -0
  42. package/dist/connectors/sqlserver/connector.js +20 -3
  43. package/dist/connectors/sqlserver/dialect.d.ts +2 -2
  44. package/dist/context/cache/content-result-cache.d.ts +36 -0
  45. package/dist/context/cache/content-result-cache.js +14 -0
  46. package/dist/context/cache/sqlite-content-result-cache.d.ts +18 -0
  47. package/dist/context/cache/sqlite-content-result-cache.js +223 -0
  48. package/dist/context/connections/bigquery-identifiers.d.ts +1 -0
  49. package/dist/context/connections/bigquery-identifiers.js +7 -0
  50. package/dist/context/connections/configured-connections.d.ts +9 -0
  51. package/dist/context/connections/configured-connections.js +18 -0
  52. package/dist/context/connections/dialects.d.ts +30 -4
  53. package/dist/context/connections/dialects.js +38 -11
  54. package/dist/context/connections/drivers.js +21 -0
  55. package/dist/context/connections/gdrive-config.d.ts +19 -0
  56. package/dist/context/connections/gdrive-config.js +57 -0
  57. package/dist/context/connections/query-deadline.d.ts +31 -0
  58. package/dist/context/connections/query-deadline.js +37 -0
  59. package/dist/context/ingest/adapters/gdrive/chunk.d.ts +3 -0
  60. package/dist/context/ingest/adapters/gdrive/chunk.js +74 -0
  61. package/dist/context/ingest/adapters/gdrive/detect.d.ts +1 -0
  62. package/dist/context/ingest/adapters/gdrive/detect.js +20 -0
  63. package/dist/context/ingest/adapters/gdrive/fetch.d.ts +6 -0
  64. package/dist/context/ingest/adapters/gdrive/fetch.js +95 -0
  65. package/dist/context/ingest/adapters/gdrive/gdrive-client.d.ts +30 -0
  66. package/dist/context/ingest/adapters/gdrive/gdrive-client.js +132 -0
  67. package/dist/context/ingest/adapters/gdrive/gdrive.adapter.d.ts +11 -0
  68. package/dist/context/ingest/adapters/gdrive/gdrive.adapter.js +27 -0
  69. package/dist/context/ingest/adapters/gdrive/normalize.d.ts +2 -0
  70. package/dist/context/ingest/adapters/gdrive/normalize.js +256 -0
  71. package/dist/context/ingest/adapters/gdrive/types.d.ts +153 -0
  72. package/dist/context/ingest/adapters/gdrive/types.js +36 -0
  73. package/dist/context/ingest/adapters/live-database/daemon-introspection.js +24 -0
  74. package/dist/context/ingest/adapters/live-database/fetch-report.d.ts +8 -0
  75. package/dist/context/ingest/adapters/live-database/fetch-report.js +37 -0
  76. package/dist/context/ingest/adapters/live-database/live-database.adapter.d.ts +2 -1
  77. package/dist/context/ingest/adapters/live-database/live-database.adapter.js +9 -2
  78. package/dist/context/ingest/adapters/live-database/manifest.d.ts +2 -0
  79. package/dist/context/ingest/adapters/live-database/manifest.js +8 -4
  80. package/dist/context/ingest/adapters/live-database/scan-outcome.d.ts +17 -0
  81. package/dist/context/ingest/adapters/live-database/scan-outcome.js +40 -0
  82. package/dist/context/ingest/adapters/live-database/stage.js +5 -5
  83. package/dist/context/ingest/adapters/metabase/types.d.ts +1 -1
  84. package/dist/context/ingest/adapters/metabase/types.js +1 -1
  85. package/dist/context/ingest/adapters/sigma/chunk.d.ts +6 -0
  86. package/dist/context/ingest/adapters/sigma/chunk.js +119 -0
  87. package/dist/context/ingest/adapters/sigma/client-port.d.ts +45 -0
  88. package/dist/context/ingest/adapters/sigma/client-port.js +1 -0
  89. package/dist/context/ingest/adapters/sigma/client.d.ts +33 -0
  90. package/dist/context/ingest/adapters/sigma/client.js +176 -0
  91. package/dist/context/ingest/adapters/sigma/detect.d.ts +1 -0
  92. package/dist/context/ingest/adapters/sigma/detect.js +23 -0
  93. package/dist/context/ingest/adapters/sigma/fetch.d.ts +14 -0
  94. package/dist/context/ingest/adapters/sigma/fetch.js +191 -0
  95. package/dist/context/ingest/adapters/sigma/local-sigma.adapter.d.ts +17 -0
  96. package/dist/context/ingest/adapters/sigma/local-sigma.adapter.js +41 -0
  97. package/dist/context/ingest/adapters/sigma/project.d.ts +8 -0
  98. package/dist/context/ingest/adapters/sigma/project.js +186 -0
  99. package/dist/context/ingest/adapters/sigma/sigma.adapter.d.ts +18 -0
  100. package/dist/context/ingest/adapters/sigma/sigma.adapter.js +43 -0
  101. package/dist/context/ingest/adapters/sigma/types.d.ts +80 -0
  102. package/dist/context/ingest/adapters/sigma/types.js +82 -0
  103. package/dist/context/ingest/artifact-gates.d.ts +32 -1
  104. package/dist/context/ingest/artifact-gates.js +85 -18
  105. package/dist/context/ingest/final-gate-prune.d.ts +35 -0
  106. package/dist/context/ingest/final-gate-prune.js +229 -0
  107. package/dist/context/ingest/ingest-bundle.runner.d.ts +3 -0
  108. package/dist/context/ingest/ingest-bundle.runner.js +459 -240
  109. package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +0 -11
  110. package/dist/context/ingest/isolated-diff/patch-integrator.js +0 -44
  111. package/dist/context/ingest/isolated-diff/work-unit-executor.d.ts +1 -0
  112. package/dist/context/ingest/isolated-diff/work-unit-executor.js +13 -4
  113. package/dist/context/ingest/local-adapters.d.ts +2 -1
  114. package/dist/context/ingest/local-adapters.js +28 -0
  115. package/dist/context/ingest/local-bundle-runtime.js +7 -2
  116. package/dist/context/ingest/local-ingest.js +1 -1
  117. package/dist/context/ingest/local-stage-ingest.d.ts +3 -1
  118. package/dist/context/ingest/local-stage-ingest.js +3 -1
  119. package/dist/context/ingest/ports.d.ts +3 -0
  120. package/dist/context/ingest/report-snapshot.js +13 -3
  121. package/dist/context/ingest/reports.d.ts +3 -3
  122. package/dist/context/ingest/reports.js +3 -1
  123. package/dist/context/ingest/stages/build-wu-context.d.ts +1 -0
  124. package/dist/context/ingest/stages/build-wu-context.js +2 -1
  125. package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -1
  126. package/dist/context/ingest/stages/stage-3-work-units.js +4 -10
  127. package/dist/context/ingest/stages/validate-wu-sources.d.ts +12 -0
  128. package/dist/context/ingest/stages/validate-wu-sources.js +23 -8
  129. package/dist/context/ingest/tools/read-raw-file.tool.js +10 -5
  130. package/dist/context/ingest/tools/read-raw-span.tool.js +10 -5
  131. package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +1 -1
  132. package/dist/context/ingest/tools/warehouse-verification/entity-details.tool.js +1 -1
  133. package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +1 -1
  134. package/dist/context/ingest/types.d.ts +3 -0
  135. package/dist/context/ingest/wiki-body-refs.d.ts +28 -0
  136. package/dist/context/ingest/wiki-body-refs.js +37 -8
  137. package/dist/context/ingest/wiki-sl-ref-repair.d.ts +1 -0
  138. package/dist/context/ingest/wiki-sl-ref-repair.js +4 -0
  139. package/dist/context/ingest/work-unit-cache.d.ts +67 -0
  140. package/dist/context/ingest/work-unit-cache.js +230 -0
  141. package/dist/context/llm/ai-sdk-runtime.d.ts +1 -0
  142. package/dist/context/llm/ai-sdk-runtime.js +5 -0
  143. package/dist/context/llm/claude-code-runtime.d.ts +4 -1
  144. package/dist/context/llm/claude-code-runtime.js +38 -8
  145. package/dist/context/llm/codex-runtime.d.ts +4 -1
  146. package/dist/context/llm/codex-runtime.js +48 -35
  147. package/dist/context/llm/runtime-port.d.ts +26 -0
  148. package/dist/context/llm/subprocess-generate-object-child.d.ts +1 -0
  149. package/dist/context/llm/subprocess-generate-object-child.js +34 -0
  150. package/dist/context/llm/subprocess-generate-object.d.ts +42 -0
  151. package/dist/context/llm/subprocess-generate-object.js +122 -0
  152. package/dist/context/mcp/context-tools.d.ts +2 -0
  153. package/dist/context/mcp/context-tools.js +111 -17
  154. package/dist/context/mcp/local-project-ports.d.ts +7 -0
  155. package/dist/context/mcp/local-project-ports.js +29 -0
  156. package/dist/context/mcp/logger.d.ts +24 -0
  157. package/dist/context/mcp/logger.js +49 -0
  158. package/dist/context/mcp/server.js +2 -0
  159. package/dist/context/mcp/types.d.ts +17 -0
  160. package/dist/context/memory/memory-agent.service.js +1 -1
  161. package/dist/context/project/config.d.ts +72 -0
  162. package/dist/context/project/config.js +5 -0
  163. package/dist/context/project/driver-schemas.d.ts +35 -0
  164. package/dist/context/project/driver-schemas.js +89 -1
  165. package/dist/context/project/project.js +1 -1
  166. package/dist/context/project/setup-config.js +1 -0
  167. package/dist/context/scan/description-generation.d.ts +4 -0
  168. package/dist/context/scan/description-generation.js +100 -13
  169. package/dist/context/scan/enabled-tables.d.ts +5 -0
  170. package/dist/context/scan/enabled-tables.js +13 -1
  171. package/dist/context/scan/enrichment-state.d.ts +50 -7
  172. package/dist/context/scan/enrichment-state.js +30 -13
  173. package/dist/context/scan/local-enrichment-artifacts.d.ts +41 -0
  174. package/dist/context/scan/local-enrichment-artifacts.js +155 -32
  175. package/dist/context/scan/local-enrichment.d.ts +39 -4
  176. package/dist/context/scan/local-enrichment.js +345 -92
  177. package/dist/context/scan/local-scan.d.ts +4 -1
  178. package/dist/context/scan/local-scan.js +54 -13
  179. package/dist/context/scan/local-structural-artifacts.d.ts +3 -1
  180. package/dist/context/scan/local-structural-artifacts.js +5 -0
  181. package/dist/context/scan/object-introspection.d.ts +21 -0
  182. package/dist/context/scan/object-introspection.js +35 -0
  183. package/dist/context/scan/relationship-benchmarks.js +5 -3
  184. package/dist/context/scan/relationship-composite-candidates.d.ts +6 -3
  185. package/dist/context/scan/relationship-composite-candidates.js +13 -1
  186. package/dist/context/scan/relationship-detection-budget.d.ts +35 -0
  187. package/dist/context/scan/relationship-detection-budget.js +53 -0
  188. package/dist/context/scan/relationship-diagnostics.d.ts +5 -0
  189. package/dist/context/scan/relationship-diagnostics.js +2 -0
  190. package/dist/context/scan/relationship-discovery.d.ts +9 -3
  191. package/dist/context/scan/relationship-discovery.js +27 -4
  192. package/dist/context/scan/relationship-llm-proposal.js +36 -27
  193. package/dist/context/scan/relationship-profiling.d.ts +7 -3
  194. package/dist/context/scan/relationship-profiling.js +53 -41
  195. package/dist/context/scan/relationship-validation.d.ts +6 -4
  196. package/dist/context/scan/relationship-validation.js +46 -32
  197. package/dist/context/scan/sqlite-local-enrichment-state-store.d.ts +8 -1
  198. package/dist/context/scan/sqlite-local-enrichment-state-store.js +71 -159
  199. package/dist/context/scan/types.d.ts +5 -3
  200. package/dist/context/scan/types.js +2 -0
  201. package/dist/context/sl/local-query.js +5 -0
  202. package/dist/context/sl/pglite-sl-search-prototype.js +1 -1
  203. package/dist/context/sl/semantic-layer.service.js +1 -1
  204. package/dist/context/sl/source-files.d.ts +5 -0
  205. package/dist/context/sl/source-files.js +17 -11
  206. package/dist/context/sl/tools/connection-id-schema.js +1 -1
  207. package/dist/context/sql-analysis/dialect-notes.d.ts +9 -0
  208. package/dist/context/sql-analysis/dialect-notes.js +40 -0
  209. package/dist/context/sql-analysis/dialects/bigquery.md +13 -0
  210. package/dist/context/sql-analysis/dialects/clickhouse.md +9 -0
  211. package/dist/context/sql-analysis/dialects/mysql.md +9 -0
  212. package/dist/context/sql-analysis/dialects/postgres.md +10 -0
  213. package/dist/context/sql-analysis/dialects/snowflake.md +10 -0
  214. package/dist/context/sql-analysis/dialects/sqlite.md +11 -0
  215. package/dist/context/sql-analysis/dialects/tsql.md +10 -0
  216. package/dist/context/wiki/keys.js +1 -1
  217. package/dist/context/wiki/knowledge-wiki.service.d.ts +4 -4
  218. package/dist/context/wiki/knowledge-wiki.service.js +2 -1
  219. package/dist/context/wiki/local-knowledge.d.ts +13 -0
  220. package/dist/context/wiki/local-knowledge.js +50 -6
  221. package/dist/context/wiki/sqlite-knowledge-index.d.ts +2 -0
  222. package/dist/context/wiki/sqlite-knowledge-index.js +21 -5
  223. package/dist/context/wiki/tools/wiki-write.tool.d.ts +2 -0
  224. package/dist/context/wiki/tools/wiki-write.tool.js +27 -0
  225. package/dist/context/wiki/types.d.ts +6 -0
  226. package/dist/context-build-view.d.ts +3 -0
  227. package/dist/context-build-view.js +1 -0
  228. package/dist/knowledge.d.ts +2 -0
  229. package/dist/knowledge.js +12 -1
  230. package/dist/local-adapters.js +11 -0
  231. package/dist/mcp-http-server.js +15 -1
  232. package/dist/mcp-server-factory.d.ts +2 -0
  233. package/dist/mcp-server-factory.js +15 -1
  234. package/dist/mcp-stdio-server.js +10 -2
  235. package/dist/notion-page-picker.js +1 -1
  236. package/dist/prompts/memory_agent_external_ingest.md +2 -0
  237. package/dist/public-ingest.d.ts +3 -1
  238. package/dist/public-ingest.js +4 -0
  239. package/dist/scan.d.ts +3 -1
  240. package/dist/scan.js +7 -0
  241. package/dist/setup-databases.d.ts +1 -1
  242. package/dist/setup-databases.js +43 -1
  243. package/dist/setup-sources.d.ts +6 -1
  244. package/dist/setup-sources.js +208 -48
  245. package/dist/setup.d.ts +3 -0
  246. package/dist/setup.js +6 -1
  247. package/dist/skills/analytics/SKILL.md +200 -2
  248. package/dist/skills/gdrive_synthesize/SKILL.md +97 -0
  249. package/dist/skills/sigma_ingest/SKILL.md +189 -0
  250. package/dist/skills/wiki_capture/SKILL.md +24 -0
  251. package/dist/sql.js +4 -0
  252. package/dist/status-project.d.ts +4 -0
  253. package/dist/status-project.js +54 -2
  254. package/dist/telemetry/events.d.ts +2 -2
  255. package/dist/text-ingest.d.ts +4 -0
  256. package/dist/text-ingest.js +58 -29
  257. package/dist/verbatim-ingest.d.ts +46 -0
  258. package/dist/verbatim-ingest.js +193 -0
  259. package/package.json +5 -1
  260. package/dist/context/ingest/final-gate-repair.d.ts +0 -28
  261. package/dist/context/ingest/final-gate-repair.js +0 -91
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { KtxSubprocessDeadlineError, runGenerateObjectInSubprocess, } from '../../context/llm/subprocess-generate-object.js';
2
3
  const batchedTableDescriptionSchema = z.object({
3
4
  tableDescription: z.string(),
4
5
  columns: z.array(z.object({
@@ -281,11 +282,13 @@ export class KtxDescriptionGenerator {
281
282
  logger;
282
283
  onWarning;
283
284
  settings;
285
+ spawnSubprocessGenerateChild;
284
286
  constructor(options) {
285
287
  this.llmRuntime = options.llmRuntime;
286
288
  this.cache = options.cache;
287
289
  this.logger = options.logger;
288
290
  this.onWarning = options.onWarning;
291
+ this.spawnSubprocessGenerateChild = options.spawnSubprocessGenerateChild;
289
292
  this.settings = {
290
293
  columnMaxWords: options.settings.columnMaxWords,
291
294
  tableMaxWords: options.settings.tableMaxWords,
@@ -500,6 +503,20 @@ export class KtxDescriptionGenerator {
500
503
  const descriptions = new Map();
501
504
  let tableDescription = null;
502
505
  let structuredGenerationSucceeded = false;
506
+ // Bound + retry the per-table enrichment LLM call. A transient backend error
507
+ // (e.g. an "overloaded"/burst rejection when many tables enrich concurrently)
508
+ // otherwise nulls a whole table's descriptions on the FIRST failure — sampleTable
509
+ // already retries, this call did not, so transient errors silently dropped most
510
+ // tables of a db. retryAsync gives it the same 3-attempt backoff. A FRESH timeout
511
+ // per attempt still bounds a wedged wide table (it never returns a result message);
512
+ // a timeout is surfaced as KtxAbortedError so retryAsync does NOT retry it (one
513
+ // wedge stays one timeout, not 3×). Tune via KTX_ENRICH_LLM_TIMEOUT_MS (default
514
+ // 120s) and KTX_ENRICH_LLM_ATTEMPTS (default 3).
515
+ const rawEnrichTimeoutMs = Number(process.env.KTX_ENRICH_LLM_TIMEOUT_MS);
516
+ const enrichTimeoutMs = Number.isFinite(rawEnrichTimeoutMs) && rawEnrichTimeoutMs > 0 ? rawEnrichTimeoutMs : 120_000;
517
+ const enrichAttempts = Math.max(1, Number(process.env.KTX_ENRICH_LLM_ATTEMPTS ?? 3) || 3);
518
+ let llmStartedAt = 0;
519
+ let lastTimedOut = false;
503
520
  try {
504
521
  const prompt = batchedPrompt({
505
522
  table: input.table,
@@ -508,12 +525,78 @@ export class KtxDescriptionGenerator {
508
525
  tableMaxWords: this.settings.tableMaxWords,
509
526
  columnMaxWords: this.settings.columnMaxWords,
510
527
  });
511
- const generated = await this.llmRuntime.generateObject({
512
- role: 'candidateExtraction',
513
- system: prompt.system,
514
- prompt: prompt.user,
515
- schema: batchedTableDescriptionSchema,
516
- temperature: this.settings.temperature,
528
+ llmStartedAt = Date.now();
529
+ this.logger?.info(`[enrich] llm:start table=${input.table.name} cols=${input.table.columns.length} promptChars=${prompt.user.length} timeoutMs=${enrichTimeoutMs} attempts=${enrichAttempts}`, { connectorId: input.connector.id, table: input.table.name, columns: input.table.columns.length });
530
+ // Subprocess backends (codex/claude-code) own an SDK child that ignores the
531
+ // in-process abort, so each attempt runs behind a tree-killable boundary;
532
+ // HTTP backends keep the native abortSignal -> fetch cancellation.
533
+ const enrichForkSpec = this.llmRuntime.subprocessForkSpec();
534
+ const enrichJsonSchema = enrichForkSpec
535
+ ? z.toJSONSchema(batchedTableDescriptionSchema, { target: 'draft-7' })
536
+ : null;
537
+ const generated = await retryAsync(async () => {
538
+ if (enrichForkSpec && enrichJsonSchema) {
539
+ try {
540
+ return await runGenerateObjectInSubprocess({
541
+ forkSpec: enrichForkSpec,
542
+ role: 'candidateExtraction',
543
+ system: prompt.system,
544
+ prompt: prompt.user,
545
+ schema: batchedTableDescriptionSchema,
546
+ jsonSchema: enrichJsonSchema,
547
+ deadlineMs: enrichTimeoutMs,
548
+ ...(input.context.signal ? { signal: input.context.signal } : {}),
549
+ ...(this.spawnSubprocessGenerateChild
550
+ ? { spawnChild: this.spawnSubprocessGenerateChild }
551
+ : {}),
552
+ });
553
+ }
554
+ catch (error) {
555
+ // The boundary tree-kills the wedged child on deadline; a per-table
556
+ // timeout is not worth retrying (it would just time out again), so
557
+ // surface it as KtxAbortedError so retryAsync stops immediately.
558
+ if (error instanceof KtxSubprocessDeadlineError && !input.context.signal?.aborted) {
559
+ lastTimedOut = true;
560
+ throw new KtxAbortedError();
561
+ }
562
+ throw error;
563
+ }
564
+ }
565
+ const enrichTimeout = AbortSignal.timeout(enrichTimeoutMs);
566
+ const abortSignal = input.context.signal
567
+ ? AbortSignal.any([enrichTimeout, input.context.signal])
568
+ : enrichTimeout;
569
+ try {
570
+ return await this.llmRuntime.generateObject({
571
+ role: 'candidateExtraction',
572
+ system: prompt.system,
573
+ prompt: prompt.user,
574
+ schema: batchedTableDescriptionSchema,
575
+ temperature: this.settings.temperature,
576
+ abortSignal,
577
+ });
578
+ }
579
+ catch (error) {
580
+ // A per-table timeout is not worth retrying (it would just time out
581
+ // again); surface it as KtxAbortedError so retryAsync stops immediately.
582
+ // A genuine context cancellation is handled by retryAsync's own signal check.
583
+ if (enrichTimeout.aborted && !input.context.signal?.aborted) {
584
+ lastTimedOut = true;
585
+ throw new KtxAbortedError();
586
+ }
587
+ throw error;
588
+ }
589
+ }, {
590
+ attempts: enrichAttempts,
591
+ baseDelayMs: 500,
592
+ ...(input.context.signal ? { signal: input.context.signal } : {}),
593
+ onAttemptFailure: (error, attempt) => {
594
+ this.logger?.warn(`[enrich] llm:retry table=${input.table.name} attempt=${attempt}: ${errorMessage(error)}`, { connectorId: input.connector.id, table: input.table.name, attempt });
595
+ },
596
+ });
597
+ this.logger?.info(`[enrich] llm:done table=${input.table.name} ms=${Date.now() - llmStartedAt}`, {
598
+ connectorId: input.connector.id,
599
+ table: input.table.name,
517
600
  });
518
601
  structuredGenerationSucceeded = true;
519
602
  tableDescription = generated.tableDescription.trim() || null;
@@ -533,16 +616,20 @@ export class KtxDescriptionGenerator {
533
616
  }
534
617
  }
535
618
  catch (error) {
536
- this.logger?.warn(`Batched table description failed for ${input.table.name}: ${errorMessage(error)}`, {
537
- connectorId: input.connector.id,
538
- table: input.table.name,
539
- });
619
+ // A genuine cancellation propagates so the stage fails and resumes; a
620
+ // per-table timeout (context.signal not aborted) still degrades to null.
621
+ if (input.context.signal?.aborted) {
622
+ throw error;
623
+ }
624
+ const elapsedMs = llmStartedAt ? Date.now() - llmStartedAt : 0;
625
+ const timedOut = lastTimedOut;
626
+ this.logger?.warn(`[enrich] llm:${timedOut ? 'TIMEOUT' : 'fail'} table=${input.table.name} cols=${input.table.columns.length} ms=${elapsedMs}: ${errorMessage(error)}`, { connectorId: input.connector.id, table: input.table.name, timedOut, elapsedMs });
540
627
  this.onWarning?.({
541
- code: 'enrichment_failed',
542
- message: `Failed to generate batched description for table ${input.table.name}: ${errorMessage(error)}`,
628
+ code: timedOut ? 'enrichment_timeout' : 'enrichment_failed',
629
+ message: `${timedOut ? `Timed out after ${elapsedMs}ms generating` : 'Failed to generate'} batched description for table ${input.table.name}: ${errorMessage(error)}`,
543
630
  table: input.table.name,
544
631
  recoverable: true,
545
- metadata: { connectorId: input.connector.id },
632
+ metadata: { connectorId: input.connector.id, ...(timedOut ? { timeoutMs: enrichTimeoutMs } : {}) },
546
633
  });
547
634
  }
548
635
  if (!structuredGenerationSucceeded) {
@@ -9,6 +9,11 @@ import type { KtxTableRef } from './types.js';
9
9
  * "catalog.db.name" — fully qualified
10
10
  * "db.name" — schema-qualified (catalog = null)
11
11
  * "name" — bare (catalog = db = null; SQLite-shape)
12
+ *
13
+ * SQLite exposes a single schema named `main` but the connector emits objects
14
+ * with `db: null`, so the `"main.<name>"` form is normalized to the bare shape
15
+ * to match. Both `"main.customers"` and `"customers"` therefore select the same
16
+ * object.
12
17
  */
13
18
  export declare function resolveEnabledTables(connection: Record<string, unknown> | undefined): ReadonlySet<KtxTableRefKey> | null;
14
19
  /** @internal */
@@ -8,21 +8,33 @@ import { tableRefSet } from './table-ref.js';
8
8
  * "catalog.db.name" — fully qualified
9
9
  * "db.name" — schema-qualified (catalog = null)
10
10
  * "name" — bare (catalog = db = null; SQLite-shape)
11
+ *
12
+ * SQLite exposes a single schema named `main` but the connector emits objects
13
+ * with `db: null`, so the `"main.<name>"` form is normalized to the bare shape
14
+ * to match. Both `"main.customers"` and `"customers"` therefore select the same
15
+ * object.
11
16
  */
12
17
  export function resolveEnabledTables(connection) {
13
18
  const raw = connection?.enabled_tables;
14
19
  if (!Array.isArray(raw) || raw.length === 0)
15
20
  return null;
21
+ const driver = typeof connection?.driver === 'string' ? connection.driver : undefined;
16
22
  const refs = [];
17
23
  for (const value of raw) {
18
24
  const parsed = parseEnabledTableEntry(value);
19
25
  if (parsed)
20
- refs.push(parsed);
26
+ refs.push(normalizeRefForDriver(parsed, driver));
21
27
  }
22
28
  if (refs.length === 0)
23
29
  return null;
24
30
  return tableRefSet(refs);
25
31
  }
32
+ function normalizeRefForDriver(ref, driver) {
33
+ if (driver === 'sqlite' && ref.catalog === null && ref.db === 'main') {
34
+ return { catalog: null, db: null, name: ref.name };
35
+ }
36
+ return ref;
37
+ }
26
38
  function parseEnabledTableEntry(value) {
27
39
  if (typeof value === 'string') {
28
40
  return parseDottedTableEntry(value);
@@ -1,6 +1,12 @@
1
+ import type { KtxScanRelationshipConfig } from '../project/config.js';
1
2
  import type { KtxScanEnrichmentStage, KtxScanEnrichmentStateSummary, KtxScanMode, KtxSchemaSnapshot } from './types.js';
3
+ /**
4
+ * Canonical enrichment-stage registry. The `--stages` CLI parser validates
5
+ * against this list, and stage selection / iteration derives its order here.
6
+ */
7
+ export declare const KTX_SCAN_ENRICHMENT_STAGES: readonly KtxScanEnrichmentStage[];
2
8
  export interface KtxScanEnrichmentStageLookup {
3
- runId: string;
9
+ connectionId: string;
4
10
  stage: KtxScanEnrichmentStage;
5
11
  inputHash: string;
6
12
  }
@@ -31,17 +37,54 @@ export interface KtxScanEnrichmentFailedStage {
31
37
  export type KtxScanEnrichmentStageRecord<TOutput = unknown> = KtxScanEnrichmentCompletedStage<TOutput> | KtxScanEnrichmentFailedStage;
32
38
  export interface KtxScanEnrichmentStateStore {
33
39
  findCompletedStage<TOutput = unknown>(input: KtxScanEnrichmentStageLookup): Promise<KtxScanEnrichmentCompletedStage<TOutput> | null>;
40
+ /**
41
+ * The most recently completed row for a (connection, stage) pair regardless of
42
+ * input hash. Used by the staleness check to compare a stage's stored hash
43
+ * against its freshly recomputed one (D4).
44
+ */
45
+ findLatestCompletedStage(input: {
46
+ connectionId: string;
47
+ stage: KtxScanEnrichmentStage;
48
+ }): Promise<KtxScanEnrichmentCompletedStage | null>;
34
49
  saveCompletedStage<TOutput = unknown>(input: Omit<KtxScanEnrichmentCompletedStage<TOutput>, 'status' | 'errorMessage'>): Promise<void>;
35
50
  saveFailedStage(input: Omit<KtxScanEnrichmentFailedStage, 'status' | 'output'>): Promise<void>;
36
51
  listRunStages(runId: string): Promise<KtxScanEnrichmentStageRecord[]>;
37
52
  }
38
- export interface ComputeKtxScanEnrichmentInputHashInput {
53
+ /** Description-LLM identity: the inputs that change a description's content. */
54
+ export interface KtxScanLlmIdentity {
55
+ model: string | null;
56
+ baseUrlConfigured: boolean;
57
+ }
58
+ /** Embedding-model identity: the inputs that change an embedding vector. */
59
+ export interface KtxScanEmbeddingIdentity {
60
+ model: string | null;
61
+ dimensions: number | null;
62
+ batchSize: number | null;
63
+ }
64
+ export interface KtxDescriptionsStageHashInput {
39
65
  snapshot: KtxSchemaSnapshot;
40
- mode: KtxScanMode;
41
- detectRelationships: boolean;
42
- providerIdentity: Record<string, unknown>;
43
- relationshipSettings?: unknown;
66
+ llmIdentity: KtxScanLlmIdentity;
67
+ }
68
+ export interface KtxEmbeddingsStageHashInput {
69
+ snapshot: KtxSchemaSnapshot;
70
+ embeddingIdentity: KtxScanEmbeddingIdentity;
71
+ /** Digest of the resolved description text the embeddings consume (see {@link computeKtxScanDescriptionDigest}). */
72
+ descriptionDigest: string;
73
+ }
74
+ export interface KtxRelationshipsStageHashInput {
75
+ snapshot: KtxSchemaSnapshot;
76
+ relationshipSettings: KtxScanRelationshipConfig;
77
+ llmIdentity: KtxScanLlmIdentity;
44
78
  }
45
- export declare function computeKtxScanEnrichmentInputHash(input: ComputeKtxScanEnrichmentInputHashInput): string;
79
+ export declare function computeKtxDescriptionsStageHash(input: KtxDescriptionsStageHashInput): string;
80
+ export declare function computeKtxEmbeddingsStageHash(input: KtxEmbeddingsStageHashInput): string;
81
+ export declare function computeKtxRelationshipsStageHash(input: KtxRelationshipsStageHashInput): string;
82
+ /**
83
+ * Content digest of the resolved per-column description text the embeddings
84
+ * stage consumes. Folding it into the embeddings hash content-addresses
85
+ * embeddings on their real upstream, so re-describing busts only the embeddings
86
+ * that depend on the changed text (D4 self-healing).
87
+ */
88
+ export declare function computeKtxScanDescriptionDigest(texts: readonly string[]): string;
46
89
  export declare function completedKtxScanEnrichmentStateSummary(): KtxScanEnrichmentStateSummary;
47
90
  export declare function summarizeKtxScanEnrichmentState(input: KtxScanEnrichmentStateSummary): KtxScanEnrichmentStateSummary;
@@ -1,21 +1,38 @@
1
- import { createHash } from 'node:crypto';
2
- const KTX_SCAN_ENRICHMENT_STAGES = [
1
+ import { stableContentHash } from '../cache/content-result-cache.js';
2
+ /**
3
+ * Canonical enrichment-stage registry. The `--stages` CLI parser validates
4
+ * against this list, and stage selection / iteration derives its order here.
5
+ */
6
+ export const KTX_SCAN_ENRICHMENT_STAGES = [
3
7
  'descriptions',
4
8
  'embeddings',
5
9
  'relationships',
6
10
  ];
7
- function stableJson(value) {
8
- if (Array.isArray(value)) {
9
- return `[${value.map(stableJson).join(',')}]`;
10
- }
11
- if (value && typeof value === 'object') {
12
- const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
13
- return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(',')}}`;
14
- }
15
- return JSON.stringify(value);
11
+ export function computeKtxDescriptionsStageHash(input) {
12
+ return stableContentHash({ snapshot: input.snapshot, llmIdentity: input.llmIdentity });
13
+ }
14
+ export function computeKtxEmbeddingsStageHash(input) {
15
+ return stableContentHash({
16
+ snapshot: input.snapshot,
17
+ embeddingIdentity: input.embeddingIdentity,
18
+ descriptionDigest: input.descriptionDigest,
19
+ });
20
+ }
21
+ export function computeKtxRelationshipsStageHash(input) {
22
+ return stableContentHash({
23
+ snapshot: input.snapshot,
24
+ relationshipSettings: input.relationshipSettings,
25
+ llmIdentity: input.llmIdentity,
26
+ });
16
27
  }
17
- export function computeKtxScanEnrichmentInputHash(input) {
18
- return createHash('sha256').update(stableJson(input)).digest('hex');
28
+ /**
29
+ * Content digest of the resolved per-column description text the embeddings
30
+ * stage consumes. Folding it into the embeddings hash content-addresses
31
+ * embeddings on their real upstream, so re-describing busts only the embeddings
32
+ * that depend on the changed text (D4 self-healing).
33
+ */
34
+ export function computeKtxScanDescriptionDigest(texts) {
35
+ return stableContentHash(texts);
19
36
  }
20
37
  function uniqueStages(stages) {
21
38
  const seen = new Set();
@@ -11,6 +11,12 @@ export interface WriteLocalScanManifestShardsInput {
11
11
  dryRun: boolean;
12
12
  descriptionUpdates?: KtxLocalScanEnrichmentResult['descriptionUpdates'];
13
13
  relationshipUpdate?: KtxLocalScanEnrichmentResult['relationshipUpdate'];
14
+ /**
15
+ * When set, write only the shards that contain one of these tables. All shards
16
+ * are still built (so merging preserves prior content); the unlisted shards are
17
+ * left untouched on disk. Used by the incremental flush to bound git commits.
18
+ */
19
+ onlyChangedTableNames?: ReadonlySet<string>;
14
20
  }
15
21
  export interface WriteLocalScanManifestShardsResult {
16
22
  manifestShards: string[];
@@ -28,5 +34,40 @@ export interface WriteLocalScanEnrichmentArtifactsInput {
28
34
  export interface WriteLocalScanEnrichmentArtifactsResult extends WriteLocalScanManifestShardsResult {
29
35
  enrichmentArtifacts: string[];
30
36
  }
37
+ type LocalDescriptionUpdates = KtxLocalScanEnrichmentResult['descriptionUpdates'];
38
+ /**
39
+ * Reconstructs the descriptions already persisted in the on-disk `_schema` as
40
+ * the in-memory `descriptionUpdates` shape, so a stage-selective run that skips
41
+ * the descriptions stage (e.g. `--stages relationships`/`--stages embeddings`)
42
+ * can still feed embeddings + relationships the prior AI descriptions. Tables or
43
+ * columns with no AI description carry `null`.
44
+ */
45
+ export declare function loadOnDiskDescriptionUpdates(project: KtxLocalProject, connectionId: string, snapshot: KtxSchemaSnapshot): Promise<LocalDescriptionUpdates>;
46
+ export interface KtxScanDescriptionResumeStore {
47
+ /** Prior enriched descriptions when the durable record matches `inputHash`, else null. */
48
+ load(inputHash: string): Promise<LocalDescriptionUpdates | null>;
49
+ /** Persist the descriptions so far + the manifest shards that gained a table this batch. */
50
+ flush(input: {
51
+ inputHash: string;
52
+ snapshot: KtxSchemaSnapshot;
53
+ descriptionUpdates: LocalDescriptionUpdates;
54
+ changedTableNames: ReadonlySet<string>;
55
+ }): Promise<void>;
56
+ }
57
+ export declare function createKtxScanDescriptionResumeStore(deps: {
58
+ project: KtxLocalProject;
59
+ connectionId: string;
60
+ syncId: string;
61
+ driver: KtxConnectionDriver;
62
+ }): KtxScanDescriptionResumeStore;
31
63
  export declare function writeLocalScanManifestShards(input: WriteLocalScanManifestShardsInput): Promise<WriteLocalScanManifestShardsResult>;
64
+ /**
65
+ * Promote the descriptions + embeddings into the queryable `_schema` manifest
66
+ * (and the raw enrichment artifacts) before relationship detection runs. The
67
+ * generated joins and the relationship diagnostics are deliberately left to the
68
+ * final write, so an interrupted relationship stage never loses the paid LLM
69
+ * enrichment and never emits empty relationship diagnostics.
70
+ */
71
+ export declare function writeLocalScanEnrichmentCheckpoint(input: WriteLocalScanEnrichmentArtifactsInput): Promise<WriteLocalScanEnrichmentArtifactsResult>;
32
72
  export declare function writeLocalScanEnrichmentArtifacts(input: WriteLocalScanEnrichmentArtifactsInput): Promise<WriteLocalScanEnrichmentArtifactsResult>;
73
+ export {};
@@ -1,7 +1,8 @@
1
1
  import YAML from 'yaml';
2
- import { buildLiveDatabaseManifestShards } from '../../context/ingest/adapters/live-database/manifest.js';
2
+ import { buildLiveDatabaseManifestShards, buildTableRef } from '../../context/ingest/adapters/live-database/manifest.js';
3
3
  import { isSlYamlPath } from '../../context/sl/source-files.js';
4
4
  import { deriveFederatedConnection } from '../connections/federation.js';
5
+ import { tableRefKey } from './table-ref.js';
5
6
  import { buildKtxRelationshipArtifacts, buildKtxRelationshipDiagnostics, emptyKtxRelationshipProfileArtifact, } from './relationship-diagnostics.js';
6
7
  const LIVE_DATABASE_ADAPTER = 'live-database';
7
8
  const LOCAL_AUTHOR = 'ktx';
@@ -19,8 +20,7 @@ function artifactDir(connectionId, syncId) {
19
20
  function schemaDir(connectionId) {
20
21
  return `${SL_DIR_PREFIX}/${connectionId}/${SCHEMA_DIR}`;
21
22
  }
22
- function tableDescription(table, descriptionUpdates = []) {
23
- const update = descriptionUpdates.find((candidate) => candidate.table.name === table.name);
23
+ function tableDescription(table, update) {
24
24
  const descriptions = {};
25
25
  if (table.comment) {
26
26
  descriptions.db = table.comment;
@@ -30,8 +30,7 @@ function tableDescription(table, descriptionUpdates = []) {
30
30
  }
31
31
  return Object.keys(descriptions).length > 0 ? descriptions : undefined;
32
32
  }
33
- function columnDescription(table, column, descriptionUpdates = []) {
34
- const update = descriptionUpdates.find((candidate) => candidate.table.name === table.name);
33
+ function columnDescription(column, update) {
35
34
  const aiDescription = update?.columnDescriptions[column.name] ?? null;
36
35
  const descriptions = {};
37
36
  if (column.comment) {
@@ -43,19 +42,25 @@ function columnDescription(table, column, descriptionUpdates = []) {
43
42
  return Object.keys(descriptions).length > 0 ? descriptions : undefined;
44
43
  }
45
44
  function snapshotTablesToManifestData(snapshot, descriptionUpdates = []) {
46
- return snapshot.tables.map((table) => ({
47
- name: table.name,
48
- catalog: table.catalog,
49
- db: table.db,
50
- descriptions: tableDescription(table, descriptionUpdates),
51
- columns: table.columns.map((column) => ({
52
- name: column.name,
53
- type: column.dimensionType,
54
- ...(column.primaryKey ? { pk: true } : {}),
55
- ...(column.nullable === false ? { nullable: false } : {}),
56
- descriptions: columnDescription(table, column, descriptionUpdates),
57
- })),
58
- }));
45
+ // Resolve a table's descriptions by full identity: two same-named tables in
46
+ // different schemas must not collapse onto one update.
47
+ const updateByRef = new Map(descriptionUpdates.map((update) => [tableRefKey(update.table), update]));
48
+ return snapshot.tables.map((table) => {
49
+ const update = updateByRef.get(tableRefKey({ catalog: table.catalog, db: table.db, name: table.name }));
50
+ return {
51
+ name: table.name,
52
+ catalog: table.catalog,
53
+ db: table.db,
54
+ descriptions: tableDescription(table, update),
55
+ columns: table.columns.map((column) => ({
56
+ name: column.name,
57
+ type: column.dimensionType,
58
+ ...(column.primaryKey ? { pk: true } : {}),
59
+ ...(column.nullable === false ? { nullable: false } : {}),
60
+ descriptions: columnDescription(column, update),
61
+ })),
62
+ };
63
+ });
59
64
  }
60
65
  function formalJoins(snapshot) {
61
66
  const joins = [];
@@ -163,12 +168,15 @@ async function loadExistingManifestState(project, connectionId, snapshot, siblin
163
168
  if (!validTableNames.has(tableName)) {
164
169
  continue;
165
170
  }
166
- descriptions.set(tableName, {
171
+ // Descriptions/usage key on the fully-qualified `entry.table` ref so two
172
+ // same-named tables across schemas stay distinct; joins remain keyed by
173
+ // bare name to match the bare-name join graph.
174
+ descriptions.set(entry.table, {
167
175
  table: entry.descriptions ? { ...entry.descriptions } : undefined,
168
176
  columns: new Map((entry.columns ?? []).flatMap((column) => column.descriptions ? [[column.name, { ...column.descriptions }]] : [])),
169
177
  });
170
178
  if (entry.usage) {
171
- usage.set(tableName, { ...entry.usage });
179
+ usage.set(entry.table, { ...entry.usage });
172
180
  }
173
181
  const joins = (entry.joins ?? []).filter((join) => {
174
182
  return ((join.source === 'manual' || join.source === 'inferred') &&
@@ -186,6 +194,76 @@ async function loadExistingManifestState(project, connectionId, snapshot, siblin
186
194
  }
187
195
  return { descriptions, preservedJoins, usage };
188
196
  }
197
+ /**
198
+ * Reconstructs the descriptions already persisted in the on-disk `_schema` as
199
+ * the in-memory `descriptionUpdates` shape, so a stage-selective run that skips
200
+ * the descriptions stage (e.g. `--stages relationships`/`--stages embeddings`)
201
+ * can still feed embeddings + relationships the prior AI descriptions. Tables or
202
+ * columns with no AI description carry `null`.
203
+ */
204
+ export async function loadOnDiskDescriptionUpdates(project, connectionId, snapshot) {
205
+ const siblingTargets = await federatedSiblingTargets(project, connectionId);
206
+ const existing = await loadExistingManifestState(project, connectionId, snapshot, siblingTargets);
207
+ return snapshot.tables.map((table) => {
208
+ const entry = existing.descriptions.get(buildTableRef(table.name, table.catalog, table.db));
209
+ const columnDescriptions = {};
210
+ for (const column of table.columns) {
211
+ columnDescriptions[column.name] = entry?.columns.get(column.name)?.ai ?? null;
212
+ }
213
+ return {
214
+ table: { catalog: table.catalog, db: table.db, name: table.name },
215
+ tableDescription: entry?.table?.ai ?? null,
216
+ columnDescriptions,
217
+ };
218
+ });
219
+ }
220
+ // The incremental descriptions resume record. It lives at a stable, NON-syncId
221
+ // path: a from-scratch interruption gets a fresh syncId on the next run, so a
222
+ // syncId-scoped record would be unreachable on resume. The manifest already lives
223
+ // at the same stable per-connection scope.
224
+ function descriptionsProgressPath(connectionId) {
225
+ return `raw-sources/${connectionId}/${LIVE_DATABASE_ADAPTER}/enrichment-progress/descriptions.json`;
226
+ }
227
+ export function createKtxScanDescriptionResumeStore(deps) {
228
+ const path = descriptionsProgressPath(deps.connectionId);
229
+ return {
230
+ async load(inputHash) {
231
+ let content;
232
+ try {
233
+ ({ content } = await deps.project.fileStore.readFile(path));
234
+ }
235
+ catch {
236
+ return null;
237
+ }
238
+ try {
239
+ const record = JSON.parse(content);
240
+ // A changed inputHash (schema or enrichment settings changed) ignores the
241
+ // prior record and recomputes — spec-19's inputHash-gated resume semantics.
242
+ if (!record || record.inputHash !== inputHash || !Array.isArray(record.descriptions)) {
243
+ return null;
244
+ }
245
+ return record.descriptions;
246
+ }
247
+ catch {
248
+ return null;
249
+ }
250
+ },
251
+ async flush({ inputHash, snapshot, descriptionUpdates, changedTableNames }) {
252
+ const record = { inputHash, descriptions: descriptionUpdates };
253
+ await writeJsonArtifact(deps.project, path, record, `scan(${LIVE_DATABASE_ADAPTER}): flush enrichment descriptions progress syncId=${deps.syncId}`);
254
+ await writeLocalScanManifestShards({
255
+ project: deps.project,
256
+ connectionId: deps.connectionId,
257
+ syncId: deps.syncId,
258
+ driver: deps.driver,
259
+ snapshot,
260
+ descriptionUpdates,
261
+ dryRun: false,
262
+ onlyChangedTableNames: changedTableNames,
263
+ });
264
+ },
265
+ };
266
+ }
189
267
  async function writeJsonArtifact(project, path, value, commitMessage) {
190
268
  await project.fileStore.writeFile(path, `${JSON.stringify(value, null, 2)}\n`, LOCAL_AUTHOR, LOCAL_AUTHOR_EMAIL, commitMessage);
191
269
  }
@@ -210,6 +288,9 @@ export async function writeLocalScanManifestShards(input) {
210
288
  });
211
289
  const manifestShards = [];
212
290
  for (const [shardKey, shard] of [...shards.entries()].sort(([left], [right]) => left.localeCompare(right))) {
291
+ if (input.onlyChangedTableNames && !Object.keys(shard.tables).some((table) => input.onlyChangedTableNames.has(table))) {
292
+ continue;
293
+ }
213
294
  const path = `${schemaDir(input.connectionId)}/${shardKey}.yaml`;
214
295
  await input.project.fileStore.writeFile(path, YAML.stringify(shard, { indent: 2, lineWidth: 0, version: '1.1' }), LOCAL_AUTHOR, LOCAL_AUTHOR_EMAIL, `scan(${LIVE_DATABASE_ADAPTER}): write manifest shard ${shardKey} syncId=${input.syncId}`);
215
296
  manifestShards.push(path);
@@ -219,6 +300,53 @@ export async function writeLocalScanManifestShards(input) {
219
300
  manifestShardsWritten: manifestShards.length,
220
301
  };
221
302
  }
303
+ async function writeEnrichmentDescriptionArtifacts(input) {
304
+ const descriptionsArtifact = `${input.enrichmentRoot}/descriptions.json`;
305
+ const embeddingsArtifact = `${input.enrichmentRoot}/embeddings.json`;
306
+ const enrichmentArtifacts = [];
307
+ if (input.enrichment.summary.tableDescriptions === 'completed' ||
308
+ input.enrichment.summary.columnDescriptions === 'completed') {
309
+ enrichmentArtifacts.push(descriptionsArtifact);
310
+ await writeJsonArtifact(input.project, descriptionsArtifact, input.enrichment.descriptionUpdates, `scan(${LIVE_DATABASE_ADAPTER}): write enrichment descriptions syncId=${input.syncId}`);
311
+ }
312
+ if (input.enrichment.summary.embeddings === 'completed') {
313
+ enrichmentArtifacts.push(embeddingsArtifact);
314
+ await writeJsonArtifact(input.project, embeddingsArtifact, input.enrichment.embeddingUpdates, `scan(${LIVE_DATABASE_ADAPTER}): write enrichment embeddings syncId=${input.syncId}`);
315
+ }
316
+ return enrichmentArtifacts;
317
+ }
318
+ /**
319
+ * Promote the descriptions + embeddings into the queryable `_schema` manifest
320
+ * (and the raw enrichment artifacts) before relationship detection runs. The
321
+ * generated joins and the relationship diagnostics are deliberately left to the
322
+ * final write, so an interrupted relationship stage never loses the paid LLM
323
+ * enrichment and never emits empty relationship diagnostics.
324
+ */
325
+ export async function writeLocalScanEnrichmentCheckpoint(input) {
326
+ if (input.dryRun) {
327
+ return { enrichmentArtifacts: [], manifestShards: [], manifestShardsWritten: 0 };
328
+ }
329
+ const enrichmentArtifacts = await writeEnrichmentDescriptionArtifacts({
330
+ project: input.project,
331
+ enrichmentRoot: artifactDir(input.connectionId, input.syncId),
332
+ syncId: input.syncId,
333
+ enrichment: input.enrichment,
334
+ });
335
+ const manifestResult = await writeLocalScanManifestShards({
336
+ project: input.project,
337
+ connectionId: input.connectionId,
338
+ syncId: input.syncId,
339
+ driver: input.driver,
340
+ snapshot: input.enrichment.snapshot,
341
+ descriptionUpdates: input.enrichment.descriptionUpdates,
342
+ dryRun: false,
343
+ });
344
+ return {
345
+ enrichmentArtifacts,
346
+ manifestShards: manifestResult.manifestShards,
347
+ manifestShardsWritten: manifestResult.manifestShardsWritten,
348
+ };
349
+ }
222
350
  export async function writeLocalScanEnrichmentArtifacts(input) {
223
351
  if (input.dryRun) {
224
352
  return {
@@ -228,21 +356,15 @@ export async function writeLocalScanEnrichmentArtifacts(input) {
228
356
  };
229
357
  }
230
358
  const enrichmentRoot = artifactDir(input.connectionId, input.syncId);
231
- const descriptionsArtifact = `${enrichmentRoot}/descriptions.json`;
232
- const embeddingsArtifact = `${enrichmentRoot}/embeddings.json`;
233
359
  const relationshipsArtifact = `${enrichmentRoot}/relationships.json`;
234
360
  const relationshipProfileArtifact = `${enrichmentRoot}/relationship-profile.json`;
235
361
  const relationshipDiagnosticsArtifact = `${enrichmentRoot}/relationship-diagnostics.json`;
236
- const enrichmentArtifacts = [];
237
- if (input.enrichment.summary.tableDescriptions === 'completed' ||
238
- input.enrichment.summary.columnDescriptions === 'completed') {
239
- enrichmentArtifacts.push(descriptionsArtifact);
240
- await writeJsonArtifact(input.project, descriptionsArtifact, input.enrichment.descriptionUpdates, `scan(${LIVE_DATABASE_ADAPTER}): write enrichment descriptions syncId=${input.syncId}`);
241
- }
242
- if (input.enrichment.summary.embeddings === 'completed') {
243
- enrichmentArtifacts.push(embeddingsArtifact);
244
- await writeJsonArtifact(input.project, embeddingsArtifact, input.enrichment.embeddingUpdates, `scan(${LIVE_DATABASE_ADAPTER}): write enrichment embeddings syncId=${input.syncId}`);
245
- }
362
+ const enrichmentArtifacts = await writeEnrichmentDescriptionArtifacts({
363
+ project: input.project,
364
+ enrichmentRoot,
365
+ syncId: input.syncId,
366
+ enrichment: input.enrichment,
367
+ });
246
368
  enrichmentArtifacts.push(relationshipsArtifact, relationshipProfileArtifact, relationshipDiagnosticsArtifact);
247
369
  const hasResolvedRelationships = input.enrichment.resolvedRelationships !== null;
248
370
  const relationshipArtifacts = buildKtxRelationshipArtifacts({
@@ -267,6 +389,7 @@ export async function writeLocalScanEnrichmentArtifacts(input) {
267
389
  artifacts: relationshipArtifacts,
268
390
  profile: relationshipProfile,
269
391
  warnings: input.enrichment.warnings,
392
+ partial: input.enrichment.relationshipPartial,
270
393
  thresholds: input.relationshipSettings
271
394
  ? {
272
395
  acceptThreshold: input.relationshipSettings.acceptThreshold,