@kaelio/ktx 0.13.0 → 0.14.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 (243) hide show
  1. package/assets/python/{kaelio_ktx-0.13.0-py3-none-any.whl → kaelio_ktx-0.14.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 +14 -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 +23 -5
  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/connections/read-only-sql.d.ts +5 -0
  60. package/dist/context/connections/read-only-sql.js +139 -1
  61. package/dist/context/ingest/adapters/gdrive/chunk.d.ts +3 -0
  62. package/dist/context/ingest/adapters/gdrive/chunk.js +74 -0
  63. package/dist/context/ingest/adapters/gdrive/detect.d.ts +1 -0
  64. package/dist/context/ingest/adapters/gdrive/detect.js +20 -0
  65. package/dist/context/ingest/adapters/gdrive/fetch.d.ts +6 -0
  66. package/dist/context/ingest/adapters/gdrive/fetch.js +95 -0
  67. package/dist/context/ingest/adapters/gdrive/gdrive-client.d.ts +30 -0
  68. package/dist/context/ingest/adapters/gdrive/gdrive-client.js +132 -0
  69. package/dist/context/ingest/adapters/gdrive/gdrive.adapter.d.ts +11 -0
  70. package/dist/context/ingest/adapters/gdrive/gdrive.adapter.js +27 -0
  71. package/dist/context/ingest/adapters/gdrive/normalize.d.ts +2 -0
  72. package/dist/context/ingest/adapters/gdrive/normalize.js +256 -0
  73. package/dist/context/ingest/adapters/gdrive/types.d.ts +153 -0
  74. package/dist/context/ingest/adapters/gdrive/types.js +36 -0
  75. package/dist/context/ingest/adapters/live-database/daemon-introspection.js +24 -0
  76. package/dist/context/ingest/adapters/live-database/fetch-report.d.ts +8 -0
  77. package/dist/context/ingest/adapters/live-database/fetch-report.js +37 -0
  78. package/dist/context/ingest/adapters/live-database/live-database.adapter.d.ts +2 -1
  79. package/dist/context/ingest/adapters/live-database/live-database.adapter.js +9 -2
  80. package/dist/context/ingest/adapters/live-database/manifest.d.ts +2 -0
  81. package/dist/context/ingest/adapters/live-database/manifest.js +8 -4
  82. package/dist/context/ingest/adapters/live-database/scan-outcome.d.ts +17 -0
  83. package/dist/context/ingest/adapters/live-database/scan-outcome.js +40 -0
  84. package/dist/context/ingest/adapters/live-database/stage.js +5 -5
  85. package/dist/context/ingest/adapters/metabase/types.d.ts +1 -1
  86. package/dist/context/ingest/adapters/metabase/types.js +1 -1
  87. package/dist/context/ingest/artifact-gates.d.ts +32 -1
  88. package/dist/context/ingest/artifact-gates.js +85 -18
  89. package/dist/context/ingest/final-gate-prune.d.ts +35 -0
  90. package/dist/context/ingest/final-gate-prune.js +229 -0
  91. package/dist/context/ingest/ingest-bundle.runner.d.ts +3 -0
  92. package/dist/context/ingest/ingest-bundle.runner.js +459 -240
  93. package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +0 -11
  94. package/dist/context/ingest/isolated-diff/patch-integrator.js +0 -44
  95. package/dist/context/ingest/isolated-diff/work-unit-executor.d.ts +1 -0
  96. package/dist/context/ingest/isolated-diff/work-unit-executor.js +13 -4
  97. package/dist/context/ingest/local-adapters.js +6 -0
  98. package/dist/context/ingest/local-bundle-runtime.js +7 -2
  99. package/dist/context/ingest/local-ingest.js +1 -1
  100. package/dist/context/ingest/local-stage-ingest.d.ts +3 -1
  101. package/dist/context/ingest/local-stage-ingest.js +3 -1
  102. package/dist/context/ingest/ports.d.ts +3 -0
  103. package/dist/context/ingest/report-snapshot.js +13 -3
  104. package/dist/context/ingest/reports.d.ts +3 -3
  105. package/dist/context/ingest/reports.js +3 -1
  106. package/dist/context/ingest/stages/build-wu-context.d.ts +1 -0
  107. package/dist/context/ingest/stages/build-wu-context.js +2 -1
  108. package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -1
  109. package/dist/context/ingest/stages/stage-3-work-units.js +4 -10
  110. package/dist/context/ingest/stages/validate-wu-sources.d.ts +12 -0
  111. package/dist/context/ingest/stages/validate-wu-sources.js +23 -8
  112. package/dist/context/ingest/tools/read-raw-file.tool.js +10 -5
  113. package/dist/context/ingest/tools/read-raw-span.tool.js +10 -5
  114. package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +1 -1
  115. package/dist/context/ingest/tools/warehouse-verification/entity-details.tool.js +1 -1
  116. package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +1 -1
  117. package/dist/context/ingest/types.d.ts +3 -0
  118. package/dist/context/ingest/wiki-body-refs.d.ts +28 -0
  119. package/dist/context/ingest/wiki-body-refs.js +37 -8
  120. package/dist/context/ingest/wiki-sl-ref-repair.d.ts +1 -0
  121. package/dist/context/ingest/wiki-sl-ref-repair.js +4 -0
  122. package/dist/context/ingest/work-unit-cache.d.ts +67 -0
  123. package/dist/context/ingest/work-unit-cache.js +230 -0
  124. package/dist/context/llm/ai-sdk-runtime.d.ts +1 -0
  125. package/dist/context/llm/ai-sdk-runtime.js +5 -0
  126. package/dist/context/llm/claude-code-runtime.d.ts +4 -1
  127. package/dist/context/llm/claude-code-runtime.js +38 -8
  128. package/dist/context/llm/codex-runtime.d.ts +4 -1
  129. package/dist/context/llm/codex-runtime.js +48 -35
  130. package/dist/context/llm/runtime-port.d.ts +26 -0
  131. package/dist/context/llm/subprocess-generate-object-child.d.ts +1 -0
  132. package/dist/context/llm/subprocess-generate-object-child.js +34 -0
  133. package/dist/context/llm/subprocess-generate-object.d.ts +42 -0
  134. package/dist/context/llm/subprocess-generate-object.js +122 -0
  135. package/dist/context/mcp/context-tools.d.ts +2 -0
  136. package/dist/context/mcp/context-tools.js +111 -17
  137. package/dist/context/mcp/local-project-ports.d.ts +7 -0
  138. package/dist/context/mcp/local-project-ports.js +29 -0
  139. package/dist/context/mcp/logger.d.ts +24 -0
  140. package/dist/context/mcp/logger.js +49 -0
  141. package/dist/context/mcp/server.js +2 -0
  142. package/dist/context/mcp/types.d.ts +17 -0
  143. package/dist/context/memory/memory-agent.service.js +1 -1
  144. package/dist/context/project/config.d.ts +42 -0
  145. package/dist/context/project/config.js +5 -0
  146. package/dist/context/project/driver-schemas.d.ts +20 -0
  147. package/dist/context/project/driver-schemas.js +50 -1
  148. package/dist/context/project/project.js +1 -1
  149. package/dist/context/project/setup-config.js +1 -0
  150. package/dist/context/scan/description-generation.d.ts +4 -0
  151. package/dist/context/scan/description-generation.js +100 -13
  152. package/dist/context/scan/enabled-tables.d.ts +5 -0
  153. package/dist/context/scan/enabled-tables.js +13 -1
  154. package/dist/context/scan/enrichment-state.d.ts +50 -7
  155. package/dist/context/scan/enrichment-state.js +30 -13
  156. package/dist/context/scan/local-enrichment-artifacts.d.ts +41 -0
  157. package/dist/context/scan/local-enrichment-artifacts.js +155 -32
  158. package/dist/context/scan/local-enrichment.d.ts +39 -4
  159. package/dist/context/scan/local-enrichment.js +345 -92
  160. package/dist/context/scan/local-scan.d.ts +4 -1
  161. package/dist/context/scan/local-scan.js +54 -13
  162. package/dist/context/scan/local-structural-artifacts.d.ts +3 -1
  163. package/dist/context/scan/local-structural-artifacts.js +5 -0
  164. package/dist/context/scan/object-introspection.d.ts +21 -0
  165. package/dist/context/scan/object-introspection.js +35 -0
  166. package/dist/context/scan/relationship-benchmarks.js +5 -3
  167. package/dist/context/scan/relationship-composite-candidates.d.ts +6 -3
  168. package/dist/context/scan/relationship-composite-candidates.js +13 -1
  169. package/dist/context/scan/relationship-detection-budget.d.ts +35 -0
  170. package/dist/context/scan/relationship-detection-budget.js +53 -0
  171. package/dist/context/scan/relationship-diagnostics.d.ts +5 -0
  172. package/dist/context/scan/relationship-diagnostics.js +2 -0
  173. package/dist/context/scan/relationship-discovery.d.ts +9 -3
  174. package/dist/context/scan/relationship-discovery.js +27 -4
  175. package/dist/context/scan/relationship-llm-proposal.js +36 -27
  176. package/dist/context/scan/relationship-profiling.d.ts +7 -3
  177. package/dist/context/scan/relationship-profiling.js +53 -41
  178. package/dist/context/scan/relationship-validation.d.ts +6 -4
  179. package/dist/context/scan/relationship-validation.js +46 -32
  180. package/dist/context/scan/sqlite-local-enrichment-state-store.d.ts +8 -1
  181. package/dist/context/scan/sqlite-local-enrichment-state-store.js +71 -159
  182. package/dist/context/scan/types.d.ts +5 -3
  183. package/dist/context/scan/types.js +2 -0
  184. package/dist/context/sl/local-query.js +5 -0
  185. package/dist/context/sl/pglite-sl-search-prototype.js +1 -1
  186. package/dist/context/sl/semantic-layer.service.js +1 -1
  187. package/dist/context/sl/source-files.d.ts +5 -0
  188. package/dist/context/sl/source-files.js +17 -11
  189. package/dist/context/sl/tools/connection-id-schema.js +1 -1
  190. package/dist/context/sql-analysis/dialect-notes.d.ts +9 -0
  191. package/dist/context/sql-analysis/dialect-notes.js +40 -0
  192. package/dist/context/sql-analysis/dialects/bigquery.md +13 -0
  193. package/dist/context/sql-analysis/dialects/clickhouse.md +9 -0
  194. package/dist/context/sql-analysis/dialects/mysql.md +9 -0
  195. package/dist/context/sql-analysis/dialects/postgres.md +10 -0
  196. package/dist/context/sql-analysis/dialects/snowflake.md +10 -0
  197. package/dist/context/sql-analysis/dialects/sqlite.md +11 -0
  198. package/dist/context/sql-analysis/dialects/tsql.md +10 -0
  199. package/dist/context/wiki/keys.js +1 -1
  200. package/dist/context/wiki/knowledge-wiki.service.d.ts +4 -4
  201. package/dist/context/wiki/knowledge-wiki.service.js +2 -1
  202. package/dist/context/wiki/local-knowledge.d.ts +13 -0
  203. package/dist/context/wiki/local-knowledge.js +50 -6
  204. package/dist/context/wiki/sqlite-knowledge-index.d.ts +2 -0
  205. package/dist/context/wiki/sqlite-knowledge-index.js +21 -5
  206. package/dist/context/wiki/tools/wiki-write.tool.d.ts +2 -0
  207. package/dist/context/wiki/tools/wiki-write.tool.js +27 -0
  208. package/dist/context/wiki/types.d.ts +6 -0
  209. package/dist/context-build-view.d.ts +3 -0
  210. package/dist/context-build-view.js +1 -0
  211. package/dist/knowledge.d.ts +2 -0
  212. package/dist/knowledge.js +12 -1
  213. package/dist/local-adapters.js +11 -0
  214. package/dist/mcp-http-server.js +15 -1
  215. package/dist/mcp-server-factory.d.ts +2 -0
  216. package/dist/mcp-server-factory.js +15 -1
  217. package/dist/mcp-stdio-server.js +10 -2
  218. package/dist/notion-page-picker.js +1 -1
  219. package/dist/prompts/memory_agent_external_ingest.md +2 -0
  220. package/dist/public-ingest.d.ts +3 -1
  221. package/dist/public-ingest.js +3 -0
  222. package/dist/scan.d.ts +3 -1
  223. package/dist/scan.js +7 -0
  224. package/dist/setup-databases.d.ts +1 -1
  225. package/dist/setup-databases.js +43 -1
  226. package/dist/setup-sources.d.ts +5 -1
  227. package/dist/setup-sources.js +124 -48
  228. package/dist/setup.d.ts +3 -0
  229. package/dist/setup.js +6 -1
  230. package/dist/skills/analytics/SKILL.md +200 -2
  231. package/dist/skills/gdrive_synthesize/SKILL.md +97 -0
  232. package/dist/skills/wiki_capture/SKILL.md +24 -0
  233. package/dist/sql.js +4 -0
  234. package/dist/status-project.d.ts +4 -0
  235. package/dist/status-project.js +54 -2
  236. package/dist/telemetry/events.d.ts +2 -2
  237. package/dist/text-ingest.d.ts +4 -0
  238. package/dist/text-ingest.js +58 -29
  239. package/dist/verbatim-ingest.d.ts +46 -0
  240. package/dist/verbatim-ingest.js +193 -0
  241. package/package.json +5 -1
  242. package/dist/context/ingest/final-gate-repair.d.ts +0 -28
  243. package/dist/context/ingest/final-gate-repair.js +0 -91
@@ -45,6 +45,9 @@ function profileForColumn(profile, table, column) {
45
45
  function rowCountForTable(profile, table) {
46
46
  return profile.tables.find((item) => item.table.name.toLowerCase() === table.ref.name.toLowerCase())?.rowCount ?? null;
47
47
  }
48
+ function resolvedDescription(descriptions) {
49
+ return descriptions.ai ?? descriptions.db ?? null;
50
+ }
48
51
  function buildEvidencePacket(schema, profile, settings) {
49
52
  return {
50
53
  connectionId: schema.connectionId,
@@ -52,33 +55,39 @@ function buildEvidencePacket(schema, profile, settings) {
52
55
  tables: schema.tables
53
56
  .filter((table) => table.enabled)
54
57
  .slice(0, settings.maxTablesPerBatch)
55
- .map((table) => ({
56
- name: table.ref.name,
57
- catalog: table.ref.catalog,
58
- db: table.ref.db,
59
- rowCount: rowCountForTable(profile, table),
60
- columns: table.columns.slice(0, settings.maxColumnsPerTable).map((column) => {
61
- const columnProfile = profileForColumn(profile, table, column);
62
- return {
63
- name: column.name,
64
- nativeType: column.nativeType,
65
- normalizedType: column.normalizedType,
66
- dimensionType: column.dimensionType,
67
- nullable: column.nullable,
68
- declaredPrimaryKey: column.primaryKey,
69
- profile: columnProfile
70
- ? {
71
- rowCount: columnProfile.rowCount,
72
- nullCount: columnProfile.nullCount,
73
- distinctCount: columnProfile.distinctCount,
74
- uniquenessRatio: columnProfile.uniquenessRatio,
75
- nullRate: columnProfile.nullRate,
76
- sampleValues: columnProfile.sampleValues.slice(0, settings.maxSampleValuesPerColumn),
77
- }
78
- : null,
79
- };
80
- }),
81
- })),
58
+ .map((table) => {
59
+ const tableDescription = resolvedDescription(table.descriptions);
60
+ return {
61
+ name: table.ref.name,
62
+ catalog: table.ref.catalog,
63
+ db: table.ref.db,
64
+ rowCount: rowCountForTable(profile, table),
65
+ ...(tableDescription ? { description: tableDescription } : {}),
66
+ columns: table.columns.slice(0, settings.maxColumnsPerTable).map((column) => {
67
+ const columnProfile = profileForColumn(profile, table, column);
68
+ const columnDescription = resolvedDescription(column.descriptions);
69
+ return {
70
+ name: column.name,
71
+ nativeType: column.nativeType,
72
+ normalizedType: column.normalizedType,
73
+ dimensionType: column.dimensionType,
74
+ nullable: column.nullable,
75
+ declaredPrimaryKey: column.primaryKey,
76
+ ...(columnDescription ? { description: columnDescription } : {}),
77
+ profile: columnProfile
78
+ ? {
79
+ rowCount: columnProfile.rowCount,
80
+ nullCount: columnProfile.nullCount,
81
+ distinctCount: columnProfile.distinctCount,
82
+ uniquenessRatio: columnProfile.uniquenessRatio,
83
+ nullRate: columnProfile.nullRate,
84
+ sampleValues: columnProfile.sampleValues.slice(0, settings.maxSampleValuesPerColumn),
85
+ }
86
+ : null,
87
+ };
88
+ }),
89
+ };
90
+ }),
82
91
  };
83
92
  }
84
93
  function pkProposalKey(table, column) {
@@ -1,6 +1,7 @@
1
- import type { KtxDialect } from '../connections/dialects.js';
1
+ import type { KtxSqlDialect } from '../connections/dialects.js';
2
2
  import type { KtxEnrichedSchema } from './enrichment-types.js';
3
- import type { KtxConnectionDriver, KtxQueryResult, KtxReadOnlyQueryInput, KtxScanContext, KtxTableRef } from './types.js';
3
+ import { type KtxRelationshipDetectionBudget } from './relationship-detection-budget.js';
4
+ import type { KtxConnectionDriver, KtxProgressPort, KtxQueryResult, KtxReadOnlyQueryInput, KtxScanContext, KtxTableRef } from './types.js';
4
5
  export interface KtxRelationshipReadOnlyExecutor {
5
6
  executeReadOnly(input: KtxReadOnlyQueryInput, ctx: KtxScanContext): Promise<KtxQueryResult>;
6
7
  }
@@ -42,7 +43,8 @@ export interface KtxRelationshipProfileCache {
42
43
  }
43
44
  export interface ProfileKtxRelationshipSchemaInput {
44
45
  connectionId: string;
45
- dialect: KtxDialect;
46
+ driver: KtxConnectionDriver;
47
+ dialect: KtxSqlDialect | null;
46
48
  schema: KtxEnrichedSchema;
47
49
  executor: KtxRelationshipReadOnlyExecutor | null;
48
50
  ctx: KtxScanContext;
@@ -50,6 +52,8 @@ export interface ProfileKtxRelationshipSchemaInput {
50
52
  profileSampleRows?: number;
51
53
  profileConcurrency?: number;
52
54
  cache?: KtxRelationshipProfileCache;
55
+ budget?: KtxRelationshipDetectionBudget;
56
+ progress?: KtxProgressPort;
53
57
  }
54
58
  export declare function createKtxRelationshipProfileCache(): KtxRelationshipProfileCache;
55
59
  export declare function profileKtxRelationshipSchema(input: ProfileKtxRelationshipSchemaInput): Promise<KtxRelationshipProfileArtifact>;
@@ -1,4 +1,4 @@
1
- import { mapWithConcurrency } from './relationship-validation.js';
1
+ import { mapWithBudget } from './relationship-detection-budget.js';
2
2
  export function createKtxRelationshipProfileCache() {
3
3
  return { tableProfiles: new Map() };
4
4
  }
@@ -180,10 +180,10 @@ async function queryTableProfile(input) {
180
180
  };
181
181
  }
182
182
  export async function profileKtxRelationshipSchema(input) {
183
- if (!input.executor) {
183
+ if (!input.executor || !input.dialect) {
184
184
  return {
185
185
  connectionId: input.connectionId,
186
- driver: input.dialect.type,
186
+ driver: input.driver,
187
187
  sqlAvailable: false,
188
188
  queryCount: 0,
189
189
  tables: [],
@@ -196,51 +196,63 @@ export async function profileKtxRelationshipSchema(input) {
196
196
  const columns = {};
197
197
  const warnings = [];
198
198
  const executor = input.executor;
199
+ const dialect = input.dialect;
199
200
  const enabledTables = input.schema.tables.filter((candidate) => candidate.enabled);
200
- const tableResults = await mapWithConcurrency(enabledTables, input.profileConcurrency ?? 4, async (table) => {
201
- const sampleValuesPerColumn = input.sampleValuesPerColumn ?? 5;
202
- const profileSampleRows = input.profileSampleRows ?? 10000;
203
- const cacheKey = tableProfileCacheKey({
204
- connectionId: input.connectionId,
205
- dialect: input.dialect,
206
- ctx: input.ctx,
207
- table: table.ref,
208
- sampleValuesPerColumn,
209
- profileSampleRows,
210
- });
211
- const cached = input.cache?.tableProfiles.get(cacheKey);
212
- if (cached) {
213
- return { cached, queryCount: 0 };
214
- }
215
- try {
216
- const tableProfile = await queryTableProfile({
201
+ const { results: tableResults } = await mapWithBudget({
202
+ inputs: enabledTables,
203
+ concurrency: input.profileConcurrency ?? 4,
204
+ budget: input.budget,
205
+ onStart: async (index, total) => {
206
+ await input.progress?.update((index + 1) / total, `Profiling table ${index + 1}/${total}`, { transient: true });
207
+ },
208
+ mapOne: async (table) => {
209
+ const sampleValuesPerColumn = input.sampleValuesPerColumn ?? 5;
210
+ const profileSampleRows = input.profileSampleRows ?? 10000;
211
+ const cacheKey = tableProfileCacheKey({
217
212
  connectionId: input.connectionId,
218
- dialect: input.dialect,
219
- table,
220
- executor,
213
+ dialect,
221
214
  ctx: input.ctx,
215
+ table: table.ref,
222
216
  sampleValuesPerColumn,
223
217
  profileSampleRows,
224
218
  });
225
- input.cache?.tableProfiles.set(cacheKey, {
226
- table: tableProfile.table,
227
- columns: tableProfile.columns,
228
- warnings: [],
229
- });
230
- return { tableProfile };
231
- }
232
- catch (error) {
233
- const failureWarning = `profile_failed:${table.ref.name}:${error instanceof Error ? error.message : String(error)}`;
234
- const cachedFailure = {
235
- table: { table: table.ref, rowCount: 0 },
236
- columns: {},
237
- warnings: [failureWarning],
238
- };
239
- input.cache?.tableProfiles.set(cacheKey, cachedFailure);
240
- return { cached: cachedFailure, queryCount: 0 };
241
- }
219
+ const cached = input.cache?.tableProfiles.get(cacheKey);
220
+ if (cached) {
221
+ return { cached, queryCount: 0 };
222
+ }
223
+ try {
224
+ const tableProfile = await queryTableProfile({
225
+ connectionId: input.connectionId,
226
+ dialect,
227
+ table,
228
+ executor,
229
+ ctx: input.ctx,
230
+ sampleValuesPerColumn,
231
+ profileSampleRows,
232
+ });
233
+ input.cache?.tableProfiles.set(cacheKey, {
234
+ table: tableProfile.table,
235
+ columns: tableProfile.columns,
236
+ warnings: [],
237
+ });
238
+ return { tableProfile };
239
+ }
240
+ catch (error) {
241
+ const failureWarning = `profile_failed:${table.ref.name}:${error instanceof Error ? error.message : String(error)}`;
242
+ const cachedFailure = {
243
+ table: { table: table.ref, rowCount: 0 },
244
+ columns: {},
245
+ warnings: [failureWarning],
246
+ };
247
+ input.cache?.tableProfiles.set(cacheKey, cachedFailure);
248
+ return { cached: cachedFailure, queryCount: 0 };
249
+ }
250
+ },
242
251
  });
243
252
  for (const result of tableResults) {
253
+ if (!result) {
254
+ continue;
255
+ }
244
256
  if ('tableProfile' in result) {
245
257
  queryTotal += result.tableProfile.queryCount;
246
258
  tables.push(result.tableProfile.table);
@@ -255,7 +267,7 @@ export async function profileKtxRelationshipSchema(input) {
255
267
  }
256
268
  return {
257
269
  connectionId: input.connectionId,
258
- driver: input.dialect.type,
270
+ driver: input.driver,
259
271
  sqlAvailable: true,
260
272
  queryCount: queryTotal,
261
273
  tables,
@@ -1,8 +1,9 @@
1
- import type { KtxDialect } from '../connections/dialects.js';
1
+ import type { KtxSqlDialect } from '../connections/dialects.js';
2
2
  import { type KtxRelationshipValidationBudget } from './relationship-budget.js';
3
3
  import type { KtxRelationshipDiscoveryCandidate } from './relationship-candidates.js';
4
+ import { type KtxRelationshipDetectionBudget } from './relationship-detection-budget.js';
4
5
  import { type KtxRelationshipProfileArtifact, type KtxRelationshipReadOnlyExecutor } from './relationship-profiling.js';
5
- import type { KtxScanContext } from './types.js';
6
+ import type { KtxProgressPort, KtxScanContext } from './types.js';
6
7
  type KtxValidatedRelationshipStatus = 'accepted' | 'review' | 'rejected';
7
8
  interface KtxRelationshipValidationSettings {
8
9
  acceptThreshold: number;
@@ -34,14 +35,15 @@ export interface KtxValidatedRelationshipDiscoveryCandidate extends Omit<KtxRela
34
35
  }
35
36
  export interface ValidateKtxRelationshipDiscoveryCandidatesInput {
36
37
  connectionId: string;
37
- dialect: KtxDialect;
38
+ dialect: KtxSqlDialect | null;
38
39
  candidates: readonly KtxRelationshipDiscoveryCandidate[];
39
40
  profiles: KtxRelationshipProfileArtifact;
40
41
  executor: KtxRelationshipReadOnlyExecutor | null;
41
42
  ctx: KtxScanContext;
42
43
  tableCount?: number;
43
44
  settings?: Partial<KtxRelationshipValidationSettings>;
45
+ budget?: KtxRelationshipDetectionBudget;
46
+ progress?: KtxProgressPort;
44
47
  }
45
- export declare function mapWithConcurrency<TInput, TOutput>(inputs: readonly TInput[], concurrency: number, mapOne: (input: TInput) => Promise<TOutput>): Promise<TOutput[]>;
46
48
  export declare function validateKtxRelationshipDiscoveryCandidates(input: ValidateKtxRelationshipDiscoveryCandidatesInput): Promise<KtxValidatedRelationshipDiscoveryCandidate[]>;
47
49
  export {};
@@ -1,4 +1,6 @@
1
+ import { KtxQueryError } from '../../errors.js';
1
2
  import { applyKtxRelationshipValidationBudget } from './relationship-budget.js';
3
+ import { mapWithBudget } from './relationship-detection-budget.js';
2
4
  const DEFAULT_SETTINGS = {
3
5
  acceptThreshold: 0.85,
4
6
  reviewThreshold: 0.55,
@@ -89,20 +91,6 @@ function statusFor(input) {
89
91
  }
90
92
  return 'rejected';
91
93
  }
92
- export async function mapWithConcurrency(inputs, concurrency, mapOne) {
93
- const safeConcurrency = Math.max(1, Math.floor(concurrency));
94
- const outputs = new Array(inputs.length);
95
- let nextIndex = 0;
96
- async function worker() {
97
- while (nextIndex < inputs.length) {
98
- const index = nextIndex;
99
- nextIndex += 1;
100
- outputs[index] = await mapOne(inputs[index]);
101
- }
102
- }
103
- await Promise.all(Array.from({ length: Math.min(safeConcurrency, inputs.length) }, () => worker()));
104
- return outputs;
105
- }
106
94
  function reviewWithoutValidation(candidate, profiles, reason) {
107
95
  const sourceColumn = singleRelationshipColumn(candidate.from);
108
96
  const targetColumn = singleRelationshipColumn(candidate.to);
@@ -129,10 +117,11 @@ function reviewWithoutValidation(candidate, profiles, reason) {
129
117
  }
130
118
  export async function validateKtxRelationshipDiscoveryCandidates(input) {
131
119
  const settings = mergeSettings(input.settings);
132
- if (!input.executor || !input.profiles.sqlAvailable) {
120
+ if (!input.executor || !input.profiles.sqlAvailable || !input.dialect) {
133
121
  return input.candidates.map((candidate) => reviewWithoutValidation(candidate, input.profiles, 'validation_unavailable'));
134
122
  }
135
123
  const executor = input.executor;
124
+ const dialect = input.dialect;
136
125
  async function validateCandidate(candidate) {
137
126
  const sourceColumn = singleRelationshipColumn(candidate.from);
138
127
  const targetColumn = singleRelationshipColumn(candidate.to);
@@ -141,18 +130,31 @@ export async function validateKtxRelationshipDiscoveryCandidates(input) {
141
130
  if (!sourceProfile || !targetProfile) {
142
131
  return reviewWithoutValidation(candidate, input.profiles, 'profile_unavailable');
143
132
  }
144
- const result = await executor.executeReadOnly({
145
- connectionId: input.connectionId,
146
- sql: buildCoverageSql({
147
- dialect: input.dialect,
148
- childTable: candidate.from.table,
149
- childColumn: sourceColumn,
150
- parentTable: candidate.to.table,
151
- parentColumn: targetColumn,
152
- maxDistinctSourceValues: settings.maxDistinctSourceValues,
153
- }),
154
- maxRows: 1,
155
- }, input.ctx);
133
+ let result;
134
+ try {
135
+ result = await executor.executeReadOnly({
136
+ connectionId: input.connectionId,
137
+ sql: buildCoverageSql({
138
+ dialect,
139
+ childTable: candidate.from.table,
140
+ childColumn: sourceColumn,
141
+ parentTable: candidate.to.table,
142
+ parentColumn: targetColumn,
143
+ maxDistinctSourceValues: settings.maxDistinctSourceValues,
144
+ }),
145
+ maxRows: 1,
146
+ }, input.ctx);
147
+ }
148
+ catch (error) {
149
+ // A bounded-query timeout (or other query rejection) on this one coverage
150
+ // probe is best-effort: skip the candidate to review rather than aborting
151
+ // the whole validation pass.
152
+ if (error instanceof KtxQueryError) {
153
+ input.ctx.logger?.warn(`relationship validation query skipped for ${candidate.from.table.name}.${sourceColumn} -> ${candidate.to.table.name}.${targetColumn}: ${error.message}`);
154
+ return reviewWithoutValidation(candidate, input.profiles, 'validation_query_failed');
155
+ }
156
+ throw error;
157
+ }
156
158
  const childDistinct = numberAt(result, 'child_distinct');
157
159
  const parentDistinct = numberAt(result, 'parent_distinct');
158
160
  const overlap = numberAt(result, 'overlap');
@@ -208,14 +210,26 @@ export async function validateKtxRelationshipDiscoveryCandidates(input) {
208
210
  budget: settings.validationBudget,
209
211
  score: (candidate) => candidate.confidence,
210
212
  });
211
- const validated = await mapWithConcurrency(budgeted.toValidate.map((entry) => entry.candidate), settings.concurrency, validateCandidate);
213
+ const { results: validated } = await mapWithBudget({
214
+ inputs: budgeted.toValidate,
215
+ concurrency: settings.concurrency,
216
+ budget: input.budget,
217
+ onStart: async (index, total) => {
218
+ await input.progress?.update((index + 1) / total, `Validating candidate ${index + 1}/${total}`, {
219
+ transient: true,
220
+ });
221
+ },
222
+ mapOne: (entry) => validateCandidate(entry.candidate),
223
+ });
212
224
  const byOriginalIndex = new Map();
213
225
  for (let index = 0; index < budgeted.toValidate.length; index += 1) {
214
- const originalIndex = budgeted.toValidate[index]?.originalIndex;
215
- const candidate = validated[index];
216
- if (originalIndex !== undefined && candidate) {
217
- byOriginalIndex.set(originalIndex, candidate);
226
+ const entry = budgeted.toValidate[index];
227
+ if (!entry) {
228
+ continue;
218
229
  }
230
+ // A candidate left unvalidated by the wall-clock budget degrades to the
231
+ // same review status as one deferred by the validation count budget.
232
+ byOriginalIndex.set(entry.originalIndex, validated[index] ?? reviewWithoutValidation(entry.candidate, input.profiles, 'validation_unattempted'));
219
233
  }
220
234
  for (const entry of budgeted.deferred) {
221
235
  byOriginalIndex.set(entry.originalIndex, reviewWithoutValidation(entry.candidate, input.profiles, 'validation_unattempted'));
@@ -1,11 +1,18 @@
1
+ import type { ContentResultCache } from '../cache/content-result-cache.js';
1
2
  import type { KtxScanEnrichmentCompletedStage, KtxScanEnrichmentFailedStage, KtxScanEnrichmentStageLookup, KtxScanEnrichmentStageRecord, KtxScanEnrichmentStateStore } from './enrichment-state.js';
3
+ import type { KtxScanEnrichmentStage } from './types.js';
2
4
  export interface SqliteLocalScanEnrichmentStateStoreOptions {
3
5
  dbPath: string;
6
+ cache?: ContentResultCache;
4
7
  }
5
8
  export declare class SqliteLocalScanEnrichmentStateStore implements KtxScanEnrichmentStateStore {
6
- private readonly db;
9
+ private readonly cache;
7
10
  constructor(options: SqliteLocalScanEnrichmentStateStoreOptions);
8
11
  findCompletedStage<TOutput = unknown>(input: KtxScanEnrichmentStageLookup): Promise<KtxScanEnrichmentCompletedStage<TOutput> | null>;
12
+ findLatestCompletedStage(input: {
13
+ connectionId: string;
14
+ stage: KtxScanEnrichmentStage;
15
+ }): Promise<KtxScanEnrichmentCompletedStage | null>;
9
16
  saveCompletedStage<TOutput = unknown>(input: Omit<KtxScanEnrichmentCompletedStage<TOutput>, 'status' | 'errorMessage'>): Promise<void>;
10
17
  saveFailedStage(input: Omit<KtxScanEnrichmentFailedStage, 'status' | 'output'>): Promise<void>;
11
18
  listRunStages(runId: string): Promise<KtxScanEnrichmentStageRecord[]>;
@@ -1,190 +1,102 @@
1
- import { mkdirSync } from 'node:fs';
2
- import { dirname } from 'node:path';
3
- import Database from 'better-sqlite3';
4
- function parseStageRow(row) {
5
- if (row.status === 'completed') {
1
+ import { SqliteContentResultCache } from '../cache/sqlite-content-result-cache.js';
2
+ import { KTX_SCAN_ENRICHMENT_STAGES } from './enrichment-state.js';
3
+ import { KTX_SCAN_MODES } from './types.js';
4
+ function namespace(stage) {
5
+ return `scan:${stage}`;
6
+ }
7
+ function metadataFor(input) {
8
+ return {
9
+ connectionId: input.connectionId,
10
+ syncId: input.syncId,
11
+ mode: input.mode,
12
+ stage: input.stage,
13
+ };
14
+ }
15
+ function isScanMode(value) {
16
+ return typeof value === 'string' && KTX_SCAN_MODES.includes(value);
17
+ }
18
+ function isScanEnrichmentStage(value) {
19
+ return typeof value === 'string' && KTX_SCAN_ENRICHMENT_STAGES.includes(value);
20
+ }
21
+ function parseMetadata(record) {
22
+ const { connectionId, syncId, mode, stage } = record.metadata;
23
+ if (typeof connectionId !== 'string' || typeof syncId !== 'string' || !isScanMode(mode) || !isScanEnrichmentStage(stage)) {
24
+ throw new Error(`Invalid scan enrichment cache metadata for ${record.namespace}/${record.scopeKey}`);
25
+ }
26
+ return { connectionId, syncId, mode, stage };
27
+ }
28
+ function toScanRecord(record) {
29
+ const metadata = parseMetadata(record);
30
+ const base = {
31
+ runId: record.runId,
32
+ connectionId: metadata.connectionId,
33
+ syncId: metadata.syncId,
34
+ mode: metadata.mode,
35
+ stage: metadata.stage,
36
+ inputHash: record.inputHash,
37
+ updatedAt: record.updatedAt,
38
+ };
39
+ if (record.status === 'completed') {
6
40
  return {
7
- runId: row.run_id,
8
- connectionId: row.connection_id,
9
- syncId: row.sync_id,
10
- mode: row.mode,
11
- stage: row.stage,
12
- inputHash: row.input_hash,
41
+ ...base,
13
42
  status: 'completed',
14
- output: JSON.parse(row.output_json ?? 'null'),
43
+ output: record.output,
15
44
  errorMessage: null,
16
- updatedAt: row.updated_at,
17
45
  };
18
46
  }
19
47
  return {
20
- runId: row.run_id,
21
- connectionId: row.connection_id,
22
- syncId: row.sync_id,
23
- mode: row.mode,
24
- stage: row.stage,
25
- inputHash: row.input_hash,
48
+ ...base,
26
49
  status: 'failed',
27
50
  output: null,
28
- errorMessage: row.error_message ?? 'Unknown enrichment stage failure',
29
- updatedAt: row.updated_at,
51
+ errorMessage: record.errorMessage,
30
52
  };
31
53
  }
32
- function isSafeRunId(runId) {
33
- return /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(runId);
34
- }
35
54
  export class SqliteLocalScanEnrichmentStateStore {
36
- db;
55
+ cache;
37
56
  constructor(options) {
38
- mkdirSync(dirname(options.dbPath), { recursive: true });
39
- this.db = new Database(options.dbPath);
40
- this.db.pragma('journal_mode = WAL');
41
- this.db.exec(`
42
- CREATE TABLE IF NOT EXISTS local_scan_enrichment_stages (
43
- run_id TEXT NOT NULL,
44
- stage TEXT NOT NULL,
45
- input_hash TEXT NOT NULL,
46
- connection_id TEXT NOT NULL,
47
- sync_id TEXT NOT NULL,
48
- mode TEXT NOT NULL,
49
- status TEXT NOT NULL,
50
- output_json TEXT,
51
- error_message TEXT,
52
- updated_at TEXT NOT NULL,
53
- PRIMARY KEY (run_id, stage)
54
- );
55
-
56
- CREATE INDEX IF NOT EXISTS local_scan_enrichment_stages_run_idx
57
- ON local_scan_enrichment_stages (run_id, updated_at, stage);
58
- `);
57
+ this.cache = options.cache ?? new SqliteContentResultCache({ dbPath: options.dbPath });
59
58
  }
60
59
  async findCompletedStage(input) {
61
- if (!isSafeRunId(input.runId)) {
62
- return null;
63
- }
64
- const row = this.db
65
- .prepare(`
66
- SELECT *
67
- FROM local_scan_enrichment_stages
68
- WHERE run_id = ?
69
- AND stage = ?
70
- AND input_hash = ?
71
- AND status = 'completed'
72
- `)
73
- .get(input.runId, input.stage, input.inputHash);
74
- if (!row) {
75
- return null;
76
- }
77
- const parsed = parseStageRow(row);
78
- return parsed.status === 'completed' ? parsed : null;
60
+ const record = await this.cache.findCompletedResult({
61
+ namespace: namespace(input.stage),
62
+ scopeKey: input.connectionId,
63
+ inputHash: input.inputHash,
64
+ });
65
+ return record ? toScanRecord(record) : null;
66
+ }
67
+ async findLatestCompletedStage(input) {
68
+ const record = await this.cache.findLatestCompletedResult({
69
+ namespace: namespace(input.stage),
70
+ scopeKey: input.connectionId,
71
+ });
72
+ return record ? toScanRecord(record) : null;
79
73
  }
80
74
  async saveCompletedStage(input) {
81
- this.db
82
- .prepare(`
83
- INSERT INTO local_scan_enrichment_stages (
84
- run_id,
85
- stage,
86
- input_hash,
87
- connection_id,
88
- sync_id,
89
- mode,
90
- status,
91
- output_json,
92
- error_message,
93
- updated_at
94
- )
95
- VALUES (
96
- @runId,
97
- @stage,
98
- @inputHash,
99
- @connectionId,
100
- @syncId,
101
- @mode,
102
- 'completed',
103
- @outputJson,
104
- NULL,
105
- @updatedAt
106
- )
107
- ON CONFLICT(run_id, stage) DO UPDATE SET
108
- input_hash = excluded.input_hash,
109
- connection_id = excluded.connection_id,
110
- sync_id = excluded.sync_id,
111
- mode = excluded.mode,
112
- status = excluded.status,
113
- output_json = excluded.output_json,
114
- error_message = excluded.error_message,
115
- updated_at = excluded.updated_at
116
- `)
117
- .run({
75
+ await this.cache.saveCompletedResult({
118
76
  runId: input.runId,
119
- stage: input.stage,
77
+ namespace: namespace(input.stage),
78
+ scopeKey: input.connectionId,
120
79
  inputHash: input.inputHash,
121
- connectionId: input.connectionId,
122
- syncId: input.syncId,
123
- mode: input.mode,
124
- outputJson: JSON.stringify(input.output),
80
+ output: input.output,
81
+ metadata: metadataFor(input),
125
82
  updatedAt: input.updatedAt,
126
83
  });
127
84
  }
128
85
  async saveFailedStage(input) {
129
- this.db
130
- .prepare(`
131
- INSERT INTO local_scan_enrichment_stages (
132
- run_id,
133
- stage,
134
- input_hash,
135
- connection_id,
136
- sync_id,
137
- mode,
138
- status,
139
- output_json,
140
- error_message,
141
- updated_at
142
- )
143
- VALUES (
144
- @runId,
145
- @stage,
146
- @inputHash,
147
- @connectionId,
148
- @syncId,
149
- @mode,
150
- 'failed',
151
- NULL,
152
- @errorMessage,
153
- @updatedAt
154
- )
155
- ON CONFLICT(run_id, stage) DO UPDATE SET
156
- input_hash = excluded.input_hash,
157
- connection_id = excluded.connection_id,
158
- sync_id = excluded.sync_id,
159
- mode = excluded.mode,
160
- status = excluded.status,
161
- output_json = excluded.output_json,
162
- error_message = excluded.error_message,
163
- updated_at = excluded.updated_at
164
- `)
165
- .run({
86
+ await this.cache.saveFailedResult({
166
87
  runId: input.runId,
167
- stage: input.stage,
88
+ namespace: namespace(input.stage),
89
+ scopeKey: input.connectionId,
168
90
  inputHash: input.inputHash,
169
- connectionId: input.connectionId,
170
- syncId: input.syncId,
171
- mode: input.mode,
172
91
  errorMessage: input.errorMessage,
92
+ metadata: metadataFor(input),
173
93
  updatedAt: input.updatedAt,
174
94
  });
175
95
  }
176
96
  async listRunStages(runId) {
177
- if (!isSafeRunId(runId)) {
178
- return [];
179
- }
180
- const rows = this.db
181
- .prepare(`
182
- SELECT *
183
- FROM local_scan_enrichment_stages
184
- WHERE run_id = ?
185
- ORDER BY updated_at ASC, stage ASC
186
- `)
187
- .all(runId);
188
- return rows.map((row) => parseStageRow(row));
97
+ const records = await this.cache.listRunResults(runId);
98
+ return records
99
+ .filter((record) => record.namespace.startsWith('scan:'))
100
+ .map((record) => toScanRecord(record));
189
101
  }
190
102
  }