@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
@@ -1,6 +1,8 @@
1
1
  import type { KtxTableRefKey } from './table-ref.js';
2
- export type KtxConnectionDriver = 'sqlite' | 'postgres' | 'sqlserver' | 'bigquery' | 'snowflake' | 'mysql' | 'clickhouse';
3
- export type KtxScanMode = 'structural' | 'relationships' | 'enriched';
2
+ export type KtxConnectionDriver = 'sqlite' | 'postgres' | 'sqlserver' | 'bigquery' | 'snowflake' | 'mysql' | 'clickhouse' | 'mongodb';
3
+ /** Canonical scan-mode registry. Runtime validation derives its allowlist here. */
4
+ export declare const KTX_SCAN_MODES: readonly ["structural", "relationships", "enriched"];
5
+ export type KtxScanMode = (typeof KTX_SCAN_MODES)[number];
4
6
  export type KtxScanTrigger = 'cli' | 'mcp' | 'schema_scan' | 'scheduled' | 'manual';
5
7
  export interface KtxConnectorCapabilities {
6
8
  structuralIntrospection: true;
@@ -293,7 +295,7 @@ interface KtxScanArtifactPaths {
293
295
  manifestShards: string[];
294
296
  enrichmentArtifacts: string[];
295
297
  }
296
- type KtxScanWarningCode = 'connector_capability_missing' | 'sampling_failed' | 'statistics_failed' | 'llm_unavailable' | 'embedding_unavailable' | 'scan_enrichment_backend_not_configured' | 'relationship_validation_failed' | 'relationship_llm_invalid_reference' | 'relationship_llm_proposal_failed' | 'credential_redacted' | 'enrichment_failed' | 'description_fallback_used' | 'constraint_discovery_unauthorized';
298
+ type KtxScanWarningCode = 'connector_capability_missing' | 'sampling_failed' | 'statistics_failed' | 'llm_unavailable' | 'embedding_unavailable' | 'scan_enrichment_backend_not_configured' | 'relationship_validation_failed' | 'relationship_detection_partial' | 'enrichment_stage_skipped' | 'enrichment_stage_stale' | 'relationship_llm_invalid_reference' | 'relationship_llm_proposal_failed' | 'credential_redacted' | 'enrichment_failed' | 'enrichment_timeout' | 'description_fallback_used' | 'constraint_discovery_unauthorized' | 'object_introspection_failed';
297
299
  export interface KtxScanWarning {
298
300
  code: KtxScanWarningCode;
299
301
  message: string;
@@ -1,3 +1,5 @@
1
+ /** Canonical scan-mode registry. Runtime validation derives its allowlist here. */
2
+ export const KTX_SCAN_MODES = ['structural', 'relationships', 'enriched'];
1
3
  export function createKtxConnectorCapabilities(capabilities = {}) {
2
4
  return {
3
5
  structuralIntrospection: true,
@@ -1,3 +1,4 @@
1
+ import { isSqlQueryableDriver } from '../connections/dialects.js';
1
2
  import { FEDERATED_CONNECTION_ID } from '../connections/federation.js';
2
3
  import { resolveRequiredConnectionId } from '../connections/resolve-connection.js';
3
4
  import { sqlAnalysisDialectForDriver } from '../sql-analysis/dialect.js';
@@ -43,6 +44,10 @@ export async function compileLocalSlQuery(project, options) {
43
44
  await options.onProgress?.({ progress: 0, message: 'Compiling query' });
44
45
  const connectionId = resolveLocalConnectionId(project, options.connectionId);
45
46
  const driver = project.config.connections[connectionId]?.driver;
47
+ if (!isSqlQueryableDriver(driver)) {
48
+ throw new Error(`Semantic-layer queries require a SQL warehouse connection; '${connectionId}' uses the non-SQL driver ` +
49
+ `'${driver ?? 'unknown'}'. MongoDB and other context-only sources are searchable and ingestable, not SL-queryable.`);
50
+ }
46
51
  const dialect = sqlAnalysisDialectForDriver(driver);
47
52
  const sources = await loadComputableSources(project, connectionId);
48
53
  await options.onProgress?.({ progress: 0.3, message: 'Generating SQL' });
@@ -41,7 +41,7 @@ async function loadCandidates(project, input = {}) {
41
41
  const connectionIds = [
42
42
  ...new Set(listed.files
43
43
  .map((path) => path.split('/')[1])
44
- .filter((connectionId) => typeof connectionId === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(connectionId))),
44
+ .filter((connectionId) => typeof connectionId === 'string' && /^[a-zA-Z0-9_][a-zA-Z0-9_-]*$/.test(connectionId))),
45
45
  ].sort();
46
46
  const candidates = [];
47
47
  for (const connectionId of connectionIds) {
@@ -4,7 +4,7 @@ import { normalizeSemanticLayerDescriptions } from './description-normalization.
4
4
  import { isOverlaySource, resolvedSourceSchema, sourceDefinitionSchema, sourceOverlaySchema } from './schemas.js';
5
5
  import { isSlYamlPath, resolveSlSourceFile, slDeclaredSourceName, slSourceFilePath } from './source-files.js';
6
6
  const SL_DIR_PREFIX = 'semantic-layer';
7
- const CONNECTION_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
7
+ const CONNECTION_ID_PATTERN = /^[a-zA-Z0-9_][a-zA-Z0-9_-]*$/;
8
8
  /** @internal */
9
9
  export class UnknownColumnOverrideError extends Error {
10
10
  }
@@ -38,6 +38,11 @@ export declare function slSourceNameForFile(path: string, content: string): stri
38
38
  * filename — wrong for human-renamed files, whose filename is not the name.
39
39
  */
40
40
  export declare function slDeclaredSourceName(content: string): string | null;
41
+ /**
42
+ * Every standalone/overlay source file for a connection (excludes the `_schema/`
43
+ * manifest). The one listing entry points share so a file is visible to all.
44
+ */
45
+ export declare function listSlSourceFiles(fileStore: Pick<KtxFileStorePort, 'listFiles' | 'readFile'>, connectionId: string): Promise<SlSourceFile[]>;
41
46
  /**
42
47
  * Find the standalone/overlay file that defines `sourceName` for a connection.
43
48
  * Returns null when no file declares the name (the source may still exist as a
@@ -31,7 +31,7 @@ export function assertSafeConnectionId(connectionId) {
31
31
  return assertSafePathToken('connection id', connectionId);
32
32
  }
33
33
  export function isSafeConnectionId(connectionId) {
34
- return typeof connectionId === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(connectionId);
34
+ return typeof connectionId === 'string' && /^[a-zA-Z0-9_][a-zA-Z0-9_-]*$/.test(connectionId);
35
35
  }
36
36
  export function sourceNameFromPath(path) {
37
37
  return (path
@@ -113,24 +113,30 @@ export function slDeclaredSourceName(content) {
113
113
  return typeof name === 'string' && name.length > 0 ? name : null;
114
114
  }
115
115
  /**
116
- * Find the standalone/overlay file that defines `sourceName` for a connection.
117
- * Returns null when no file declares the name (the source may still exist as a
118
- * manifest entry under `_schema/`). Throws when more than one file declares the
119
- * same name — that breaks the one-file-per-name invariant and must be repaired
120
- * by hand rather than silently picking one.
116
+ * Every standalone/overlay source file for a connection (excludes the `_schema/`
117
+ * manifest). The one listing entry points share so a file is visible to all.
121
118
  */
122
- export async function resolveSlSourceFile(fileStore, connectionId, sourceName) {
119
+ export async function listSlSourceFiles(fileStore, connectionId) {
123
120
  const dir = `semantic-layer/${assertSafeConnectionId(connectionId)}`;
124
121
  const schemaDir = `${dir}/_schema`;
125
122
  const listed = await fileStore.listFiles(dir);
126
123
  const paths = listed.files.filter((file) => isSlYamlPath(file) && !file.startsWith(`${schemaDir}/`)).sort();
127
- const matches = [];
124
+ const files = [];
128
125
  for (const path of paths) {
129
126
  const raw = await fileStore.readFile(path);
130
- if (slSourceNameForFile(path, raw.content) === sourceName) {
131
- matches.push({ path, content: raw.content });
132
- }
127
+ files.push({ path, content: raw.content });
133
128
  }
129
+ return files;
130
+ }
131
+ /**
132
+ * Find the standalone/overlay file that defines `sourceName` for a connection.
133
+ * Returns null when no file declares the name (the source may still exist as a
134
+ * manifest entry under `_schema/`). Throws when more than one file declares the
135
+ * same name — that breaks the one-file-per-name invariant and must be repaired
136
+ * by hand rather than silently picking one.
137
+ */
138
+ export async function resolveSlSourceFile(fileStore, connectionId, sourceName) {
139
+ const matches = (await listSlSourceFiles(fileStore, connectionId)).filter((file) => slSourceNameForFile(file.path, file.content) === sourceName);
134
140
  if (matches.length > 1) {
135
141
  throw new Error(`Multiple semantic-layer files declare source "${sourceName}": ${matches.map((match) => match.path).join(', ')}`);
136
142
  }
@@ -2,4 +2,4 @@ import { z } from 'zod';
2
2
  export const slToolConnectionIdSchema = z
3
3
  .string()
4
4
  .min(1)
5
- .regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, 'Connection id must be alphanumeric and may contain _ or -');
5
+ .regex(/^[a-zA-Z0-9_][a-zA-Z0-9_-]*$/, 'Connection id must be alphanumeric and may contain _ or -');
@@ -0,0 +1,9 @@
1
+ import type { SqlAnalysisDialect } from './ports.js';
2
+ /** @internal Dialects with an authored ./dialects/<dialect>.md file. */
3
+ export declare const DIALECTS_WITH_NOTES: readonly ["postgres", "mysql", "snowflake", "bigquery", "sqlite", "clickhouse", "tsql"];
4
+ /**
5
+ * SQL syntax notes for a resolved dialect. Falls back to `postgres` — the
6
+ * resolver's own default for unrecognized drivers — so any SQL connection yields
7
+ * usable guidance rather than an empty string.
8
+ */
9
+ export declare function sqlDialectNotes(dialect: SqlAnalysisDialect): string;
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ // Per-engine SQL syntax notes live as markdown files under ./dialects (one per
4
+ // dialect), served by the sql_dialect_notes MCP tool. They are package-internal:
5
+ // copy-runtime-assets.mjs ships them to dist, and they are never installed onto an
6
+ // agent target. The set covers every dialect reachable from a configured warehouse
7
+ // driver; duckdb/databricks are intentionally absent because no connector produces
8
+ // them.
9
+ /** @internal Dialects with an authored ./dialects/<dialect>.md file. */
10
+ export const DIALECTS_WITH_NOTES = [
11
+ 'postgres',
12
+ 'mysql',
13
+ 'snowflake',
14
+ 'bigquery',
15
+ 'sqlite',
16
+ 'clickhouse',
17
+ 'tsql',
18
+ ];
19
+ const notesCache = new Map();
20
+ function readDialectNotes(dialect) {
21
+ const cached = notesCache.get(dialect);
22
+ if (cached !== undefined) {
23
+ return cached;
24
+ }
25
+ const path = fileURLToPath(new URL(`./dialects/${dialect}.md`, import.meta.url));
26
+ const content = readFileSync(path, 'utf-8').trimEnd();
27
+ notesCache.set(dialect, content);
28
+ return content;
29
+ }
30
+ function hasNotes(dialect) {
31
+ return DIALECTS_WITH_NOTES.includes(dialect);
32
+ }
33
+ /**
34
+ * SQL syntax notes for a resolved dialect. Falls back to `postgres` — the
35
+ * resolver's own default for unrecognized drivers — so any SQL connection yields
36
+ * usable guidance rather than an empty string.
37
+ */
38
+ export function sqlDialectNotes(dialect) {
39
+ return readDialectNotes(hasNotes(dialect) ? dialect : 'postgres');
40
+ }
@@ -0,0 +1,13 @@
1
+ **bigquery** SQL conventions:
2
+ - **FQTN:** backtick-quoted `` `project.dataset.table` `` (e.g. `` `my-proj.analytics.orders` ``); backticks are required when a name contains a dash.
3
+ - **Identifiers:** backtick to quote; column and field names are case-insensitive, dataset and table names are case-sensitive.
4
+ - **Date/time:** `DATE_TRUNC(d, MONTH)`, `EXTRACT(YEAR FROM ts)`, `PARSE_DATE('%Y-%m-%d', s)`, `FORMAT_DATE('%Y-%m', d)`, `CURRENT_DATE()`.
5
+ - **Series:** build a spine with `UNNEST(GENERATE_DATE_ARRAY('2023-01-01', '2023-12-01', INTERVAL 1 MONTH))` for dates (or `GENERATE_ARRAY(1, n)` for integers), then `LEFT JOIN` the aggregated facts onto it so empty periods still appear.
6
+ - **Rolling window over time:** `RANGE` frames are numeric, so range over an integer day key — `AVG(amount) OVER (ORDER BY UNIX_DATE(day) RANGE BETWEEN 29 PRECEDING AND CURRENT ROW)` is a trailing 30-day average that tolerates gaps; or build a spine (see **Series**) and use a `ROWS` frame.
7
+ - **Safe cast:** `SAFE_CAST(x AS FLOAT64)` (or `SAFE_CAST(x AS NUMERIC)`) returns `NULL` instead of erroring on a value that does not parse, so counting residual `NULL`s among non-sentinel rows catches an encoding the sample missed.
8
+ - **Safe divide:** `SAFE_DIVIDE(num, den)` returns `NULL` instead of erroring when the denominator is `0`, so a rate/ratio/share is one expression with no `CASE den = 0` guard; multiply by `100` for a percentage. Prefer it over `num / den` for any computed measure whose denominator can be zero.
9
+ - **Top-N / windows:** `QUALIFY` filters on a window result, e.g. `QUALIFY ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) = 1`.
10
+ - **JSON:** `JSON_VALUE(col, '$.k')` returns a scalar STRING, `JSON_QUERY(col, '$.k')` returns a subtree.
11
+ - **Nested & repeated data (ARRAY / STRUCT):** the defining BigQuery shape (e.g. GA360 `ga_sessions.hits`, GA4 `event_params`/`user_properties`). Flatten a repeated column by cross-joining `UNNEST` correlated to its row — `FROM t, UNNEST(t.hits) AS h, UNNEST(h.product) AS p` — and read STRUCT fields with dot notation (`h.page.pagePath`, `p.productRevenue`). Pull one value out of a key-value parameter array with a scalar subquery: `(SELECT ep.value.int_value FROM UNNEST(event_params) AS ep WHERE ep.key = 'page_view')`. An `UNNEST` multiplies the parent row by the array's length, so a `COUNT(*)`/`SUM` after it double-counts the parent — count the parent key with `COUNT(DISTINCT visitId)` (or aggregate *inside* the unnest); use `LEFT JOIN UNNEST(arr)` to keep rows whose array is empty.
12
+ - **Geospatial (GEOGRAPHY):** build a point with `ST_GEOGPOINT(longitude, latitude)` — **longitude first** — or parse text with `ST_GEOGFROMTEXT(wkt)` / `ST_GEOGFROMGEOJSON(s)`. Predicates: containment `ST_CONTAINS(area, pt)` / `ST_WITHIN(pt, area)` (`ST_WITHIN(a,b)=ST_CONTAINS(b,a)`); proximity `ST_DWITHIN(g1, g2, meters)` (geodesic); distance `ST_DISTANCE(g1, g2)` (meters); overlap `ST_INTERSECTS`. For areal allocation use `ST_AREA(g)` (m²) and `ST_AREA(ST_INTERSECTION(a, b))` for the overlapping area. Prefer these predicates over hand-rolled lat/lon `BETWEEN` boxes.
13
+ - **Sharded tables:** query a wildcard table `` `dataset.events_*` `` and filter the shard with the `_TABLE_SUFFIX` pseudo-column, e.g. `WHERE _TABLE_SUFFIX BETWEEN '20240101' AND '20240131'`. The wildcard spans only the shards that exist — before a measure that pins specific dates/periods, confirm the matching shards are actually present (an absent endpoint silently yields no rows, not an error).
@@ -0,0 +1,9 @@
1
+ **clickhouse** SQL conventions:
2
+ - **FQTN:** `database.table` (e.g. `analytics.orders`).
3
+ - **Identifiers:** quote with backticks (`` `Order` ``) or double quotes; identifiers are case-sensitive.
4
+ - **Date/time:** native `Date`/`DateTime` types. Bucket with `toStartOfMonth(ts)`, `toStartOfDay(ts)`, `toYYYYMM(ts)`; parse with `toDate(s)` / `parseDateTimeBestEffort(s)`; format with `formatDateTime(ts, '%Y-%m')`.
5
+ - **Series:** `numbers(n)` / `range(n)` generate an integer sequence; offset a start date with `addMonths(toDate('2023-01-01'), number)` (or `arrayJoin`) to form a spine, then `LEFT JOIN` the aggregated facts onto it so empty periods still appear.
6
+ - **Rolling window over time:** a numeric range frame over a `Date` column counts in days and tolerates gaps — `AVG(amount) OVER (ORDER BY day RANGE BETWEEN 29 PRECEDING AND CURRENT ROW)` is a trailing 30-day average (use seconds for a `DateTime` key; the `INTERVAL` form is unsupported); or build a spine (see **Series**) and use a `ROWS` frame.
7
+ - **Safe cast:** `toFloat64OrNull(x)` / `toDecimal64OrNull(x, s)` returns `NULL` on a value that does not parse (the `...OrZero` variants return `0` instead), so counting residual `NULL`s among non-sentinel rows catches an encoding the sample missed.
8
+ - **Top-N / windows:** use the `LIMIT n BY key` clause for n rows per key, or rank in a CTE with `ROW_NUMBER() OVER (...)` and filter outside it.
9
+ - **JSON:** extract from a String column with `JSONExtractString(col, 'k')`, `JSONExtractInt(col, 'k')`, etc.; a native `JSON`-typed column is traversed by dot path `col.k`.
@@ -0,0 +1,9 @@
1
+ **mysql** SQL conventions:
2
+ - **FQTN:** `database.table` (MySQL has no separate schema layer — a schema is a database).
3
+ - **Identifiers:** quote with backticks (`` `order` ``); table-name case-sensitivity follows the server filesystem, while column names are case-insensitive.
4
+ - **Date/time:** `DATE_FORMAT(ts, '%Y-%m')`, `STR_TO_DATE(s, fmt)`, `YEAR(ts)`/`MONTH(ts)`, `CURDATE()`, `NOW()`.
5
+ - **Series:** no series function — build a spine with a recursive CTE, e.g. `WITH RECURSIVE months(d) AS (SELECT '2023-01-01' UNION ALL SELECT DATE_ADD(d, INTERVAL 1 MONTH) FROM months WHERE d < '2023-12-01')`, then `LEFT JOIN` the aggregated facts onto it so empty periods still appear.
6
+ - **Rolling window over time:** a native interval range frame over a temporal order key tolerates gaps — `AVG(amount) OVER (ORDER BY day RANGE BETWEEN INTERVAL 29 DAY PRECEDING AND CURRENT ROW)` is a trailing 30-day average without a spine; guard minimum periods with `COUNT(*) OVER (<same frame>)`.
7
+ - **Safe cast:** MySQL has no `TRY_CAST`, and `CAST('abc' AS DECIMAL)` returns `0` with a warning rather than erroring — guard with a pattern test first: `CASE WHEN x REGEXP '^-?[0-9.]+$' THEN CAST(x AS DECIMAL(18,4)) END` makes a value that does not parse `NULL`, so a residual-`NULL` count catches an encoding the sample missed (`REGEXP_REPLACE` can strip symbols).
8
+ - **Top-N / windows:** rank in a CTE with `ROW_NUMBER() OVER (...)` and filter outside it; use `ORDER BY ... LIMIT n` for a global top-N.
9
+ - **JSON:** `JSON_EXTRACT(col, '$.k')`, or the `col->'$.k'` / `col->>'$.k'` shortcuts (`->>` unquotes to text).
@@ -0,0 +1,10 @@
1
+ **postgres** SQL conventions:
2
+ - **FQTN:** `schema.table` (e.g. `public.orders`); one query targets a single database, so qualify by schema, not by database.
3
+ - **Identifiers:** unquoted names fold to lower-case; double-quote (`"Name"`) only to keep case or use a reserved word.
4
+ - **Date/time:** `date_trunc('month', ts)`, `EXTRACT(YEAR FROM ts)`, `to_char(ts, 'YYYY-MM')`, `CURRENT_DATE`; cast text to a date with `col::date`.
5
+ - **Series:** build a date/number spine with `generate_series('2023-01-01'::date, '2023-12-01'::date, interval '1 month')` (or `generate_series(1, n)` for integers), then `LEFT JOIN` the aggregated facts onto it so empty periods still appear.
6
+ - **Rolling window over time:** a native calendar-range frame spans real dates and tolerates gaps — `AVG(amount) OVER (ORDER BY day RANGE BETWEEN INTERVAL '29 days' PRECEDING AND CURRENT ROW)` is a trailing 30-day average without a spine; guard minimum periods with `COUNT(*) OVER (<same frame>)`.
7
+ - **Integer division:** `/` between two integers truncates (`5 / 2` → `2`), so a rate or `SUM(a) / COUNT(*)` silently floors to an integer; cast one operand first — `a::numeric / b` or `a * 1.0 / b` — and round only in the final projection.
8
+ - **Safe cast:** postgres has no `TRY_CAST`; guard a text-encoded number with a pattern test before casting — `CASE WHEN x ~ '^-?[0-9.]+$' THEN x::numeric END` yields `NULL` for a value that does not parse, so counting residual `NULL`s among non-sentinel rows catches an encoding the sample missed (`regexp_replace` can strip symbols, but chained `REPLACE` is the portable default).
9
+ - **Top-N / windows:** rank in a CTE with `ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)` and filter in the outer query, or use `DISTINCT ON (key) ... ORDER BY key, ...` for one row per key.
10
+ - **JSON:** `col->'k'` returns json, `col->>'k'` returns text, deep path `col#>>'{a,b}'`; prefer `jsonb` operators on `jsonb` columns.
@@ -0,0 +1,10 @@
1
+ **snowflake** SQL conventions:
2
+ - **FQTN:** three-part `DATABASE.SCHEMA.TABLE` (e.g. `analytics.public.orders`).
3
+ - **Identifiers:** unquoted names fold to UPPER-case; double-quote for a case-sensitive or reserved name — `orders` resolves to `"ORDERS"`, which is a different object from `"orders"`.
4
+ - **Date/time:** `DATE_TRUNC('month', ts)`, `TO_DATE(s[, fmt])`, `DATEADD(day, -7, CURRENT_DATE)`, `CURRENT_DATE`.
5
+ - **Series:** generate rows with `TABLE(GENERATOR(ROWCOUNT => n))` and offset a start date via `DATEADD('month', SEQ4(), '2023-01-01')` (or a recursive CTE) to form a spine, then `LEFT JOIN` the aggregated facts onto it so empty periods still appear.
6
+ - **Rolling window over time:** a native interval range frame over a date/timestamp order key tolerates gaps — `AVG(amount) OVER (ORDER BY day RANGE BETWEEN INTERVAL '29 days' PRECEDING AND CURRENT ROW)` is a trailing 30-day average without a spine; guard minimum periods with `COUNT(*) OVER (<same frame>)`.
7
+ - **Safe cast:** `TRY_TO_NUMBER(x)` / `TRY_TO_DECIMAL(x, p, s)` (or `TRY_CAST(x AS NUMBER)`) returns `NULL` instead of erroring on a value that does not parse, so a residual-`NULL` count among non-sentinel rows catches an encoding the sample missed.
8
+ - **Top-N / windows:** `QUALIFY` filters on a window result without a subquery, e.g. `QUALIFY ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) = 1`.
9
+ - **Semi-structured (VARIANT):** traverse with a colon path and cast with `::`, e.g. `src:vehicle[0].make::string`, `payload:events.date::date`; expand arrays with `LATERAL FLATTEN`.
10
+ - **Geospatial (GEOGRAPHY):** build a point with `ST_MAKEPOINT(longitude, latitude)` — **longitude first** — or `TO_GEOGRAPHY(wkt_or_geojson)`; an area polygon from a closed ring of corner points with `ST_MAKEPOLYGON(ST_MAKELINE(ARRAY_CONSTRUCT(p1, p2, …, p1)))` (repeat the first point last to close). Predicates: proximity `ST_DWITHIN(g1, g2, meters)` (geodesic) and distance `ST_DISTANCE(g1, g2)` (meters); containment `ST_CONTAINS(area, pt)` / `ST_WITHIN(pt, area)` where `ST_WITHIN(a,b)=ST_CONTAINS(b,a)`; overlap `ST_INTERSECTS`. Prefer these predicates over hand-rolled lat/lon `BETWEEN` boxes.
@@ -0,0 +1,11 @@
1
+ **sqlite** SQL conventions:
2
+ - **FQTN:** usually the bare `table`; `main.table` to be explicit, `attached.table` for an attached database.
3
+ - **Identifiers:** case-insensitive; double-quote (`"Name"`) to preserve a name with spaces or a keyword.
4
+ - **Date/time:** there is no native date type — values are TEXT, INTEGER, or REAL. Format and bucket with `strftime('%Y-%m', col)`, `date(col)`, `datetime(col)`, and take day differences with `julianday(a) - julianday(b)`. Confirm the stored encoding (ISO text vs Unix epoch) before comparing.
5
+ - **Series:** no series function — build a spine with a recursive CTE, e.g. `WITH RECURSIVE months(d) AS (SELECT '2023-01-01' UNION ALL SELECT date(d, '+1 month') FROM months WHERE d < '2023-12-01')`, then `LEFT JOIN` the aggregated facts onto it so empty periods still appear.
6
+ - **Rolling window over time:** there is no date-interval range frame (a `RANGE` offset needs a single numeric order key, and dates are TEXT), so build a gap-free date spine (see **Series**) and use a row frame — `AVG(amount) OVER (ORDER BY day ROWS BETWEEN 29 PRECEDING AND CURRENT ROW)` then equals a trailing 30-day average; guard minimum periods with `COUNT(*) OVER (<same frame>)`.
7
+ - **Integer division:** `/` between two integers truncates (`5 / 2` → `2`), so a rate or `SUM(a) / COUNT(*)` silently floors to an integer; force real division with `a * 1.0 / b` (or `CAST(a AS REAL) / b`) and round only in the final projection.
8
+ - **Safe cast:** sqlite has no failure-signaling cast — `CAST('abc' AS REAL)` returns `0.0` and `CAST('12abc' AS REAL)` returns `12.0` (no error, no `NULL`), so an `IS NULL` coverage check silently passes. Detect a value that did not parse with a pattern guard before casting, e.g. `CASE WHEN cleaned NOT GLOB '*[^0-9.]*' THEN CAST(cleaned AS REAL) END` (strip any leading sign first), then count the residual `NULL`s.
9
+ - **Rounding (exact half-up at `.5` boundaries):** `ROUND(x, n)` rounds half-away-from-zero, but binary floating-point stores an exact half-way value just *below* it, so the round goes the wrong way — `ROUND(6.475, 2)` returns `6.47`, not `6.48`. When a rounded measure must match exact half-up (a displayed average, rate, or price), nudge by a tiny epsilon below display precision before rounding: `ROUND(x + 1e-9, n)` lifts `6.4749999…` back to `6.475` so it rounds to `6.48` (it leaves non-boundary values unchanged). Round once, at full precision, in the final projection — never in intermediate CTEs.
10
+ - **Top-N / windows:** rank in a CTE with `ROW_NUMBER() OVER (...)` and filter in the outer query; use `ORDER BY ... LIMIT n` for a global top-N.
11
+ - **JSON:** `json_extract(col, '$.k')`, or the `col->'$.k'` / `col->>'$.k'` operators (`->>` returns text).
@@ -0,0 +1,10 @@
1
+ **tsql** (SQL Server) SQL conventions:
2
+ - **FQTN:** `schema.table` (e.g. `dbo.orders`), or `database.schema.table` across databases.
3
+ - **Identifiers:** quote with square brackets (`[Order]`), or double quotes when `QUOTED_IDENTIFIER` is on; case-sensitivity is set by the database collation (commonly case-insensitive).
4
+ - **Date/time:** `DATEPART(year, ts)`, `DATEADD(day, -7, ts)`, `DATEDIFF(day, a, b)`, `CONVERT(date, ts)`, `FORMAT(ts, 'yyyy-MM')`, `GETDATE()`.
5
+ - **Series:** no series function — build a spine with a recursive CTE, e.g. `WITH months AS (SELECT CAST('2023-01-01' AS date) AS d UNION ALL SELECT DATEADD(month, 1, d) FROM months WHERE d < '2023-12-01')` (cap with `OPTION (MAXRECURSION 0)`), or a numbers/tally table, then `LEFT JOIN` the aggregated facts onto it so empty periods still appear.
6
+ - **Rolling window over time:** `RANGE` supports only `UNBOUNDED`/`CURRENT ROW` (no offset frame), so build a gap-free date spine (see **Series**) and use a row frame — `AVG(amount) OVER (ORDER BY day ROWS BETWEEN 29 PRECEDING AND CURRENT ROW)` — or a date-keyed self-join on `f.day BETWEEN DATEADD(day, -29, d.day) AND d.day`; guard minimum periods with `COUNT(*) OVER (<same frame>)`.
7
+ - **Integer division:** `/` between two `int`s truncates (`5 / 2` → `2`), so a rate or `SUM(a) / COUNT(*)` silently floors to an integer; cast one operand first — `CAST(a AS decimal(18,4)) / b` or `a * 1.0 / b` — and round only in the final projection.
8
+ - **Safe cast:** `TRY_CAST(x AS DECIMAL(18,4))` (or `TRY_CONVERT(decimal(18,4), x)`) returns `NULL` instead of erroring on a value that does not parse, so a residual-`NULL` count among non-sentinel rows catches an encoding the sample missed.
9
+ - **Top-N / windows:** `SELECT TOP (n) ... ORDER BY ...` for a global top-N; for per-group, rank in a CTE with `ROW_NUMBER() OVER (...)` and filter in the outer query.
10
+ - **JSON:** `JSON_VALUE(col, '$.k')` returns a scalar, `JSON_QUERY(col, '$.k')` returns an object/array, and `OPENJSON(col)` shreds JSON into rows.
@@ -1,4 +1,4 @@
1
- const FLAT_WIKI_KEY_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
1
+ const FLAT_WIKI_KEY_PATTERN = /^[a-zA-Z0-9_][a-zA-Z0-9_-]*$/;
2
2
  export function suggestFlatWikiKey(key) {
3
3
  const suggested = key
4
4
  .trim()
@@ -1,5 +1,5 @@
1
1
  import type { KtxEmbeddingPort } from '../../context/core/embedding.js';
2
- import type { KtxFileStorePort } from '../../context/core/file-store.js';
2
+ import type { KtxFileStorePort, KtxFileWriteResult } from '../../context/core/file-store.js';
3
3
  import type { KtxLogger } from '../../context/core/config.js';
4
4
  import type { ReindexWorkResult } from '../index-sync/types.js';
5
5
  import type { KnowledgeGitDiffPort, KnowledgeIndexPort } from './ports.js';
@@ -31,9 +31,9 @@ export declare class KnowledgeWikiService {
31
31
  serializePage(frontmatter: WikiFrontmatter, content: string): string;
32
32
  writePage(scope: string, scopeId: string | null | undefined, pageKey: string, frontmatter: WikiFrontmatter, content: string, author: string, authorEmail: string, commitMessage?: string, options?: {
33
33
  skipLock?: boolean;
34
- }): Promise<import("../../context/core/file-store.js").KtxFileWriteResult>;
34
+ }): Promise<KtxFileWriteResult>;
35
35
  readPage(scope: string, scopeId: string | null | undefined, pageKey: string): Promise<WikiPage | null>;
36
- deletePage(scope: string, scopeId: string | null | undefined, pageKey: string, author: string, authorEmail: string): Promise<import("../../context/core/file-store.js").KtxFileWriteResult | null>;
36
+ deletePage(scope: string, scopeId: string | null | undefined, pageKey: string, author: string, authorEmail: string): Promise<KtxFileWriteResult | null>;
37
37
  listPageKeys(scope: string, scopeId?: string | null): Promise<string[]>;
38
38
  getPageHistory(scope: string, scopeId: string | null | undefined, pageKey: string): Promise<unknown>;
39
39
  readPageForUser(userId: string, pageKey: string): Promise<WikiPageWithScope | null>;
@@ -49,7 +49,7 @@ export declare class KnowledgeWikiService {
49
49
  * Write a wiki page and then sync it to the DB search index.
50
50
  * Chains the two operations so the index is only updated after the file write succeeds.
51
51
  */
52
- writePageAndSync(scope: string, scopeId: string | null | undefined, pageKey: string, frontmatter: WikiFrontmatter, content: string, author: string, authorEmail: string, commitMessage?: string): Promise<void>;
52
+ writePageAndSync(scope: string, scopeId: string | null | undefined, pageKey: string, frontmatter: WikiFrontmatter, content: string, author: string, authorEmail: string, commitMessage?: string): Promise<KtxFileWriteResult>;
53
53
  /**
54
54
  * Sync a single page to the DB search index after a write.
55
55
  * Computes search_text and embedding, then upserts to knowledge index.
@@ -166,10 +166,11 @@ export class KnowledgeWikiService {
166
166
  * Chains the two operations so the index is only updated after the file write succeeds.
167
167
  */
168
168
  async writePageAndSync(scope, scopeId, pageKey, frontmatter, content, author, authorEmail, commitMessage) {
169
- await this.writePage(scope, scopeId, pageKey, frontmatter, content, author, authorEmail, commitMessage);
169
+ const writeResult = await this.writePage(scope, scopeId, pageKey, frontmatter, content, author, authorEmail, commitMessage);
170
170
  const serialized = this.serializePage(frontmatter, content);
171
171
  const contentHash = createHash('sha256').update(serialized).digest('hex');
172
172
  await this.syncSinglePage(scope, scopeId, pageKey, frontmatter, content, contentHash);
173
+ return writeResult;
173
174
  }
174
175
  // ── Index sync (files → DB) ───────────────────────────────────
175
176
  /**
@@ -12,6 +12,7 @@ export interface LocalKnowledgePage {
12
12
  tags: string[];
13
13
  refs: string[];
14
14
  slRefs: string[];
15
+ connections: string[];
15
16
  }
16
17
  export interface LocalKnowledgeSummary {
17
18
  key: string;
@@ -40,6 +41,7 @@ export interface WriteLocalKnowledgePageInput {
40
41
  representativeSql?: string;
41
42
  usage?: HistoricSqlWikiUsageFrontmatter;
42
43
  fingerprints?: string[];
44
+ connections?: string[];
43
45
  }
44
46
  /** @internal */
45
47
  export declare function writeLocalKnowledgePage(project: KtxLocalProject, input: WriteLocalKnowledgePageInput): Promise<KtxFileWriteResult>;
@@ -49,6 +51,7 @@ export declare function readLocalKnowledgePage(project: KtxLocalProject, input:
49
51
  }): Promise<LocalKnowledgePage | null>;
50
52
  export declare function listLocalKnowledgePages(project: KtxLocalProject, input?: {
51
53
  userId?: string;
54
+ connectionId?: string;
52
55
  }): Promise<LocalKnowledgeSummary[]>;
53
56
  /**
54
57
  * List wiki page keys without reading or parsing file contents.
@@ -60,9 +63,19 @@ export declare function listLocalKnowledgePages(project: KtxLocalProject, input?
60
63
  export declare function listLocalKnowledgePageKeys(project: KtxLocalProject, input?: {
61
64
  userId?: string;
62
65
  }): Promise<string[]>;
66
+ /**
67
+ * Connection ids referenced by any stored page's `connections` frontmatter,
68
+ * sorted and deduped. Derived from files; an id here that is not configured in
69
+ * `ktx.yaml` is a warn-only condition (config and content evolve independently)
70
+ * and never blocks loading, searching, or reading.
71
+ */
72
+ export declare function listReferencedConnectionIds(project: KtxLocalProject, input?: {
73
+ userId?: string;
74
+ }): Promise<string[]>;
63
75
  export declare function searchLocalKnowledgePages(project: KtxLocalProject, input: {
64
76
  query: string;
65
77
  userId?: string;
78
+ connectionId?: string;
66
79
  embeddingService?: KtxEmbeddingPort | null;
67
80
  limit?: number;
68
81
  }): Promise<LocalKnowledgeSearchResult[]>;
@@ -20,6 +20,17 @@ function assertSafePathToken(kind, value) {
20
20
  function stringArray(value) {
21
21
  return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
22
22
  }
23
+ /** Coerce a YAML scalar or list into a string list — `connections` accepts a single id or a list. */
24
+ function stringList(value) {
25
+ if (typeof value === 'string') {
26
+ return value.trim().length > 0 ? [value] : [];
27
+ }
28
+ return stringArray(value);
29
+ }
30
+ /** A page applies to `connectionId` when it is unscoped (empty) or lists that id. */
31
+ function pageMatchesConnection(connections, connectionId) {
32
+ return connectionId === undefined || connections.length === 0 || connections.includes(connectionId);
33
+ }
23
34
  function knowledgePath(scope, userId, key) {
24
35
  const safeKey = assertFlatWikiKey(key);
25
36
  if (scope === 'GLOBAL') {
@@ -47,6 +58,7 @@ function parseKnowledgePage(key, path, scope, raw) {
47
58
  tags: [],
48
59
  refs: [],
49
60
  slRefs: [],
61
+ connections: [],
50
62
  };
51
63
  }
52
64
  const frontmatter = (YAML.parse(match[1]) ?? {});
@@ -59,6 +71,7 @@ function parseKnowledgePage(key, path, scope, raw) {
59
71
  tags: stringArray(frontmatter.tags),
60
72
  refs: stringArray(frontmatter.refs),
61
73
  slRefs: stringArray(frontmatter.sl_refs),
74
+ connections: stringList(frontmatter.connections),
62
75
  };
63
76
  }
64
77
  function serializeKnowledgePage(input) {
@@ -74,6 +87,7 @@ function serializeKnowledgePage(input) {
74
87
  ...(input.representativeSql === undefined ? {} : { representative_sql: input.representativeSql }),
75
88
  ...(input.usage === undefined ? {} : { usage: input.usage }),
76
89
  ...(input.fingerprints === undefined ? {} : { fingerprints: input.fingerprints }),
90
+ ...(input.connections === undefined ? {} : { connections: input.connections }),
77
91
  };
78
92
  return `---\n${YAML.stringify(frontmatter, { indent: 2, lineWidth: 0 }).trimEnd()}\n---\n\n${input.content.trim()}\n`;
79
93
  }
@@ -111,7 +125,7 @@ export async function listLocalKnowledgePages(project, input = {}) {
111
125
  continue;
112
126
  }
113
127
  const page = await readPageAtPath(project, key, path, scope);
114
- if (page) {
128
+ if (page && pageMatchesConnection(page.connections, input.connectionId)) {
115
129
  pages.push({ key, path, scope, summary: page.summary });
116
130
  }
117
131
  }
@@ -140,6 +154,22 @@ export async function listLocalKnowledgePageKeys(project, input = {}) {
140
154
  }
141
155
  return [...keys].sort();
142
156
  }
157
+ /**
158
+ * Connection ids referenced by any stored page's `connections` frontmatter,
159
+ * sorted and deduped. Derived from files; an id here that is not configured in
160
+ * `ktx.yaml` is a warn-only condition (config and content evolve independently)
161
+ * and never blocks loading, searching, or reading.
162
+ */
163
+ export async function listReferencedConnectionIds(project, input = {}) {
164
+ const pages = await loadAllKnowledgePages(project, { userId: input.userId });
165
+ const ids = new Set();
166
+ for (const page of pages) {
167
+ for (const id of page.connections) {
168
+ ids.add(id);
169
+ }
170
+ }
171
+ return [...ids].sort();
172
+ }
143
173
  function scorePage(page, terms) {
144
174
  const haystack = buildKnowledgeSearchText(page.key, page.summary, page.content, page.tags).toLowerCase();
145
175
  return terms.some((term) => haystack.includes(term)) ? 3 : 0;
@@ -170,7 +200,10 @@ function tokenLaneCandidates(pages, terms) {
170
200
  .sort((left, right) => right.score - left.score || left.page.path.localeCompare(right.page.path));
171
201
  }
172
202
  async function loadAllKnowledgePages(project, input = {}) {
173
- const summaries = await listLocalKnowledgePages(project, { userId: input.userId });
203
+ const summaries = await listLocalKnowledgePages(project, {
204
+ userId: input.userId,
205
+ connectionId: input.connectionId,
206
+ });
174
207
  const pages = [];
175
208
  for (const summary of summaries) {
176
209
  const page = await readPageAtPath(project, summary.key, summary.path, summary.scope);
@@ -181,8 +214,17 @@ async function loadAllKnowledgePages(project, input = {}) {
181
214
  return pages;
182
215
  }
183
216
  async function searchLocalKnowledgePagesWithSqlite(project, input) {
217
+ // The sqlite index is shared across connections and `index.sync` deletes any
218
+ // page not in its input, so sync the FULL corpus and apply the connection
219
+ // filter only to the candidate/result set (`allowedPaths`), never to sync.
184
220
  const pages = await loadAllKnowledgePages(project, { userId: input.userId });
185
- const byPath = new Map(pages.map((page) => [page.path, page]));
221
+ const allowedPaths = new Set(pages.filter((page) => pageMatchesConnection(page.connections, input.connectionId)).map((page) => page.path));
222
+ const allowedPages = pages.filter((page) => allowedPaths.has(page.path));
223
+ // Scope the lexical/semantic lanes inside the query so their LIMIT applies to
224
+ // in-scope rows; only narrow when a connection is requested (otherwise every
225
+ // path is allowed and the filter is a no-op).
226
+ const scopedPaths = input.connectionId === undefined ? undefined : [...allowedPaths];
227
+ const byPath = new Map(allowedPages.map((page) => [page.path, page]));
186
228
  const embeddingService = input.embeddingService ?? null;
187
229
  const index = new SqliteKnowledgeIndex({ dbPath: sqliteKnowledgeDbPath(project) });
188
230
  const existingPages = index.getExistingPages();
@@ -204,7 +246,7 @@ async function searchLocalKnowledgePagesWithSqlite(project, input) {
204
246
  });
205
247
  }
206
248
  index.sync(indexPages);
207
- const finalLimit = input.limit ?? Math.max(1, indexPages.length);
249
+ const finalLimit = input.limit ?? Math.max(1, allowedPages.length);
208
250
  const core = new HybridSearchCore();
209
251
  const generators = [
210
252
  {
@@ -213,6 +255,7 @@ async function searchLocalKnowledgePagesWithSqlite(project, input) {
213
255
  const rows = index.searchLexicalCandidates({
214
256
  queryText: args.queryText,
215
257
  limit: args.laneCandidatePoolLimit,
258
+ allowedPaths: scopedPaths,
216
259
  });
217
260
  return {
218
261
  candidates: rows.map((row) => ({ id: row.id, rank: row.rank, rawScore: row.rawScore })),
@@ -222,7 +265,7 @@ async function searchLocalKnowledgePagesWithSqlite(project, input) {
222
265
  {
223
266
  lane: 'token',
224
267
  async generate(args) {
225
- const rows = tokenLaneCandidates(pages, args.normalizedQuery.terms).slice(0, args.laneCandidatePoolLimit);
268
+ const rows = tokenLaneCandidates(allowedPages, args.normalizedQuery.terms).slice(0, args.laneCandidatePoolLimit);
226
269
  return {
227
270
  candidates: rows.map((row, index) => ({
228
271
  id: row.page.path,
@@ -244,6 +287,7 @@ async function searchLocalKnowledgePagesWithSqlite(project, input) {
244
287
  const rows = index.searchSemanticCandidates({
245
288
  queryEmbedding,
246
289
  limit: args.laneCandidatePoolLimit,
290
+ allowedPaths: scopedPaths,
247
291
  });
248
292
  return {
249
293
  candidates: rows
@@ -285,7 +329,7 @@ async function searchLocalKnowledgePagesWithScan(project, input) {
285
329
  .split(/\s+/)
286
330
  .map((term) => term.trim())
287
331
  .filter(Boolean);
288
- const pages = await loadAllKnowledgePages(project, { userId: input.userId });
332
+ const pages = await loadAllKnowledgePages(project, { userId: input.userId, connectionId: input.connectionId });
289
333
  const results = [];
290
334
  for (const page of pages) {
291
335
  const score = scorePage(page, terms);
@@ -36,10 +36,12 @@ export declare class SqliteKnowledgeIndex {
36
36
  searchLexicalCandidates(input: {
37
37
  queryText: string;
38
38
  limit: number;
39
+ allowedPaths?: readonly string[];
39
40
  }): WikiSqliteLaneCandidate[];
40
41
  searchSemanticCandidates(input: {
41
42
  queryEmbedding: number[];
42
43
  limit: number;
44
+ allowedPaths?: readonly string[];
43
45
  }): WikiSqliteLaneCandidate[];
44
46
  search(query: string, limit: number): SqliteKnowledgeIndexSearchResult[];
45
47
  private pathForPage;
@@ -38,6 +38,17 @@ function parseEmbedding(raw) {
38
38
  return null;
39
39
  }
40
40
  }
41
+ /** A provided-but-empty allowlist means "no page is in scope", distinct from an absent (unfiltered) one. */
42
+ function isEmptyAllowlist(allowedPaths) {
43
+ return allowedPaths !== undefined && allowedPaths.length === 0;
44
+ }
45
+ /** Build a `<keyword> path IN (?, …)` fragment so the scope filter applies inside the query, before any LIMIT. */
46
+ function pathInClause(keyword, allowedPaths) {
47
+ if (allowedPaths === undefined || allowedPaths.length === 0) {
48
+ return { sql: '', params: [] };
49
+ }
50
+ return { sql: ` ${keyword} path IN (${allowedPaths.map(() => '?').join(', ')})`, params: [...allowedPaths] };
51
+ }
41
52
  function normalizeFtsQuery(query) {
42
53
  const terms = query
43
54
  .toLowerCase()
@@ -156,18 +167,19 @@ export class SqliteKnowledgeIndex {
156
167
  }
157
168
  searchLexicalCandidates(input) {
158
169
  const ftsQuery = normalizeFtsQuery(input.queryText);
159
- if (!ftsQuery) {
170
+ if (!ftsQuery || isEmptyAllowlist(input.allowedPaths)) {
160
171
  return [];
161
172
  }
173
+ const pathFilter = pathInClause('AND', input.allowedPaths);
162
174
  const rows = this.db
163
175
  .prepare(`
164
176
  SELECT path, bm25(knowledge_pages_fts) AS rank
165
177
  FROM knowledge_pages_fts
166
- WHERE knowledge_pages_fts MATCH ?
178
+ WHERE knowledge_pages_fts MATCH ?${pathFilter.sql}
167
179
  ORDER BY rank ASC, path ASC
168
180
  LIMIT ?
169
181
  `)
170
- .all(ftsQuery, Math.max(1, input.limit));
182
+ .all(ftsQuery, ...pathFilter.params, Math.max(1, input.limit));
171
183
  return rows.map((row, index) => ({
172
184
  id: row.path,
173
185
  path: row.path,
@@ -176,13 +188,17 @@ export class SqliteKnowledgeIndex {
176
188
  }));
177
189
  }
178
190
  searchSemanticCandidates(input) {
191
+ if (isEmptyAllowlist(input.allowedPaths)) {
192
+ return [];
193
+ }
194
+ const pathFilter = pathInClause('WHERE', input.allowedPaths);
179
195
  const rows = this.db
180
196
  .prepare(`
181
197
  SELECT path, embedding_json
182
- FROM knowledge_pages
198
+ FROM knowledge_pages${pathFilter.sql}
183
199
  ORDER BY path ASC
184
200
  `)
185
- .all();
201
+ .all(...pathFilter.params);
186
202
  return rows
187
203
  .flatMap((row) => {
188
204
  if (!row.embedding_json) {
@@ -15,6 +15,7 @@ declare const wikiWriteInputSchema: z.ZodObject<{
15
15
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
16
16
  refs: z.ZodOptional<z.ZodArray<z.ZodString>>;
17
17
  sl_refs: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
+ connections: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
18
19
  source: z.ZodOptional<z.ZodString>;
19
20
  intent: z.ZodOptional<z.ZodString>;
20
21
  tables: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -58,6 +59,7 @@ export declare class WikiWriteTool extends BaseTool<typeof wikiWriteInputSchema>
58
59
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
59
60
  refs: z.ZodOptional<z.ZodArray<z.ZodString>>;
60
61
  sl_refs: z.ZodOptional<z.ZodArray<z.ZodString>>;
62
+ connections: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
61
63
  source: z.ZodOptional<z.ZodString>;
62
64
  intent: z.ZodOptional<z.ZodString>;
63
65
  tables: z.ZodOptional<z.ZodArray<z.ZodString>>;