@kaelio/ktx 0.14.0 → 0.16.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 (96) hide show
  1. package/assets/python/kaelio_ktx-0.16.0-py3-none-any.whl +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/commands/setup-commands.js +2 -0
  5. package/dist/connection-drivers.d.ts +1 -1
  6. package/dist/connection-drivers.js +2 -0
  7. package/dist/connection.js +1 -0
  8. package/dist/connectors/athena/connector.d.ts +160 -0
  9. package/dist/connectors/athena/connector.js +378 -0
  10. package/dist/connectors/athena/dialect.d.ts +31 -0
  11. package/dist/connectors/athena/dialect.js +146 -0
  12. package/dist/connectors/athena/live-database-introspection.d.ts +10 -0
  13. package/dist/connectors/athena/live-database-introspection.js +24 -0
  14. package/dist/connectors/duckdb/connector.d.ts +60 -0
  15. package/dist/connectors/duckdb/connector.js +282 -0
  16. package/dist/connectors/duckdb/dialect.d.ts +31 -0
  17. package/dist/connectors/duckdb/dialect.js +155 -0
  18. package/dist/connectors/duckdb/federated-attach.js +7 -0
  19. package/dist/connectors/duckdb/federated-executor.js +7 -13
  20. package/dist/connectors/duckdb/live-database-introspection.d.ts +8 -0
  21. package/dist/connectors/duckdb/live-database-introspection.js +24 -0
  22. package/dist/connectors/shared/duckdb-json-safe.d.ts +3 -0
  23. package/dist/connectors/shared/duckdb-json-safe.js +12 -0
  24. package/dist/context/connections/connection-type-dialect.d.ts +2 -0
  25. package/dist/context/connections/connection-type-dialect.js +20 -0
  26. package/dist/context/connections/connection-type.d.ts +6 -15
  27. package/dist/context/connections/connection-type.js +1 -10
  28. package/dist/context/connections/dialects.js +4 -0
  29. package/dist/context/connections/drivers.js +38 -0
  30. package/dist/context/connections/federation.d.ts +1 -1
  31. package/dist/context/connections/federation.js +6 -5
  32. package/dist/context/connections/local-warehouse-descriptor.d.ts +1 -0
  33. package/dist/context/connections/local-warehouse-descriptor.js +10 -0
  34. package/dist/context/connections/project-sql-executor.d.ts +24 -0
  35. package/dist/context/connections/project-sql-executor.js +56 -0
  36. package/dist/context/connections/query-policy.d.ts +12 -0
  37. package/dist/context/connections/query-policy.js +46 -0
  38. package/dist/context/core/config-reference.js +11 -1
  39. package/dist/context/daemon/semantic-layer-compute.d.ts +16 -0
  40. package/dist/context/daemon/semantic-layer-compute.js +44 -2
  41. package/dist/context/ingest/adapters/looker/mapping.d.ts +1 -0
  42. package/dist/context/ingest/adapters/looker/mapping.js +3 -10
  43. package/dist/context/ingest/adapters/looker/types.d.ts +6 -15
  44. package/dist/context/ingest/adapters/metabase/mapping.d.ts +1 -0
  45. package/dist/context/ingest/adapters/metabase/mapping.js +1 -0
  46. package/dist/context/ingest/adapters/sigma/chunk.d.ts +6 -0
  47. package/dist/context/ingest/adapters/sigma/chunk.js +119 -0
  48. package/dist/context/ingest/adapters/sigma/client-port.d.ts +45 -0
  49. package/dist/context/ingest/adapters/sigma/client-port.js +1 -0
  50. package/dist/context/ingest/adapters/sigma/client.d.ts +33 -0
  51. package/dist/context/ingest/adapters/sigma/client.js +176 -0
  52. package/dist/context/ingest/adapters/sigma/detect.d.ts +1 -0
  53. package/dist/context/ingest/adapters/sigma/detect.js +23 -0
  54. package/dist/context/ingest/adapters/sigma/fetch.d.ts +14 -0
  55. package/dist/context/ingest/adapters/sigma/fetch.js +191 -0
  56. package/dist/context/ingest/adapters/sigma/local-sigma.adapter.d.ts +17 -0
  57. package/dist/context/ingest/adapters/sigma/local-sigma.adapter.js +41 -0
  58. package/dist/context/ingest/adapters/sigma/project.d.ts +8 -0
  59. package/dist/context/ingest/adapters/sigma/project.js +186 -0
  60. package/dist/context/ingest/adapters/sigma/sigma.adapter.d.ts +18 -0
  61. package/dist/context/ingest/adapters/sigma/sigma.adapter.js +43 -0
  62. package/dist/context/ingest/adapters/sigma/types.d.ts +80 -0
  63. package/dist/context/ingest/adapters/sigma/types.js +82 -0
  64. package/dist/context/ingest/local-adapters.d.ts +2 -1
  65. package/dist/context/ingest/local-adapters.js +22 -0
  66. package/dist/context/mcp/context-tools.js +1 -0
  67. package/dist/context/mcp/local-project-ports.d.ts +1 -1
  68. package/dist/context/mcp/local-project-ports.js +53 -51
  69. package/dist/context/project/config.d.ts +122 -0
  70. package/dist/context/project/driver-schemas.d.ts +61 -0
  71. package/dist/context/project/driver-schemas.js +47 -0
  72. package/dist/context/scan/local-scan.js +3 -1
  73. package/dist/context/scan/types.d.ts +1 -1
  74. package/dist/context/sl/local-query.js +23 -6
  75. package/dist/context/sl/semantic-layer.service.d.ts +0 -4
  76. package/dist/context/sl/semantic-layer.service.js +3 -20
  77. package/dist/context/sl/tools/sl-warehouse-validation.d.ts +1 -1
  78. package/dist/context/sl/tools/sl-warehouse-validation.js +2 -2
  79. package/dist/context/sl/types.d.ts +1 -0
  80. package/dist/context/sql-analysis/dialect-notes.d.ts +1 -1
  81. package/dist/context/sql-analysis/dialect-notes.js +3 -2
  82. package/dist/context/sql-analysis/dialect.js +1 -0
  83. package/dist/context/sql-analysis/dialects/athena.md +12 -0
  84. package/dist/context/sql-analysis/dialects/duckdb.md +10 -0
  85. package/dist/local-adapters.js +17 -0
  86. package/dist/public-ingest.js +1 -0
  87. package/dist/setup-databases.d.ts +1 -1
  88. package/dist/setup-databases.js +52 -2
  89. package/dist/setup-sources.d.ts +2 -1
  90. package/dist/setup-sources.js +84 -0
  91. package/dist/skills/sigma_ingest/SKILL.md +189 -0
  92. package/dist/sql.js +9 -14
  93. package/dist/status-project.js +2 -1
  94. package/dist/telemetry/events.d.ts +1 -1
  95. package/package.json +3 -1
  96. package/assets/python/kaelio_ktx-0.14.0-py3-none-any.whl +0 -0
@@ -0,0 +1,191 @@
1
+ import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { parseSigmaPullConfig, stagedDataModelFileSchema, stagedWorkbookFileSchema, STAGED_FILES, } from './types.js';
4
+ const noopLogger = { log: () => undefined, warn: () => undefined };
5
+ async function loadExistingStagedFiles(stagedDir) {
6
+ const existing = new Map();
7
+ const dmDir = join(stagedDir, STAGED_FILES.dataModelsDir);
8
+ let entries;
9
+ try {
10
+ entries = await readdir(dmDir);
11
+ }
12
+ catch {
13
+ return existing;
14
+ }
15
+ for (const entry of entries) {
16
+ if (!entry.endsWith('.json'))
17
+ continue;
18
+ try {
19
+ const body = await readFile(join(dmDir, entry), 'utf-8');
20
+ const parsed = stagedDataModelFileSchema.parse(JSON.parse(body));
21
+ existing.set(parsed.sigmaId, parsed);
22
+ }
23
+ catch {
24
+ // Skip malformed files.
25
+ }
26
+ }
27
+ return existing;
28
+ }
29
+ async function loadExistingWorkbookFiles(stagedDir) {
30
+ const existing = new Map();
31
+ const wbDir = join(stagedDir, STAGED_FILES.workbooksDir);
32
+ let entries;
33
+ try {
34
+ entries = await readdir(wbDir);
35
+ }
36
+ catch {
37
+ return existing;
38
+ }
39
+ for (const entry of entries) {
40
+ if (!entry.endsWith('.json'))
41
+ continue;
42
+ try {
43
+ const body = await readFile(join(wbDir, entry), 'utf-8');
44
+ const parsed = stagedWorkbookFileSchema.parse(JSON.parse(body));
45
+ existing.set(parsed.sigmaId, parsed);
46
+ }
47
+ catch {
48
+ // Skip malformed files.
49
+ }
50
+ }
51
+ return existing;
52
+ }
53
+ export async function fetchSigmaBundle({ pullConfig, stagedDir, ctx, clientFactory, logger = noopLogger, }) {
54
+ const config = parseSigmaPullConfig(pullConfig);
55
+ const client = await clientFactory.createClient(config, ctx);
56
+ try {
57
+ await mkdir(join(stagedDir, STAGED_FILES.dataModelsDir), { recursive: true });
58
+ await mkdir(join(stagedDir, STAGED_FILES.workbooksDir), { recursive: true });
59
+ // Load existing staged files to enable incremental sync.
60
+ const existingByModelId = await loadExistingStagedFiles(stagedDir);
61
+ const existingByWorkbookId = await loadExistingWorkbookFiles(stagedDir);
62
+ logger.log('Listing Sigma data models...');
63
+ const summaries = await client.listDataModels();
64
+ const nonArchived = summaries.filter((dm) => !dm.isArchived);
65
+ const nonArchivedIds = new Set(nonArchived.map((dm) => dm.dataModelId));
66
+ let active = nonArchived;
67
+ if (config.dataModelFilter?.updatedSince) {
68
+ const since = new Date(config.dataModelFilter.updatedSince).getTime();
69
+ active = active.filter((dm) => new Date(dm.updatedAt).getTime() >= since);
70
+ }
71
+ logger.log(`Found ${active.length} active data model(s) (${summaries.length} total).`);
72
+ let fetched = 0;
73
+ let skipped = 0;
74
+ const SPEC_CONCURRENCY = 10;
75
+ const queue = [...active];
76
+ await Promise.all(Array.from({ length: Math.min(SPEC_CONCURRENCY, queue.length) }, async () => {
77
+ let summary;
78
+ while ((summary = queue.shift()) !== undefined) {
79
+ const existing = existingByModelId.get(summary.dataModelId);
80
+ // Only skip when the cached spec was successfully fetched. spec: null means
81
+ // the previous attempt failed transiently — retry regardless of updatedAt.
82
+ if (existing && existing.updatedAt === summary.updatedAt && existing.spec !== null) {
83
+ logger.log(`Unchanged: ${summary.name}`);
84
+ skipped++;
85
+ continue;
86
+ }
87
+ let spec = null;
88
+ try {
89
+ spec = await client.getDataModelSpec(summary.dataModelId);
90
+ }
91
+ catch (err) {
92
+ const msg = err instanceof Error ? err.message : String(err);
93
+ if (msg.includes('dataSource subtype not supported')) {
94
+ logger.warn(`Skipping spec for "${summary.name}" (${summary.dataModelId}): data source type not supported by Sigma spec export API.`);
95
+ }
96
+ else {
97
+ logger.warn(`Failed to fetch spec for "${summary.name}" (${summary.dataModelId}): ${msg}`);
98
+ }
99
+ }
100
+ const staged = {
101
+ sigmaId: summary.dataModelId,
102
+ name: summary.name,
103
+ path: summary.path,
104
+ latestVersion: summary.latestVersion,
105
+ updatedAt: summary.updatedAt,
106
+ isArchived: summary.isArchived ?? false,
107
+ dataModelUrlId: summary.dataModelUrlId,
108
+ spec,
109
+ };
110
+ const filePath = join(stagedDir, STAGED_FILES.dataModelsDir, `${summary.dataModelId}.json`);
111
+ await writeFile(filePath, JSON.stringify(staged, null, 2), 'utf-8');
112
+ logger.log(`Staged data model: ${summary.name}`);
113
+ fetched++;
114
+ }
115
+ }));
116
+ // Remove staged files for models that are archived or deleted — but not those merely outside the filter window.
117
+ for (const [modelId] of existingByModelId) {
118
+ if (nonArchivedIds.has(modelId))
119
+ continue;
120
+ try {
121
+ await rm(join(stagedDir, STAGED_FILES.dataModelsDir, `${modelId}.json`));
122
+ logger.log(`Removed stale staged file for model ${modelId}.`);
123
+ }
124
+ catch {
125
+ // Best-effort removal.
126
+ }
127
+ }
128
+ // Fetch workbooks (summary metadata only — no separate spec endpoint).
129
+ // Fetch the full non-archived/non-exploration universe first so eviction is based on
130
+ // all known workbooks, not just the updatedSince slice. Mirrors the data-model path.
131
+ logger.log('Listing Sigma workbooks...');
132
+ const { updatedSince, ...filterWithoutSince } = config.workbookFilter ?? {};
133
+ const allWorkbooks = await client.listWorkbooks(filterWithoutSince);
134
+ const nonArchivedWorkbookIds = new Set(allWorkbooks.map((wb) => wb.workbookId));
135
+ const activeWorkbooks = updatedSince
136
+ ? allWorkbooks.filter((wb) => new Date(wb.updatedAt).getTime() >= new Date(updatedSince).getTime())
137
+ : allWorkbooks;
138
+ logger.log(`Found ${activeWorkbooks.length} workbook(s) to process (${allWorkbooks.length} total).`);
139
+ let workbooksFetched = 0;
140
+ let workbooksSkipped = 0;
141
+ for (const wb of activeWorkbooks) {
142
+ const existing = existingByWorkbookId.get(wb.workbookId);
143
+ if (existing && existing.updatedAt === wb.updatedAt) {
144
+ workbooksSkipped++;
145
+ continue;
146
+ }
147
+ const staged = {
148
+ sigmaId: wb.workbookId,
149
+ name: wb.name,
150
+ path: wb.path,
151
+ latestVersion: wb.latestVersion,
152
+ updatedAt: wb.updatedAt,
153
+ isArchived: wb.isArchived ?? false,
154
+ workbookUrlId: wb.workbookUrlId,
155
+ description: wb.description,
156
+ };
157
+ const filePath = join(stagedDir, STAGED_FILES.workbooksDir, `${wb.workbookId}.json`);
158
+ await writeFile(filePath, JSON.stringify(staged, null, 2), 'utf-8');
159
+ logger.log(`Staged workbook: ${wb.name}`);
160
+ workbooksFetched++;
161
+ }
162
+ // Evict only workbooks that are archived or deleted — not those outside the updatedSince window.
163
+ for (const [workbookId] of existingByWorkbookId) {
164
+ if (nonArchivedWorkbookIds.has(workbookId))
165
+ continue;
166
+ try {
167
+ await rm(join(stagedDir, STAGED_FILES.workbooksDir, `${workbookId}.json`));
168
+ logger.log(`Removed stale staged file for workbook ${workbookId}.`);
169
+ }
170
+ catch {
171
+ // Best-effort removal.
172
+ }
173
+ }
174
+ const projectionConfig = {
175
+ connectionMappings: config.connectionMappings ?? {},
176
+ workbookFilter: config.workbookFilter ?? { includeArchived: false, includeExplorations: false },
177
+ };
178
+ await writeFile(join(stagedDir, STAGED_FILES.projectionConfig), JSON.stringify(projectionConfig, null, 2), 'utf-8');
179
+ const manifest = {
180
+ sigmaConnectionId: config.sigmaConnectionId,
181
+ fetchedAt: new Date().toISOString(),
182
+ dataModelCount: active.length,
183
+ workbookCount: activeWorkbooks.length,
184
+ };
185
+ await writeFile(join(stagedDir, STAGED_FILES.manifest), JSON.stringify(manifest, null, 2), 'utf-8');
186
+ logger.log(`Sigma fetch complete. Data models: ${fetched} fetched, ${skipped} unchanged. Workbooks: ${workbooksFetched} fetched, ${workbooksSkipped} unchanged.`);
187
+ }
188
+ finally {
189
+ await client.cleanup();
190
+ }
191
+ }
@@ -0,0 +1,17 @@
1
+ import type { KtxProjectConnectionConfig } from '../../../../context/project/config.js';
2
+ import type { KtxLocalProject } from '../../../../context/project/project.js';
3
+ import { type SigmaClientConfig } from './client.js';
4
+ import type { SigmaFetchLogger } from './fetch.js';
5
+ import { SigmaSourceAdapter } from './sigma.adapter.js';
6
+ export declare function sigmaRuntimeConfigFromLocalConnection(connectionId: string, connection: KtxProjectConnectionConfig | undefined, env?: NodeJS.ProcessEnv): {
7
+ apiUrl: string;
8
+ clientId: string;
9
+ clientSecret: string;
10
+ };
11
+ interface CreateLocalSigmaSourceAdapterOptions {
12
+ env?: NodeJS.ProcessEnv;
13
+ defaultClientConfig?: SigmaClientConfig;
14
+ logger?: SigmaFetchLogger;
15
+ }
16
+ export declare function createLocalSigmaSourceAdapter(project: KtxLocalProject, options?: CreateLocalSigmaSourceAdapterOptions): SigmaSourceAdapter;
17
+ export {};
@@ -0,0 +1,41 @@
1
+ import { resolveKtxConfigReference } from '../../../core/config-reference.js';
2
+ import { DEFAULT_SIGMA_CLIENT_CONFIG, DefaultSigmaClient } from './client.js';
3
+ import { SigmaSourceAdapter } from './sigma.adapter.js';
4
+ function stringField(value) {
5
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
6
+ }
7
+ export function sigmaRuntimeConfigFromLocalConnection(connectionId, connection, env = process.env) {
8
+ if (!connection || String(connection.driver).toLowerCase() !== 'sigma') {
9
+ throw new Error(`Connection "${connectionId}" is not a Sigma connection`);
10
+ }
11
+ const apiUrl = stringField(connection.api_url) ?? 'https://api.sigmacomputing.com';
12
+ const clientId = stringField(connection.client_id);
13
+ const literalSecret = stringField(connection.client_secret);
14
+ const secretRef = stringField(connection.client_secret_ref);
15
+ const clientSecret = literalSecret ?? (secretRef ? (resolveKtxConfigReference(secretRef, env) ?? null) : null);
16
+ if (!clientId) {
17
+ throw new Error(`Connection "${connectionId}" is missing Sigma client_id`);
18
+ }
19
+ if (!clientSecret) {
20
+ throw new Error(`Connection "${connectionId}" is missing Sigma client_secret or client_secret_ref`);
21
+ }
22
+ return { apiUrl, clientId, clientSecret };
23
+ }
24
+ class LocalSigmaClientFactory {
25
+ project;
26
+ options;
27
+ constructor(project, options) {
28
+ this.project = project;
29
+ this.options = options;
30
+ }
31
+ createClient(config, _ctx) {
32
+ const runtimeConfig = sigmaRuntimeConfigFromLocalConnection(config.sigmaConnectionId, this.project.config.connections[config.sigmaConnectionId], this.options.env);
33
+ return new DefaultSigmaClient(runtimeConfig, this.options.defaultClientConfig ?? DEFAULT_SIGMA_CLIENT_CONFIG);
34
+ }
35
+ }
36
+ export function createLocalSigmaSourceAdapter(project, options = {}) {
37
+ return new SigmaSourceAdapter({
38
+ clientFactory: new LocalSigmaClientFactory(project, options),
39
+ ...(options.logger ? { logger: options.logger } : {}),
40
+ });
41
+ }
@@ -0,0 +1,8 @@
1
+ import type { SemanticLayerService } from '../../../../context/sl/semantic-layer.service.js';
2
+ import type { DeterministicProjectionContext, ProjectionResult } from '../../types.js';
3
+ type SlService = Pick<SemanticLayerService, 'writeSource'> & {
4
+ forWorktree(workdir: string): Pick<SemanticLayerService, 'writeSource'>;
5
+ };
6
+ /** @internal */
7
+ export declare function projectSigmaDataModels(ctx: DeterministicProjectionContext, slService: SlService): Promise<ProjectionResult>;
8
+ export {};
@@ -0,0 +1,186 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { sigmaProjectionConfigSchema, stagedDataModelFileSchema, STAGED_FILES } from './types.js';
5
+ async function readProjectionConfig(stagedDir) {
6
+ try {
7
+ const body = await readFile(join(stagedDir, STAGED_FILES.projectionConfig), 'utf-8');
8
+ return sigmaProjectionConfigSchema.parse(JSON.parse(body)).connectionMappings;
9
+ }
10
+ catch {
11
+ return {};
12
+ }
13
+ }
14
+ const SIGMA_AUTHOR = { name: 'Sigma', email: 'system@kaelio.dev' };
15
+ // Best-effort schema for the raw spec blob stored in staged data model files.
16
+ const warehouseTableSourceSchema = z.object({
17
+ kind: z.literal('warehouse-table'),
18
+ connectionId: z.string(),
19
+ path: z.array(z.string()),
20
+ });
21
+ const specColumnSchema = z
22
+ .object({
23
+ id: z.string(),
24
+ formula: z.string().optional(),
25
+ name: z.string().optional(),
26
+ hidden: z.boolean().optional(),
27
+ description: z.string().optional(),
28
+ format: z.object({ kind: z.string() }).passthrough().optional(),
29
+ })
30
+ .passthrough();
31
+ const specElementSchema = z
32
+ .object({
33
+ id: z.string(),
34
+ kind: z.string().optional(),
35
+ name: z.string().optional(),
36
+ hidden: z.boolean().optional(),
37
+ source: z.object({ kind: z.string() }).passthrough().optional(),
38
+ columns: z.array(specColumnSchema).optional(),
39
+ })
40
+ .passthrough();
41
+ const specPageSchema = z
42
+ .object({
43
+ id: z.string(),
44
+ name: z.string().optional(),
45
+ elements: z.array(specElementSchema).optional(),
46
+ })
47
+ .passthrough();
48
+ const sigmaSpecSchema = z
49
+ .object({
50
+ name: z.string().optional(),
51
+ pages: z.array(specPageSchema).optional(),
52
+ })
53
+ .passthrough();
54
+ /** Extract the column name from a bracket formula like `[TABLE/Column Name]` or `[Column]`. */
55
+ function extractColumnName(formula) {
56
+ const match = /\[(?:[^\]/]+\/)?([^\]]+)\]/.exec(formula.trim());
57
+ return match?.[1] ?? null;
58
+ }
59
+ function slugify(value) {
60
+ return value
61
+ .toLowerCase()
62
+ .replace(/[^a-z0-9]+/g, '_')
63
+ .replace(/^_+|_+$/g, '');
64
+ }
65
+ function inferColumnType(col) {
66
+ const kind = col.format?.kind;
67
+ if (kind === 'datetime' || kind === 'date')
68
+ return 'time';
69
+ if (kind === 'number' || kind === 'currency' || kind === 'percent')
70
+ return 'number';
71
+ return 'string';
72
+ }
73
+ function buildSourceFromElement(dataModelName, elementName, elementId, warehousePath, columns) {
74
+ const table = warehousePath.join('.');
75
+ if (!table)
76
+ return null;
77
+ const modelSlug = slugify(dataModelName || elementId);
78
+ const elemSlug = elementName ? slugify(elementName) : '';
79
+ const sourceName = elemSlug && elemSlug !== modelSlug ? `${modelSlug}_${elemSlug}` : modelSlug;
80
+ if (!sourceName)
81
+ return null;
82
+ const slColumns = [];
83
+ for (const col of columns) {
84
+ if (col.hidden)
85
+ continue;
86
+ if (!col.formula)
87
+ continue;
88
+ // Aggregation formulas (Sum, Count, etc.) are Sigma-specific expressions that don't map to
89
+ // warehouse columns — skip them silently. The sigma_ingest skill surfaces them as wiki candidates.
90
+ if (/^[A-Za-z]+\(/.test(col.formula.trim()))
91
+ continue;
92
+ const displayName = col.name ?? extractColumnName(col.formula);
93
+ if (!displayName)
94
+ continue;
95
+ const colSlug = slugify(displayName);
96
+ if (!colSlug)
97
+ continue;
98
+ slColumns.push({
99
+ name: colSlug,
100
+ type: inferColumnType(col),
101
+ ...(col.description ? { descriptions: { user: col.description } } : {}),
102
+ });
103
+ }
104
+ const source = {
105
+ name: sourceName,
106
+ table,
107
+ grain: [],
108
+ columns: slColumns,
109
+ joins: [],
110
+ measures: [],
111
+ };
112
+ if (dataModelName) {
113
+ source.descriptions = { user: dataModelName };
114
+ }
115
+ return source;
116
+ }
117
+ /** @internal */
118
+ export async function projectSigmaDataModels(ctx, slService) {
119
+ const svc = ctx.workdir ? slService.forWorktree(ctx.workdir) : slService;
120
+ const warnings = [];
121
+ const errors = [];
122
+ const touchedSources = [];
123
+ const connectionMappings = await readProjectionConfig(ctx.stagedDir);
124
+ const dmDir = join(ctx.stagedDir, STAGED_FILES.dataModelsDir);
125
+ let entries;
126
+ try {
127
+ entries = await readdir(dmDir);
128
+ }
129
+ catch {
130
+ return { warnings, errors, touchedSources, changedWikiPageKeys: [] };
131
+ }
132
+ for (const entry of entries) {
133
+ if (!entry.endsWith('.json'))
134
+ continue;
135
+ let stagedFile;
136
+ try {
137
+ const body = await readFile(join(dmDir, entry), 'utf-8');
138
+ stagedFile = stagedDataModelFileSchema.parse(JSON.parse(body));
139
+ }
140
+ catch {
141
+ warnings.push(`Skipping malformed staged file: ${entry}`);
142
+ continue;
143
+ }
144
+ if (!stagedFile.spec)
145
+ continue;
146
+ let spec;
147
+ try {
148
+ spec = sigmaSpecSchema.parse(stagedFile.spec);
149
+ }
150
+ catch {
151
+ warnings.push(`Skipping unparseable spec for data model "${stagedFile.name}"`);
152
+ continue;
153
+ }
154
+ for (const page of spec.pages ?? []) {
155
+ for (const element of page.elements ?? []) {
156
+ if (element.hidden)
157
+ continue;
158
+ const warehouseSource = warehouseTableSourceSchema.safeParse(element.source);
159
+ if (!warehouseSource.success)
160
+ continue;
161
+ const source = buildSourceFromElement(stagedFile.name, element.name, element.id, warehouseSource.data.path, element.columns ?? []);
162
+ if (!source)
163
+ continue;
164
+ // Only write SL sources for elements whose Sigma connection is mapped to a warehouse connection.
165
+ // Writing under an unmapped connection produces gate failures because the Sigma connection
166
+ // is not a warehouse connection and cannot be validated.
167
+ const targetConnectionId = connectionMappings[warehouseSource.data.connectionId];
168
+ if (!targetConnectionId) {
169
+ warnings.push(`Skipping SL source for "${stagedFile.name}" / "${element.name ?? element.id}": ` +
170
+ `no connectionMappings entry for Sigma connection ${warehouseSource.data.connectionId}. ` +
171
+ `Add a connectionMappings entry in ktx.yaml to enable SL projection for this element.`);
172
+ continue;
173
+ }
174
+ try {
175
+ const result = await svc.writeSource(targetConnectionId, source, SIGMA_AUTHOR.name, SIGMA_AUTHOR.email, `Sigma: import data model "${stagedFile.name}"`);
176
+ touchedSources.push({ connectionId: targetConnectionId, sourceName: source.name });
177
+ warnings.push(...result.warnings);
178
+ }
179
+ catch (err) {
180
+ errors.push(`Failed to write source "${source.name}": ${err instanceof Error ? err.message : String(err)}`);
181
+ }
182
+ }
183
+ }
184
+ }
185
+ return { warnings, errors, touchedSources, changedWikiPageKeys: [] };
186
+ }
@@ -0,0 +1,18 @@
1
+ import type { ChunkResult, DeterministicProjectionContext, DiffSet, FetchContext, ProjectionResult, SourceAdapter } from '../../types.js';
2
+ import type { SigmaClientFactory } from './client-port.js';
3
+ import { type SigmaFetchLogger } from './fetch.js';
4
+ export interface SigmaSourceAdapterDeps {
5
+ clientFactory: SigmaClientFactory;
6
+ logger?: SigmaFetchLogger;
7
+ }
8
+ export declare class SigmaSourceAdapter implements SourceAdapter {
9
+ private readonly deps;
10
+ readonly source = "sigma";
11
+ readonly skillNames: string[];
12
+ constructor(deps: SigmaSourceAdapterDeps);
13
+ detect(stagedDir: string): Promise<boolean>;
14
+ fetch(pullConfig: unknown, stagedDir: string, ctx: FetchContext): Promise<void>;
15
+ chunk(stagedDir: string, diffSet?: DiffSet): Promise<ChunkResult>;
16
+ listTargetConnectionIds(stagedDir: string): Promise<string[]>;
17
+ project(ctx: DeterministicProjectionContext): Promise<ProjectionResult>;
18
+ }
@@ -0,0 +1,43 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { chunkSigmaStagedDir } from './chunk.js';
4
+ import { detectSigmaStagedDir } from './detect.js';
5
+ import { fetchSigmaBundle } from './fetch.js';
6
+ import { projectSigmaDataModels } from './project.js';
7
+ import { sigmaProjectionConfigSchema, STAGED_FILES } from './types.js';
8
+ export class SigmaSourceAdapter {
9
+ deps;
10
+ source = 'sigma';
11
+ skillNames = ['sigma_ingest'];
12
+ constructor(deps) {
13
+ this.deps = deps;
14
+ }
15
+ detect(stagedDir) {
16
+ return detectSigmaStagedDir(stagedDir);
17
+ }
18
+ async fetch(pullConfig, stagedDir, ctx) {
19
+ await fetchSigmaBundle({
20
+ pullConfig,
21
+ stagedDir,
22
+ ctx,
23
+ clientFactory: this.deps.clientFactory,
24
+ ...(this.deps.logger ? { logger: this.deps.logger } : {}),
25
+ });
26
+ }
27
+ chunk(stagedDir, diffSet) {
28
+ return chunkSigmaStagedDir(stagedDir, { diffSet });
29
+ }
30
+ async listTargetConnectionIds(stagedDir) {
31
+ try {
32
+ const body = await readFile(join(stagedDir, STAGED_FILES.projectionConfig), 'utf-8');
33
+ const config = sigmaProjectionConfigSchema.parse(JSON.parse(body));
34
+ return [...new Set(Object.values(config.connectionMappings))].sort();
35
+ }
36
+ catch {
37
+ return [];
38
+ }
39
+ }
40
+ project(ctx) {
41
+ return projectSigmaDataModels(ctx, ctx.semanticLayerService);
42
+ }
43
+ }
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ /** Filters applied when listing workbooks. Shared with ListWorkbooksOptions in client-port.ts. */
3
+ declare const workbookFilterSchema: z.ZodObject<{
4
+ includeArchived: z.ZodDefault<z.ZodBoolean>;
5
+ includeExplorations: z.ZodDefault<z.ZodBoolean>;
6
+ updatedSince: z.ZodOptional<z.ZodString>;
7
+ }, z.core.$strip>;
8
+ /** Input shape for listWorkbooks — all fields optional since the client applies its own defaults. */
9
+ export type WorkbookFilterInput = z.input<typeof workbookFilterSchema>;
10
+ /** The lean config the adapter needs at `fetch()` time, stored in the ingest job's `bundleRef.config`. */
11
+ declare const sigmaPullConfigSchema: z.ZodObject<{
12
+ sigmaConnectionId: z.ZodString;
13
+ connectionMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
14
+ workbookFilter: z.ZodDefault<z.ZodObject<{
15
+ includeArchived: z.ZodDefault<z.ZodBoolean>;
16
+ includeExplorations: z.ZodDefault<z.ZodBoolean>;
17
+ updatedSince: z.ZodOptional<z.ZodString>;
18
+ }, z.core.$strip>>;
19
+ dataModelFilter: z.ZodOptional<z.ZodObject<{
20
+ updatedSince: z.ZodOptional<z.ZodString>;
21
+ }, z.core.$strip>>;
22
+ }, z.core.$strip>;
23
+ export type SigmaPullConfig = z.infer<typeof sigmaPullConfigSchema>;
24
+ export declare function parseSigmaPullConfig(raw: unknown): SigmaPullConfig;
25
+ /** Written to stagedDir during fetch() and read back by project(), listTargetConnectionIds(), and the sigma_ingest skill. */
26
+ export declare const sigmaProjectionConfigSchema: z.ZodObject<{
27
+ connectionMappings: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
28
+ workbookFilter: z.ZodDefault<z.ZodObject<{
29
+ includeArchived: z.ZodDefault<z.ZodBoolean>;
30
+ includeExplorations: z.ZodDefault<z.ZodBoolean>;
31
+ updatedSince: z.ZodOptional<z.ZodString>;
32
+ }, z.core.$strip>>;
33
+ }, z.core.$strip>;
34
+ export type SigmaProjectionConfig = z.infer<typeof sigmaProjectionConfigSchema>;
35
+ /**
36
+ * A staged data model file, one per `data-models/<id>.json`.
37
+ * Stores the summary metadata plus the raw spec blob from GET /v2/dataModels/{id}/spec.
38
+ */
39
+ export declare const stagedDataModelFileSchema: z.ZodObject<{
40
+ sigmaId: z.ZodString;
41
+ name: z.ZodString;
42
+ path: z.ZodString;
43
+ latestVersion: z.ZodNumber;
44
+ updatedAt: z.ZodString;
45
+ isArchived: z.ZodDefault<z.ZodBoolean>;
46
+ dataModelUrlId: z.ZodOptional<z.ZodString>;
47
+ spec: z.ZodUnknown;
48
+ }, z.core.$strip>;
49
+ export type StagedDataModelFile = z.infer<typeof stagedDataModelFileSchema>;
50
+ /** The manifest written once per `fetch()`. Presence acts as the detect() sentinel. */
51
+ export declare const sigmaManifestSchema: z.ZodObject<{
52
+ sigmaConnectionId: z.ZodString;
53
+ fetchedAt: z.ZodString;
54
+ dataModelCount: z.ZodNumber;
55
+ workbookCount: z.ZodDefault<z.ZodNumber>;
56
+ }, z.core.$strip>;
57
+ export type SigmaManifest = z.infer<typeof sigmaManifestSchema>;
58
+ /**
59
+ * A staged workbook file, one per `workbooks/<id>.json`.
60
+ * Stores the summary metadata from GET /v2/workbooks (no separate spec endpoint).
61
+ */
62
+ export declare const stagedWorkbookFileSchema: z.ZodObject<{
63
+ sigmaId: z.ZodString;
64
+ name: z.ZodString;
65
+ path: z.ZodString;
66
+ latestVersion: z.ZodNumber;
67
+ updatedAt: z.ZodString;
68
+ isArchived: z.ZodDefault<z.ZodBoolean>;
69
+ workbookUrlId: z.ZodOptional<z.ZodString>;
70
+ description: z.ZodOptional<z.ZodString>;
71
+ }, z.core.$strip>;
72
+ export type StagedWorkbookFile = z.infer<typeof stagedWorkbookFileSchema>;
73
+ /** Filenames inside stagedDir. Centralized so chunk() + fetch() + detect() all agree. */
74
+ export declare const STAGED_FILES: {
75
+ readonly manifest: "sigma-manifest.json";
76
+ readonly projectionConfig: "sigma-projection-config.json";
77
+ readonly dataModelsDir: "data-models";
78
+ readonly workbooksDir: "workbooks";
79
+ };
80
+ export {};
@@ -0,0 +1,82 @@
1
+ import { z } from 'zod';
2
+ const sigmaLocalConnectionIdSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/);
3
+ /** Filters applied when listing workbooks. Shared with ListWorkbooksOptions in client-port.ts. */
4
+ const workbookFilterSchema = z.object({
5
+ includeArchived: z.boolean().default(false),
6
+ includeExplorations: z.boolean().default(false),
7
+ /** ISO 8601 date string. Only workbooks updated on or after this date are included. */
8
+ updatedSince: z.string().optional(),
9
+ });
10
+ const dataModelFilterSchema = z.object({
11
+ /** ISO 8601 date string. Only data models updated on or after this date are fetched. */
12
+ updatedSince: z.string().optional(),
13
+ });
14
+ /** The lean config the adapter needs at `fetch()` time, stored in the ingest job's `bundleRef.config`. */
15
+ const sigmaPullConfigSchema = z.object({
16
+ /** The ktx connection ID for the Sigma instance being swept. */
17
+ sigmaConnectionId: sigmaLocalConnectionIdSchema,
18
+ /**
19
+ * Maps Sigma internal connection UUIDs (source.connectionId in data model specs)
20
+ * to ktx warehouse connection IDs. When present, projected semantic-layer sources
21
+ * are written under the mapped warehouse connection rather than the Sigma connection.
22
+ */
23
+ connectionMappings: z.record(z.string(), z.string()).optional(),
24
+ /** Filters applied when listing workbooks. Defaults exclude archived and exploration workbooks. */
25
+ workbookFilter: workbookFilterSchema.default({ includeArchived: false, includeExplorations: false }),
26
+ /** Filters applied when listing data models. */
27
+ dataModelFilter: dataModelFilterSchema.optional(),
28
+ });
29
+ export function parseSigmaPullConfig(raw) {
30
+ return sigmaPullConfigSchema.parse(raw);
31
+ }
32
+ /** Written to stagedDir during fetch() and read back by project(), listTargetConnectionIds(), and the sigma_ingest skill. */
33
+ export const sigmaProjectionConfigSchema = z.object({
34
+ connectionMappings: z.record(z.string(), z.string()).default({}),
35
+ /** Filters that were active when workbooks were last fetched. Tells the skill what the staged set covers. */
36
+ workbookFilter: workbookFilterSchema.default({ includeArchived: false, includeExplorations: false }),
37
+ });
38
+ /**
39
+ * A staged data model file, one per `data-models/<id>.json`.
40
+ * Stores the summary metadata plus the raw spec blob from GET /v2/dataModels/{id}/spec.
41
+ */
42
+ export const stagedDataModelFileSchema = z.object({
43
+ sigmaId: z.string(),
44
+ name: z.string(),
45
+ /** Full path in Sigma, e.g. "Finance/Revenue Model". */
46
+ path: z.string(),
47
+ latestVersion: z.number(),
48
+ updatedAt: z.string(),
49
+ isArchived: z.boolean().default(false),
50
+ /** URL-safe slug Sigma uses in the web UI (dataModelUrlId from the API). */
51
+ dataModelUrlId: z.string().optional(),
52
+ /** Raw spec from GET /v2/dataModels/{id}/spec (JSON format). */
53
+ spec: z.unknown(),
54
+ });
55
+ /** The manifest written once per `fetch()`. Presence acts as the detect() sentinel. */
56
+ export const sigmaManifestSchema = z.object({
57
+ sigmaConnectionId: sigmaLocalConnectionIdSchema,
58
+ fetchedAt: z.string(),
59
+ dataModelCount: z.number().int(),
60
+ workbookCount: z.number().int().default(0),
61
+ });
62
+ /**
63
+ * A staged workbook file, one per `workbooks/<id>.json`.
64
+ * Stores the summary metadata from GET /v2/workbooks (no separate spec endpoint).
65
+ */
66
+ export const stagedWorkbookFileSchema = z.object({
67
+ sigmaId: z.string(),
68
+ name: z.string(),
69
+ path: z.string(),
70
+ latestVersion: z.number(),
71
+ updatedAt: z.string(),
72
+ isArchived: z.boolean().default(false),
73
+ workbookUrlId: z.string().optional(),
74
+ description: z.string().optional(),
75
+ });
76
+ /** Filenames inside stagedDir. Centralized so chunk() + fetch() + detect() all agree. */
77
+ export const STAGED_FILES = {
78
+ manifest: 'sigma-manifest.json',
79
+ projectionConfig: 'sigma-projection-config.json',
80
+ dataModelsDir: 'data-models',
81
+ workbooksDir: 'workbooks',
82
+ };