@yejiming/dsh-data-agent 0.0.13 → 0.1.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 (38) hide show
  1. package/README.en.md +43 -8
  2. package/README.md +43 -8
  3. package/conformance/dsh-ecosystem/inventory.json +26 -3
  4. package/conformance/dsh-ecosystem/restrictions.json +2 -2
  5. package/cordis.patch.yml +6 -3
  6. package/dsh-plugin.json +9 -4
  7. package/lib/catalog-DEJqOXRo.js +1944 -0
  8. package/lib/catalog-identity-CVftmvQL.js +96 -0
  9. package/lib/client.js +2217 -109
  10. package/lib/client.js.map +1 -1
  11. package/lib/command-CzzSPmag.js +1719 -0
  12. package/lib/command.js +2 -2
  13. package/lib/{connections-CHY4uB6z.js → connections-CFXOZTHZ.js} +223 -9
  14. package/lib/index.js +366 -16
  15. package/lib/routes.js +257 -4
  16. package/lib/{tool-ZTOS4B33.js → tool-DNkywSph.js} +364 -3
  17. package/lib/tool.js +1 -1
  18. package/lib/types/catalog-adapters.d.ts +52 -0
  19. package/lib/types/catalog-ai.d.ts +49 -0
  20. package/lib/types/catalog-command.d.ts +28 -0
  21. package/lib/types/catalog-identity.d.ts +23 -0
  22. package/lib/types/catalog-storage.d.ts +265 -0
  23. package/lib/types/catalog-tools.d.ts +5 -0
  24. package/lib/types/catalog-tui.d.ts +18 -0
  25. package/lib/types/catalog-types.d.ts +1376 -0
  26. package/lib/types/catalog.d.ts +59 -0
  27. package/lib/types/client/CatalogPanel.d.ts +15 -0
  28. package/lib/types/client/catalog-client.d.ts +57 -0
  29. package/lib/types/client/locales.d.ts +242 -0
  30. package/lib/types/command.d.ts +14 -3
  31. package/lib/types/connections.d.ts +9 -0
  32. package/lib/types/defaults.d.ts +22 -0
  33. package/lib/types/index.d.ts +42 -5
  34. package/lib/types/tui-connection-form.d.ts +11 -5
  35. package/package.json +4 -2
  36. package/preset/data-agent/agent.cordis.yml +9 -1
  37. package/lib/command-utC5MHd9.js +0 -916
  38. package/lib/defaults-Cngd8Tf8.js +0 -131
@@ -0,0 +1,1944 @@
1
+ import { b as DATABASE_TYPES, r as redactSecretText } from "./connections-CFXOZTHZ.js";
2
+ import { a as catalogSemanticRevisionId, c as normalizeCatalogIdentifier, i as catalogSemanticId, l as normalizeCatalogText, n as catalogAssetId, o as catalogSourceId, r as catalogRevisionId, s as catalogTechnicalFingerprint, t as canonicalCatalogIdentity, u as stableJson } from "./catalog-identity-CVftmvQL.js";
3
+ import { createHash } from "node:crypto";
4
+ import { basename } from "node:path";
5
+ import { z } from "zod";
6
+ //#region src/catalog-types.ts
7
+ /**
8
+ * Surface-neutral Catalog contracts. This module contains no Node or browser
9
+ * runtime dependencies beyond Zod and can therefore be imported type-only by
10
+ * the Web bundle.
11
+ * @module @yejiming/dsh-data-agent/catalog-types
12
+ */
13
+ const catalogDateTimeSchema = z.iso.datetime();
14
+ const catalogRunStatusSchema = z.enum([
15
+ "queued",
16
+ "running",
17
+ "applying",
18
+ "succeeded",
19
+ "failed",
20
+ "cancelled",
21
+ "interrupted"
22
+ ]);
23
+ const CATALOG_ASSET_STATUSES = [
24
+ "observed",
25
+ "missing",
26
+ "unavailable"
27
+ ];
28
+ const catalogAssetStatusSchema = z.enum(CATALOG_ASSET_STATUSES);
29
+ const CATALOG_ASSET_KINDS = [
30
+ "schema",
31
+ "table",
32
+ "view",
33
+ "column",
34
+ "primary_key",
35
+ "foreign_key",
36
+ "index"
37
+ ];
38
+ const catalogAssetKindSchema = z.enum(CATALOG_ASSET_KINDS);
39
+ const catalogEnrichmentStatusSchema = z.enum([
40
+ "queued",
41
+ "running",
42
+ "succeeded",
43
+ "partial",
44
+ "failed",
45
+ "cancelled"
46
+ ]);
47
+ const CATALOG_SEMANTIC_KINDS = [
48
+ "meaning",
49
+ "term",
50
+ "metric"
51
+ ];
52
+ const catalogSemanticKindSchema = z.enum(CATALOG_SEMANTIC_KINDS);
53
+ const CATALOG_SEMANTIC_STATUSES = [
54
+ "inferred",
55
+ "verified",
56
+ "needs_review",
57
+ "retired"
58
+ ];
59
+ const catalogSemanticStatusSchema = z.enum(CATALOG_SEMANTIC_STATUSES);
60
+ const catalogDiffKindSchema = z.enum([
61
+ "added",
62
+ "changed",
63
+ "missing",
64
+ "restored",
65
+ "unavailable"
66
+ ]);
67
+ const catalogScopeSchema = z.discriminatedUnion("kind", [
68
+ z.strictObject({ kind: z.literal("source") }),
69
+ z.strictObject({
70
+ kind: z.literal("schema"),
71
+ schema: z.string().min(1).max(256)
72
+ }),
73
+ z.strictObject({
74
+ kind: z.literal("table"),
75
+ schema: z.string().min(1).max(256),
76
+ table: z.string().min(1).max(256)
77
+ })
78
+ ]);
79
+ const catalogSourceSchema = z.strictObject({
80
+ id: z.string().min(1).max(256),
81
+ profileId: z.string().min(1).max(256),
82
+ type: z.enum(DATABASE_TYPES),
83
+ name: z.string().min(1).max(256),
84
+ host: z.string().max(512).optional(),
85
+ database: z.string().min(1).max(512),
86
+ credentialConfigured: z.boolean(),
87
+ createdAt: catalogDateTimeSchema,
88
+ updatedAt: catalogDateTimeSchema,
89
+ lastFullScanAt: catalogDateTimeSchema.optional(),
90
+ lastPartialScanAt: catalogDateTimeSchema.optional()
91
+ });
92
+ const catalogProgressSchema = z.strictObject({
93
+ schemas: z.number().int().nonnegative(),
94
+ relations: z.number().int().nonnegative(),
95
+ fields: z.number().int().nonnegative(),
96
+ assets: z.number().int().nonnegative()
97
+ });
98
+ const catalogEnrichmentSchema = z.strictObject({
99
+ status: catalogEnrichmentStatusSchema,
100
+ provider: z.string().min(1).max(256),
101
+ model: z.string().min(1).max(512),
102
+ reasoningEffort: z.string().min(1).max(64).optional(),
103
+ tablesTotal: z.number().int().nonnegative(),
104
+ tablesCompleted: z.number().int().nonnegative(),
105
+ tablesFailed: z.number().int().nonnegative(),
106
+ candidatesGenerated: z.number().int().nonnegative(),
107
+ startedAt: catalogDateTimeSchema.optional(),
108
+ completedAt: catalogDateTimeSchema.optional(),
109
+ error: z.string().max(4096).optional()
110
+ });
111
+ const catalogRunSchema = z.strictObject({
112
+ id: z.string().min(1).max(256),
113
+ sourceId: z.string().min(1).max(256),
114
+ sessionId: z.string().min(1).max(256),
115
+ scope: catalogScopeSchema,
116
+ status: catalogRunStatusSchema,
117
+ coverageComplete: z.boolean(),
118
+ progress: catalogProgressSchema,
119
+ createdAt: catalogDateTimeSchema,
120
+ startedAt: catalogDateTimeSchema.optional(),
121
+ completedAt: catalogDateTimeSchema.optional(),
122
+ error: z.string().max(4096).optional(),
123
+ enrichment: catalogEnrichmentSchema.optional()
124
+ });
125
+ const startCatalogScanInputSchema = z.strictObject({
126
+ sessionId: z.string().min(1).max(256),
127
+ scope: catalogScopeSchema
128
+ });
129
+ z.strictObject({
130
+ source: catalogSourceSchema,
131
+ activeRun: catalogRunSchema.optional(),
132
+ latestRun: catalogRunSchema.optional(),
133
+ latestSuccessfulRun: catalogRunSchema.optional(),
134
+ counts: z.strictObject({
135
+ assets: z.number().int().nonnegative(),
136
+ fields: z.number().int().nonnegative(),
137
+ needsReview: z.number().int().nonnegative()
138
+ })
139
+ });
140
+ const catalogIdentitySchema = z.strictObject({
141
+ sourceId: z.string().min(1).max(256),
142
+ database: z.string().min(1).max(512),
143
+ schema: z.string().min(1).max(256),
144
+ kind: catalogAssetKindSchema,
145
+ relation: z.string().max(256).optional(),
146
+ name: z.string().min(1).max(256)
147
+ });
148
+ const catalogCapabilitySchema = z.enum([
149
+ "supported",
150
+ "unsupported",
151
+ "unavailable"
152
+ ]);
153
+ const catalogTechnicalPayloadSchema = z.strictObject({
154
+ identity: catalogIdentitySchema,
155
+ name: z.string().min(1).max(256),
156
+ path: z.string().min(1).max(1024),
157
+ parentId: z.string().max(256).optional(),
158
+ objectType: z.enum(["table", "view"]).optional(),
159
+ dataType: z.string().max(512).optional(),
160
+ nullable: z.boolean().optional(),
161
+ ordinal: z.number().int().positive().optional(),
162
+ comment: z.string().max(4096).optional(),
163
+ referencedAssetIds: z.array(z.string().min(1).max(256)).max(512).optional(),
164
+ attributes: z.record(z.string(), z.union([
165
+ z.string().max(4096),
166
+ z.number(),
167
+ z.boolean(),
168
+ z.null()
169
+ ])).optional(),
170
+ capabilities: z.record(z.string(), catalogCapabilitySchema).optional(),
171
+ truncatedFields: z.array(z.string().max(128)).max(64).optional(),
172
+ provenance: z.strictObject({
173
+ source: z.literal("database"),
174
+ dialect: z.enum(DATABASE_TYPES),
175
+ runId: z.string().min(1).max(256)
176
+ })
177
+ });
178
+ const catalogObservationSchema = z.strictObject({
179
+ runId: z.string().min(1).max(256),
180
+ sourceId: z.string().min(1).max(256),
181
+ assetId: z.string().min(1).max(256),
182
+ status: catalogAssetStatusSchema,
183
+ fingerprint: z.string().regex(/^[a-f0-9]{64}$/),
184
+ observedAt: catalogDateTimeSchema,
185
+ payload: catalogTechnicalPayloadSchema
186
+ });
187
+ const catalogAssetRevisionSchema = z.strictObject({
188
+ id: z.string().min(1).max(512),
189
+ assetId: z.string().min(1).max(256),
190
+ sourceId: z.string().min(1).max(256),
191
+ runId: z.string().min(1).max(256),
192
+ revision: z.number().int().positive(),
193
+ status: catalogAssetStatusSchema,
194
+ fingerprint: z.string().regex(/^[a-f0-9]{64}$/),
195
+ observedAt: catalogDateTimeSchema,
196
+ previousRevisionId: z.string().max(512).optional(),
197
+ changeSummary: z.array(z.string().max(256)).max(64),
198
+ payload: catalogTechnicalPayloadSchema
199
+ });
200
+ const catalogAssetHeadSchema = z.strictObject({
201
+ assetId: z.string().min(1).max(256),
202
+ sourceId: z.string().min(1).max(256),
203
+ revisionIds: z.array(z.string().min(1).max(512)).max(1e4),
204
+ firstSeenAt: catalogDateTimeSchema,
205
+ lastSeenAt: catalogDateTimeSchema
206
+ });
207
+ const catalogRelationSchema = z.strictObject({
208
+ id: z.string().min(1).max(256),
209
+ sourceId: z.string().min(1).max(256),
210
+ runId: z.string().min(1).max(256),
211
+ kind: z.enum([
212
+ "parent",
213
+ "primary_key",
214
+ "foreign_key",
215
+ "index"
216
+ ]),
217
+ fromAssetId: z.string().min(1).max(256),
218
+ toAssetId: z.string().min(1).max(256).optional(),
219
+ name: z.string().max(256).optional(),
220
+ columnAssetIds: z.array(z.string().min(1).max(256)).max(256),
221
+ referencedColumnAssetIds: z.array(z.string().min(1).max(256)).max(256).optional(),
222
+ observedAt: catalogDateTimeSchema
223
+ });
224
+ const semanticBaseShape = {
225
+ name: z.string().min(1).max(256),
226
+ aliases: z.array(z.string().min(1).max(256)).max(64),
227
+ description: z.string().max(4096),
228
+ owner: z.string().max(256).optional(),
229
+ sourceAssetIds: z.array(z.string().min(1).max(256)).max(256),
230
+ status: catalogSemanticStatusSchema,
231
+ validFrom: catalogDateTimeSchema.optional(),
232
+ validTo: catalogDateTimeSchema.optional(),
233
+ revisionNote: z.string().max(4096).optional(),
234
+ verifiedAt: catalogDateTimeSchema.optional(),
235
+ needsReviewReason: z.string().max(4096).optional(),
236
+ triggerRunId: z.string().max(256).optional()
237
+ };
238
+ const termDefinitionSchema = z.strictObject({
239
+ kind: z.literal("term"),
240
+ ...semanticBaseShape
241
+ }).superRefine((definition, issue) => {
242
+ if (definition.validFrom !== void 0 && definition.validTo !== void 0 && definition.validFrom >= definition.validTo) issue.addIssue({
243
+ code: "custom",
244
+ path: ["validTo"],
245
+ message: "validTo must be later than validFrom"
246
+ });
247
+ });
248
+ const metricDefinitionSchema = z.strictObject({
249
+ kind: z.literal("metric"),
250
+ ...semanticBaseShape,
251
+ formula: z.string().min(1).max(8192),
252
+ grain: z.string().min(1).max(512),
253
+ timeFieldAssetId: z.string().min(1).max(256).optional(),
254
+ filters: z.array(z.string().max(2048)).max(64),
255
+ exclusions: z.array(z.string().max(2048)).max(64)
256
+ }).superRefine((definition, issue) => {
257
+ if (definition.validFrom !== void 0 && definition.validTo !== void 0 && definition.validFrom >= definition.validTo) issue.addIssue({
258
+ code: "custom",
259
+ path: ["validTo"],
260
+ message: "validTo must be later than validFrom"
261
+ });
262
+ });
263
+ const meaningDefinitionSchema = z.strictObject({
264
+ kind: z.literal("meaning"),
265
+ ...semanticBaseShape,
266
+ targetAssetId: z.string().min(1).max(256),
267
+ targetKind: z.enum([
268
+ "table",
269
+ "view",
270
+ "column"
271
+ ]),
272
+ generatedBy: z.strictObject({
273
+ kind: z.literal("ai"),
274
+ provider: z.string().min(1).max(256),
275
+ model: z.string().min(1).max(512),
276
+ runId: z.string().min(1).max(256)
277
+ })
278
+ });
279
+ const semanticDefinitionSchema = z.discriminatedUnion("kind", [
280
+ meaningDefinitionSchema,
281
+ termDefinitionSchema,
282
+ metricDefinitionSchema
283
+ ]);
284
+ const catalogSemanticEntrySchema = z.strictObject({
285
+ id: z.string().min(1).max(256),
286
+ sourceId: z.string().min(1).max(256),
287
+ kind: catalogSemanticKindSchema,
288
+ currentVersion: z.number().int().positive(),
289
+ createdAt: catalogDateTimeSchema,
290
+ updatedAt: catalogDateTimeSchema
291
+ });
292
+ const catalogSemanticRevisionSchema = z.strictObject({
293
+ id: z.string().min(1).max(512),
294
+ semanticId: z.string().min(1).max(256),
295
+ sourceId: z.string().min(1).max(256),
296
+ version: z.number().int().positive(),
297
+ createdAt: catalogDateTimeSchema,
298
+ definition: semanticDefinitionSchema
299
+ });
300
+ const catalogSearchFiltersSchema = z.strictObject({
301
+ sourceId: z.string().min(1).max(256).optional(),
302
+ schema: z.string().min(1).max(256).optional(),
303
+ assetKinds: z.array(catalogAssetKindSchema).max(CATALOG_ASSET_KINDS.length).optional(),
304
+ semanticKinds: z.array(catalogSemanticKindSchema).max(CATALOG_SEMANTIC_KINDS.length).optional(),
305
+ assetStatuses: z.array(catalogAssetStatusSchema).max(CATALOG_ASSET_STATUSES.length).optional(),
306
+ semanticStatuses: z.array(catalogSemanticStatusSchema).max(CATALOG_SEMANTIC_STATUSES.length).optional(),
307
+ includeInferred: z.boolean().default(false)
308
+ });
309
+ const catalogSearchRequestSchema = z.strictObject({
310
+ query: z.string().trim().min(1).max(512),
311
+ filters: catalogSearchFiltersSchema.default({ includeInferred: false }),
312
+ cursor: z.string().max(512).optional(),
313
+ pageSize: z.number().int().min(1).max(200).default(50)
314
+ });
315
+ const catalogSearchItemSchema = z.strictObject({
316
+ id: z.string().min(1).max(256),
317
+ sourceId: z.string().min(1).max(256),
318
+ resultType: z.enum(["asset", "semantic"]),
319
+ kind: z.string().min(1).max(64),
320
+ name: z.string().min(1).max(256),
321
+ path: z.string().max(1024),
322
+ summary: z.string().max(1024),
323
+ matchReasons: z.array(z.string().max(128)).max(16),
324
+ status: z.string().min(1).max(64),
325
+ version: z.number().int().positive().optional(),
326
+ provenance: z.enum([
327
+ "database",
328
+ "human",
329
+ "inferred"
330
+ ]),
331
+ untrusted: z.literal(true)
332
+ });
333
+ z.strictObject({
334
+ sourceId: z.string().min(1).max(256),
335
+ query: z.string().max(512),
336
+ items: z.array(catalogSearchItemSchema).max(200),
337
+ nextCursor: z.string().max(512).optional(),
338
+ truncated: z.boolean(),
339
+ warnings: z.array(z.string().max(512)).max(16)
340
+ });
341
+ z.strictObject({
342
+ asset: catalogAssetRevisionSchema,
343
+ fields: z.array(catalogAssetRevisionSchema).max(200),
344
+ relations: z.array(catalogRelationSchema).max(200),
345
+ semantics: z.array(catalogSemanticRevisionSchema).max(200),
346
+ history: z.array(catalogAssetRevisionSchema).max(200),
347
+ nextCursor: z.string().max(512).optional(),
348
+ truncated: z.boolean(),
349
+ untrusted: z.literal(true)
350
+ });
351
+ const catalogDiffItemSchema = z.strictObject({
352
+ kind: catalogDiffKindSchema,
353
+ assetId: z.string().min(1).max(256),
354
+ name: z.string().min(1).max(256),
355
+ path: z.string().min(1).max(1024),
356
+ fromRevisionId: z.string().max(512).optional(),
357
+ toRevisionId: z.string().max(512).optional(),
358
+ summary: z.array(z.string().max(256)).max(64)
359
+ });
360
+ z.strictObject({
361
+ sourceId: z.string().min(1).max(256),
362
+ fromRunId: z.string().min(1).max(256),
363
+ toRunId: z.string().min(1).max(256),
364
+ scope: catalogScopeSchema,
365
+ items: z.array(catalogDiffItemSchema).max(200),
366
+ nextCursor: z.string().max(512).optional(),
367
+ truncated: z.boolean()
368
+ });
369
+ //#endregion
370
+ //#region src/catalog-adapters.ts
371
+ const ACCESS_DENIED = /access denied|permission denied|not authorized|insufficient privilege|ora-01031|authorizationexception/i;
372
+ /** Registry contains an explicit adapter entry for every supported dialect. */
373
+ function createCatalogAdapterRegistry() {
374
+ return {
375
+ mysql: richAdapter("mysql"),
376
+ doris: richAdapter("doris"),
377
+ postgres: richAdapter("postgres"),
378
+ sqlserver: richAdapter("sqlserver"),
379
+ sqlite: richAdapter("sqlite"),
380
+ oracle: richAdapter("oracle"),
381
+ clickhouse: richAdapter("clickhouse"),
382
+ hive: describeAdapter("hive"),
383
+ impala: describeAdapter("impala")
384
+ };
385
+ }
386
+ function richAdapter(type) {
387
+ return {
388
+ type,
389
+ capabilities: {
390
+ schemas: "supported",
391
+ tables: "supported",
392
+ views: "supported",
393
+ columns: "supported",
394
+ comments: type === "sqlite" ? "unsupported" : "supported",
395
+ ...{
396
+ mysql: {
397
+ primaryKeys: "supported",
398
+ foreignKeys: "supported",
399
+ indexes: "supported"
400
+ },
401
+ doris: {
402
+ primaryKeys: "unsupported",
403
+ foreignKeys: "unsupported",
404
+ indexes: "unsupported"
405
+ },
406
+ postgres: {
407
+ primaryKeys: "supported",
408
+ foreignKeys: "supported",
409
+ indexes: "supported"
410
+ },
411
+ sqlserver: {
412
+ primaryKeys: "supported",
413
+ foreignKeys: "supported",
414
+ indexes: "supported"
415
+ },
416
+ sqlite: {
417
+ primaryKeys: "supported",
418
+ foreignKeys: "supported",
419
+ indexes: "supported"
420
+ },
421
+ oracle: {
422
+ primaryKeys: "supported",
423
+ foreignKeys: "supported",
424
+ indexes: "supported"
425
+ },
426
+ clickhouse: {
427
+ primaryKeys: "supported",
428
+ foreignKeys: "unsupported",
429
+ indexes: "supported"
430
+ }
431
+ }[type]
432
+ },
433
+ async scan(context) {
434
+ const scanned = await mapLimit(await scopedSchemas(context), context.options.schemaConcurrency, async (schema) => {
435
+ context.signal.throwIfAborted();
436
+ context.onProgress?.("schema");
437
+ try {
438
+ const sql = buildCatalogMetadataSql(type, context.connection.database, schema, tableName(context.scope));
439
+ const result = await context.connections.queryMetadata(context.sessionId, sql, context.signal);
440
+ if (result.truncated) throw new Error("Catalog metadata output exceeded catalogMaxResultChars; narrow the scan scope or increase the Catalog metadata limit");
441
+ const built = observationsFromRows(context, schema, parseCatalogMetadataRows(type, result.stdout));
442
+ return {
443
+ observations: [observationForSchema(context, schema, "observed"), ...built.observations],
444
+ relations: built.relations,
445
+ unavailableScope: void 0
446
+ };
447
+ } catch (error) {
448
+ if (!ACCESS_DENIED.test(error instanceof Error ? error.message : String(error))) throw error;
449
+ return {
450
+ observations: [observationForSchema(context, schema, "unavailable")],
451
+ unavailableScope: schema
452
+ };
453
+ }
454
+ }, context.signal);
455
+ const observations = scanned.flatMap((value) => value.observations);
456
+ const relations = scanned.flatMap((value) => value.relations ?? []);
457
+ const unavailableScopes = scanned.flatMap((value) => value.unavailableScope === void 0 ? [] : [value.unavailableScope]);
458
+ return {
459
+ observations: dedupeObservations(observations),
460
+ relations: dedupeRelations(relations),
461
+ coverageComplete: unavailableScopes.length === 0,
462
+ unavailableScopes
463
+ };
464
+ }
465
+ };
466
+ }
467
+ function describeAdapter(type) {
468
+ return {
469
+ type,
470
+ capabilities: {
471
+ schemas: "supported",
472
+ tables: "supported",
473
+ views: "unavailable",
474
+ columns: "supported",
475
+ comments: "supported",
476
+ primaryKeys: "unsupported",
477
+ foreignKeys: "unsupported",
478
+ indexes: "unsupported"
479
+ },
480
+ async scan(context) {
481
+ const scanned = await mapLimit(await scopedSchemas(context), context.options.schemaConcurrency, async (schema) => {
482
+ context.signal.throwIfAborted();
483
+ context.onProgress?.("schema");
484
+ let relations;
485
+ try {
486
+ relations = context.scope.kind === "table" ? [context.scope.table] : await context.connections.listTables(context.sessionId, schema, context.signal);
487
+ } catch (error) {
488
+ if (!ACCESS_DENIED.test(error instanceof Error ? error.message : String(error))) throw error;
489
+ return {
490
+ observations: [observationForSchema(context, schema, "unavailable")],
491
+ unavailableScopes: [schema]
492
+ };
493
+ }
494
+ const relationDetails = await mapLimit(relations, context.options.assetConcurrency, async (relation) => {
495
+ context.signal.throwIfAborted();
496
+ context.onProgress?.("relation");
497
+ try {
498
+ const columns = await context.connections.describe(context.sessionId, schema, relation, context.signal);
499
+ return {
500
+ observations: [observationForRelation(context, schema, relation, "table", "", "observed"), ...columns.map((column, index) => {
501
+ const observation = observationForColumn(context, schema, relation, "table", column.name, column.type, column.nullable, "", index + 1);
502
+ context.onProgress?.("field");
503
+ return observation;
504
+ })],
505
+ unavailableScope: void 0
506
+ };
507
+ } catch (error) {
508
+ if (!ACCESS_DENIED.test(error instanceof Error ? error.message : String(error))) throw error;
509
+ return {
510
+ observations: [observationForRelation(context, schema, relation, "table", "", "unavailable")],
511
+ unavailableScope: `${schema}.${relation}`
512
+ };
513
+ }
514
+ }, context.signal);
515
+ const detailUnavailable = relationDetails.flatMap((value) => value.unavailableScope === void 0 ? [] : [value.unavailableScope]);
516
+ return {
517
+ observations: [observationForSchema(context, schema, "observed"), ...relationDetails.flatMap((value) => value.observations)],
518
+ unavailableScopes: detailUnavailable
519
+ };
520
+ }, context.signal);
521
+ const observations = scanned.flatMap((value) => value.observations);
522
+ const unavailableScopes = scanned.flatMap((value) => value.unavailableScopes);
523
+ return {
524
+ observations: dedupeObservations(observations),
525
+ relations: [],
526
+ coverageComplete: unavailableScopes.length === 0,
527
+ unavailableScopes
528
+ };
529
+ }
530
+ };
531
+ }
532
+ async function scopedSchemas(context) {
533
+ if (context.scope.kind !== "source") return [context.scope.schema];
534
+ return context.connections.listSchemas(context.sessionId, context.signal);
535
+ }
536
+ function tableName(scope) {
537
+ return scope.kind === "table" ? scope.table : void 0;
538
+ }
539
+ function catalogDatabase(context) {
540
+ if (context.connection.type !== "sqlite") return context.connection.database;
541
+ return context.connection.database.split(/[\\/]/).filter(Boolean).at(-1) ?? context.connection.database;
542
+ }
543
+ /** Pure SQL constructor used by fixture tests; values are SQL literals, never identifiers. */
544
+ function buildCatalogMetadataSql(type, database, schema, table) {
545
+ const schemaValue = sqlLiteral(schema);
546
+ const tableFilter = (column) => table === void 0 ? "" : ` AND ${column}=${sqlLiteral(table)}`;
547
+ switch (type) {
548
+ case "mysql":
549
+ case "doris": return [
550
+ "SELECT 'relation' AS row_kind, TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, COALESCE(TABLE_COMMENT,''), '', '', '', '', '0'",
551
+ `FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=${schemaValue}${tableFilter("TABLE_NAME")}`,
552
+ "UNION ALL",
553
+ "SELECT 'column', TABLE_SCHEMA, TABLE_NAME, '', '', COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COALESCE(COLUMN_COMMENT,''), CAST(ORDINAL_POSITION AS CHAR)",
554
+ `FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=${schemaValue}${tableFilter("TABLE_NAME")}`,
555
+ ...type === "mysql" ? [
556
+ "UNION ALL",
557
+ "SELECT CASE tc.CONSTRAINT_TYPE WHEN 'PRIMARY KEY' THEN 'primary_key' ELSE 'foreign_key' END, k.TABLE_SCHEMA, k.TABLE_NAME, k.CONSTRAINT_NAME, COALESCE(k.REFERENCED_TABLE_SCHEMA,''), k.COLUMN_NAME, COALESCE(k.REFERENCED_TABLE_NAME,''), COALESCE(k.REFERENCED_COLUMN_NAME,''), '', CAST(k.ORDINAL_POSITION AS CHAR)",
558
+ "FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k ON k.CONSTRAINT_SCHEMA=tc.CONSTRAINT_SCHEMA AND k.TABLE_NAME=tc.TABLE_NAME AND k.CONSTRAINT_NAME=tc.CONSTRAINT_NAME",
559
+ `WHERE k.TABLE_SCHEMA=${schemaValue} AND tc.CONSTRAINT_TYPE IN ('PRIMARY KEY','FOREIGN KEY')${tableFilter("k.TABLE_NAME")}`,
560
+ "UNION ALL",
561
+ "SELECT 'index', TABLE_SCHEMA, TABLE_NAME, INDEX_NAME, '', COALESCE(COLUMN_NAME,''), '', '', CASE NON_UNIQUE WHEN 0 THEN 'unique' ELSE '' END, CAST(SEQ_IN_INDEX AS CHAR)",
562
+ `FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=${schemaValue} AND INDEX_NAME <> 'PRIMARY'${tableFilter("TABLE_NAME")}`
563
+ ] : [],
564
+ "ORDER BY 2,3,1,10;"
565
+ ].join(" ");
566
+ case "postgres": return [
567
+ "SELECT 'relation', n.nspname, c.relname, CASE c.relkind WHEN 'v' THEN 'VIEW' WHEN 'm' THEN 'VIEW' ELSE 'BASE TABLE' END, COALESCE(obj_description(c.oid),'') , '', '', '', '', '0'",
568
+ "FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace",
569
+ `WHERE n.nspname=${schemaValue} AND c.relkind IN ('r','p','v','m')${tableFilter("c.relname")}`,
570
+ "UNION ALL",
571
+ "SELECT 'column', n.nspname, c.relname, '', '', a.attname, pg_catalog.format_type(a.atttypid,a.atttypmod), CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END, COALESCE(col_description(c.oid,a.attnum),''), a.attnum::text",
572
+ "FROM pg_catalog.pg_attribute a JOIN pg_catalog.pg_class c ON c.oid=a.attrelid JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace",
573
+ `WHERE n.nspname=${schemaValue} AND c.relkind IN ('r','p','v','m') AND a.attnum>0 AND NOT a.attisdropped${tableFilter("c.relname")}`,
574
+ "UNION ALL",
575
+ "SELECT CASE con.contype WHEN 'p' THEN 'primary_key' ELSE 'foreign_key' END, n.nspname, c.relname, con.conname, COALESCE(rn.nspname,''), a.attname, COALESCE(rc.relname,''), COALESCE(ra.attname,''), '', ord.n::text",
576
+ "FROM pg_catalog.pg_constraint con JOIN pg_catalog.pg_class c ON c.oid=con.conrelid JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace JOIN LATERAL unnest(con.conkey) WITH ORDINALITY ord(attnum,n) ON true JOIN pg_catalog.pg_attribute a ON a.attrelid=c.oid AND a.attnum=ord.attnum LEFT JOIN pg_catalog.pg_class rc ON rc.oid=con.confrelid LEFT JOIN pg_catalog.pg_namespace rn ON rn.oid=rc.relnamespace LEFT JOIN pg_catalog.pg_attribute ra ON ra.attrelid=rc.oid AND ra.attnum=con.confkey[ord.n]",
577
+ `WHERE n.nspname=${schemaValue} AND con.contype IN ('p','f')${tableFilter("c.relname")}`,
578
+ "UNION ALL",
579
+ "SELECT 'index', n.nspname, c.relname, i.relname, '', a.attname, '', '', CASE ix.indisunique WHEN true THEN 'unique' ELSE '' END, ord.n::text",
580
+ "FROM pg_catalog.pg_index ix JOIN pg_catalog.pg_class c ON c.oid=ix.indrelid JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace JOIN pg_catalog.pg_class i ON i.oid=ix.indexrelid JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY ord(attnum,n) ON true LEFT JOIN pg_catalog.pg_attribute a ON a.attrelid=c.oid AND a.attnum=ord.attnum",
581
+ `WHERE n.nspname=${schemaValue} AND NOT ix.indisprimary${tableFilter("c.relname")}`,
582
+ "ORDER BY 2,3,1,10;"
583
+ ].join(" ");
584
+ case "sqlserver": return [
585
+ "SELECT 'relation', s.name, o.name, CASE WHEN o.type='V' THEN 'VIEW' ELSE 'BASE TABLE' END, COALESCE(CAST(ep.value AS nvarchar(4000)),''), '', '', '', '', '0'",
586
+ "FROM sys.objects o JOIN sys.schemas s ON s.schema_id=o.schema_id LEFT JOIN sys.extended_properties ep ON ep.major_id=o.object_id AND ep.minor_id=0 AND ep.name='MS_Description'",
587
+ `WHERE s.name=${schemaValue} AND o.type IN ('U','V')${tableFilter("o.name")}`,
588
+ "UNION ALL",
589
+ "SELECT 'column', s.name, o.name, '', '', c.name, ty.name, CASE WHEN c.is_nullable=1 THEN 'YES' ELSE 'NO' END, COALESCE(CAST(ep.value AS nvarchar(4000)),''), CAST(c.column_id AS nvarchar(20))",
590
+ "FROM sys.columns c JOIN sys.objects o ON o.object_id=c.object_id JOIN sys.schemas s ON s.schema_id=o.schema_id JOIN sys.types ty ON ty.user_type_id=c.user_type_id LEFT JOIN sys.extended_properties ep ON ep.major_id=o.object_id AND ep.minor_id=c.column_id AND ep.name='MS_Description'",
591
+ `WHERE s.name=${schemaValue} AND o.type IN ('U','V')${tableFilter("o.name")}`,
592
+ "UNION ALL",
593
+ "SELECT 'primary_key', s.name, o.name, kc.name, '', c.name, '', '', '', CAST(ic.key_ordinal AS nvarchar(20))",
594
+ "FROM sys.key_constraints kc JOIN sys.objects o ON o.object_id=kc.parent_object_id JOIN sys.schemas s ON s.schema_id=o.schema_id JOIN sys.index_columns ic ON ic.object_id=o.object_id AND ic.index_id=kc.unique_index_id JOIN sys.columns c ON c.object_id=o.object_id AND c.column_id=ic.column_id",
595
+ `WHERE s.name=${schemaValue} AND kc.type='PK'${tableFilter("o.name")}`,
596
+ "UNION ALL",
597
+ "SELECT 'foreign_key', s.name, o.name, fk.name, rs.name, c.name, ro.name, rc.name, '', CAST(fkc.constraint_column_id AS nvarchar(20))",
598
+ "FROM sys.foreign_key_columns fkc JOIN sys.foreign_keys fk ON fk.object_id=fkc.constraint_object_id JOIN sys.objects o ON o.object_id=fkc.parent_object_id JOIN sys.schemas s ON s.schema_id=o.schema_id JOIN sys.columns c ON c.object_id=o.object_id AND c.column_id=fkc.parent_column_id JOIN sys.objects ro ON ro.object_id=fkc.referenced_object_id JOIN sys.schemas rs ON rs.schema_id=ro.schema_id JOIN sys.columns rc ON rc.object_id=fkc.referenced_object_id AND rc.column_id=fkc.referenced_column_id",
599
+ `WHERE s.name=${schemaValue}${tableFilter("o.name")}`,
600
+ "UNION ALL",
601
+ "SELECT 'index', s.name, o.name, i.name, '', c.name, '', '', CASE WHEN i.is_unique=1 THEN 'unique' ELSE '' END, CAST(ic.key_ordinal AS nvarchar(20))",
602
+ "FROM sys.indexes i JOIN sys.objects o ON o.object_id=i.object_id JOIN sys.schemas s ON s.schema_id=o.schema_id JOIN sys.index_columns ic ON ic.object_id=i.object_id AND ic.index_id=i.index_id JOIN sys.columns c ON c.object_id=o.object_id AND c.column_id=ic.column_id",
603
+ `WHERE s.name=${schemaValue} AND o.type='U' AND i.is_primary_key=0 AND i.is_hypothetical=0${tableFilter("o.name")}`,
604
+ "ORDER BY 2,3,1,10;"
605
+ ].join(" ");
606
+ case "sqlite": return [
607
+ "SELECT 'relation', 'main', m.name, CASE m.type WHEN 'view' THEN 'VIEW' ELSE 'BASE TABLE' END, '', '', '', '', '', '0'",
608
+ `FROM sqlite_master m WHERE m.type IN ('table','view') AND m.name NOT LIKE 'sqlite_%'${tableFilter("m.name")}`,
609
+ "UNION ALL",
610
+ "SELECT 'column', 'main', m.name, '', '', p.name, p.type, CASE p.[notnull] WHEN 1 THEN 'NO' ELSE 'YES' END, '', CAST(p.cid + 1 AS TEXT)",
611
+ `FROM sqlite_master m JOIN pragma_table_xinfo(m.name) p WHERE m.type IN ('table','view') AND m.name NOT LIKE 'sqlite_%'${tableFilter("m.name")}`,
612
+ "UNION ALL",
613
+ "SELECT 'primary_key', 'main', m.name, 'PRIMARY', '', p.name, '', '', '', CAST(p.pk AS TEXT)",
614
+ `FROM sqlite_master m JOIN pragma_table_xinfo(m.name) p WHERE m.type='table' AND m.name NOT LIKE 'sqlite_%' AND p.pk>0${tableFilter("m.name")}`,
615
+ "UNION ALL",
616
+ "SELECT 'foreign_key', 'main', m.name, 'fk_' || f.id, 'main', f.[from], f.[table], f.[to], '', CAST(f.seq + 1 AS TEXT)",
617
+ `FROM sqlite_master m JOIN pragma_foreign_key_list(m.name) f WHERE m.type='table' AND m.name NOT LIKE 'sqlite_%'${tableFilter("m.name")}`,
618
+ "UNION ALL",
619
+ "SELECT 'index', 'main', m.name, il.name, '', ii.name, '', '', CASE il.[unique] WHEN 1 THEN 'unique' ELSE '' END, CAST(ii.seqno + 1 AS TEXT)",
620
+ `FROM sqlite_master m JOIN pragma_index_list(m.name) il JOIN pragma_index_info(il.name) ii WHERE m.type='table' AND m.name NOT LIKE 'sqlite_%' AND il.origin <> 'pk'${tableFilter("m.name")}`,
621
+ "ORDER BY 2,3,1,10;"
622
+ ].join(" ");
623
+ case "oracle": {
624
+ const owner = sqlLiteral(schema.toUpperCase());
625
+ return [
626
+ "SELECT 'relation', o.owner, o.object_name, CASE o.object_type WHEN 'VIEW' THEN 'VIEW' ELSE 'BASE TABLE' END, NVL(tc.comments,''), '', '', '', '', '0'",
627
+ "FROM all_objects o LEFT JOIN all_tab_comments tc ON tc.owner=o.owner AND tc.table_name=o.object_name",
628
+ `WHERE o.owner=${owner} AND o.object_type IN ('TABLE','VIEW')${tableFilter("o.object_name")}`,
629
+ "UNION ALL",
630
+ "SELECT 'column', c.owner, c.table_name, '', '', c.column_name, c.data_type, c.nullable, NVL(cc.comments,''), TO_CHAR(c.column_id)",
631
+ "FROM all_tab_columns c LEFT JOIN all_col_comments cc ON cc.owner=c.owner AND cc.table_name=c.table_name AND cc.column_name=c.column_name",
632
+ `WHERE c.owner=${owner}${tableFilter("c.table_name")}`,
633
+ "UNION ALL",
634
+ "SELECT CASE ac.constraint_type WHEN 'P' THEN 'primary_key' ELSE 'foreign_key' END, ac.owner, ac.table_name, ac.constraint_name, NVL(rac.owner,''), acc.column_name, NVL(rac.table_name,''), NVL(racc.column_name,''), '', TO_CHAR(acc.position)",
635
+ "FROM all_constraints ac JOIN all_cons_columns acc ON acc.owner=ac.owner AND acc.constraint_name=ac.constraint_name LEFT JOIN all_constraints rac ON rac.owner=ac.r_owner AND rac.constraint_name=ac.r_constraint_name LEFT JOIN all_cons_columns racc ON racc.owner=rac.owner AND racc.constraint_name=rac.constraint_name AND racc.position=acc.position",
636
+ `WHERE ac.owner=${owner} AND ac.constraint_type IN ('P','R')${tableFilter("ac.table_name")}`,
637
+ "UNION ALL",
638
+ "SELECT 'index', i.table_owner, i.table_name, i.index_name, '', ic.column_name, '', '', CASE i.uniqueness WHEN 'UNIQUE' THEN 'unique' ELSE '' END, TO_CHAR(ic.column_position)",
639
+ "FROM all_indexes i JOIN all_ind_columns ic ON ic.index_owner=i.owner AND ic.index_name=i.index_name",
640
+ `WHERE i.table_owner=${owner} AND NOT EXISTS (SELECT 1 FROM all_constraints c WHERE c.owner=i.table_owner AND c.table_name=i.table_name AND c.index_name=i.index_name AND c.constraint_type='P')${tableFilter("i.table_name")}`,
641
+ "ORDER BY 2,3,1,10;"
642
+ ].join(" ");
643
+ }
644
+ case "clickhouse": return [
645
+ "SELECT 'relation', database, name, CASE WHEN engine='View' OR engine='MaterializedView' THEN 'VIEW' ELSE 'BASE TABLE' END, comment, '', '', '', '', '0'",
646
+ `FROM system.tables WHERE database=${schemaValue}${tableFilter("name")}`,
647
+ "UNION ALL",
648
+ "SELECT 'column', database, table, '', '', name, type, if(startsWith(type,'Nullable('),'YES','NO'), comment, toString(position)",
649
+ `FROM system.columns WHERE database=${schemaValue}${tableFilter("table")}`,
650
+ "UNION ALL",
651
+ "SELECT 'primary_key', database, name, concat('PRIMARY ',substring(primary_key,1,200)), '', '', '', '', '', '0'",
652
+ `FROM system.tables WHERE database=${schemaValue} AND primary_key != ''${tableFilter("name")}`,
653
+ "UNION ALL",
654
+ "SELECT 'index', database, name, concat('ORDER BY ',substring(sorting_key,1,200)), '', '', '', '', '', '0'",
655
+ `FROM system.tables WHERE database=${schemaValue} AND sorting_key != ''${tableFilter("name")}`,
656
+ "ORDER BY 2,3,1,10;"
657
+ ].join(" ");
658
+ }
659
+ }
660
+ function parseCatalogMetadataRows(type, stdout) {
661
+ const delimiter = type === "sqlserver" ? "" : type === "oracle" || type === "postgres" || type === "sqlite" ? "|" : " ";
662
+ const lines = stdout.replace(/\r\n?/g, "\n").split("\n").filter((line) => line.trim().length > 0);
663
+ const start = type === "mysql" || type === "doris" ? 1 : 0;
664
+ const rows = [];
665
+ for (const line of lines.slice(start)) {
666
+ const fields = line.split(delimiter).map((value) => value.trim());
667
+ if (fields.length < 10 || ![
668
+ "relation",
669
+ "column",
670
+ "primary_key",
671
+ "foreign_key",
672
+ "index"
673
+ ].includes(fields[0])) continue;
674
+ rows.push({
675
+ rowKind: fields[0],
676
+ schema: fields[1] ?? "",
677
+ relation: fields[2] ?? "",
678
+ relationType: fields[3] ?? "",
679
+ relationComment: fields[4] ?? "",
680
+ column: fields[5] ?? "",
681
+ dataType: fields[6] ?? "",
682
+ nullable: fields[7] ?? "",
683
+ columnComment: fields[8] ?? "",
684
+ ordinal: fields[9] ?? ""
685
+ });
686
+ }
687
+ return rows;
688
+ }
689
+ function observationsFromRows(context, fallbackSchema, rows) {
690
+ const observations = [];
691
+ const relationTypes = /* @__PURE__ */ new Map();
692
+ for (const row of rows) {
693
+ if (row.rowKind !== "relation" || row.relation.length === 0) continue;
694
+ const schema = row.schema || fallbackSchema;
695
+ relationTypes.set(`${schema}\0${row.relation}`, /view/i.test(row.relationType) ? "view" : "table");
696
+ }
697
+ for (const row of rows) {
698
+ const schema = row.schema || fallbackSchema;
699
+ if (row.relation.length === 0) continue;
700
+ if (row.rowKind === "relation") {
701
+ const objectType = relationTypes.get(`${schema}\0${row.relation}`) ?? "table";
702
+ observations.push(observationForRelation(context, schema, row.relation, objectType, row.relationComment, "observed"));
703
+ context.onProgress?.("relation");
704
+ continue;
705
+ }
706
+ if (row.rowKind !== "column" || row.column.length === 0) continue;
707
+ observations.push(observationForColumn(context, schema, row.relation, relationTypes.get(`${schema}\0${row.relation}`) ?? "table", row.column, row.dataType, parseNullable(row.nullable), row.columnComment, Number.parseInt(row.ordinal, 10) || void 0));
708
+ context.onProgress?.("field");
709
+ }
710
+ const grouped = /* @__PURE__ */ new Map();
711
+ for (const row of rows) {
712
+ if (row.rowKind === "relation" || row.rowKind === "column") continue;
713
+ const schema = row.schema || fallbackSchema;
714
+ if (row.relation.length === 0 || row.relationType.length === 0) continue;
715
+ const key = [
716
+ row.rowKind,
717
+ schema,
718
+ row.relation,
719
+ row.relationType,
720
+ row.relationComment,
721
+ row.dataType
722
+ ].join("\0");
723
+ const values = grouped.get(key) ?? [];
724
+ values.push(row);
725
+ grouped.set(key, values);
726
+ }
727
+ return {
728
+ observations,
729
+ relations: [...grouped.values()].map((group) => relationFromRows(context, fallbackSchema, group))
730
+ };
731
+ }
732
+ function relationFromRows(context, fallbackSchema, rows) {
733
+ const first = rows[0];
734
+ if (first.rowKind === "relation" || first.rowKind === "column") throw new Error("Catalog relation grouping received a non-relation metadata row");
735
+ const schema = first.schema || fallbackSchema;
736
+ const fromIdentity = {
737
+ sourceId: context.sourceId,
738
+ database: catalogDatabase(context),
739
+ schema,
740
+ kind: "table",
741
+ name: first.relation
742
+ };
743
+ const fromAssetId = catalogAssetId(context.connection.type, fromIdentity);
744
+ const sorted = [...rows].sort((a, b) => (Number.parseInt(a.ordinal, 10) || 0) - (Number.parseInt(b.ordinal, 10) || 0));
745
+ const columnAssetIds = sorted.flatMap((row) => row.column.length === 0 ? [] : [catalogAssetId(context.connection.type, {
746
+ ...fromIdentity,
747
+ relation: first.relation,
748
+ kind: "column",
749
+ name: row.column
750
+ })]);
751
+ const referencedSchema = first.relationComment || schema;
752
+ const referencedRelation = first.dataType;
753
+ const toAssetId = first.rowKind === "foreign_key" && referencedRelation.length > 0 ? catalogAssetId(context.connection.type, {
754
+ sourceId: context.sourceId,
755
+ database: catalogDatabase(context),
756
+ schema: referencedSchema,
757
+ kind: "table",
758
+ name: referencedRelation
759
+ }) : void 0;
760
+ const referencedColumnAssetIds = first.rowKind === "foreign_key" && referencedRelation.length > 0 ? sorted.flatMap((row) => row.nullable.length === 0 ? [] : [catalogAssetId(context.connection.type, {
761
+ sourceId: context.sourceId,
762
+ database: catalogDatabase(context),
763
+ schema: referencedSchema,
764
+ relation: referencedRelation,
765
+ kind: "column",
766
+ name: row.nullable
767
+ })]) : void 0;
768
+ const name = normalizeCatalogText(first.relationType, 256).value;
769
+ return {
770
+ id: `relation_${createHash("sha256").update(stableJson({
771
+ sourceId: context.sourceId,
772
+ kind: first.rowKind,
773
+ fromAssetId,
774
+ toAssetId,
775
+ name
776
+ })).digest("hex").slice(0, 32)}`,
777
+ sourceId: context.sourceId,
778
+ runId: context.runId,
779
+ kind: first.rowKind,
780
+ fromAssetId,
781
+ ...toAssetId !== void 0 ? { toAssetId } : {},
782
+ name,
783
+ columnAssetIds,
784
+ ...referencedColumnAssetIds !== void 0 ? { referencedColumnAssetIds } : {},
785
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
786
+ };
787
+ }
788
+ function observationForSchema(context, schema, status) {
789
+ return makeObservation(context, {
790
+ sourceId: context.sourceId,
791
+ database: catalogDatabase(context),
792
+ schema,
793
+ kind: "schema",
794
+ name: schema
795
+ }, {
796
+ name: schema,
797
+ path: `${catalogDatabase(context)}.${schema}`,
798
+ capabilities: contextCapabilities(context)
799
+ }, status);
800
+ }
801
+ function observationForRelation(context, schema, relation, objectType, rawComment, status) {
802
+ const schemaIdentity = {
803
+ sourceId: context.sourceId,
804
+ database: catalogDatabase(context),
805
+ schema,
806
+ kind: "schema",
807
+ name: schema
808
+ };
809
+ const comment = normalizeCatalogText(rawComment, context.options.maxTextChars);
810
+ return makeObservation(context, {
811
+ sourceId: context.sourceId,
812
+ database: catalogDatabase(context),
813
+ schema,
814
+ kind: objectType,
815
+ name: relation
816
+ }, {
817
+ name: relation,
818
+ path: `${catalogDatabase(context)}.${schema}.${relation}`,
819
+ parentId: catalogAssetId(context.connection.type, schemaIdentity),
820
+ objectType,
821
+ ...comment.value.length > 0 ? { comment: comment.value } : {},
822
+ ...comment.truncated ? { truncatedFields: ["comment"] } : {},
823
+ capabilities: contextCapabilities(context)
824
+ }, status);
825
+ }
826
+ function observationForColumn(context, schema, relation, objectType, column, rawType, nullable, rawComment, ordinal) {
827
+ const parentIdentity = {
828
+ sourceId: context.sourceId,
829
+ database: catalogDatabase(context),
830
+ schema,
831
+ kind: objectType,
832
+ name: relation
833
+ };
834
+ const type = normalizeCatalogText(rawType, 512);
835
+ const comment = normalizeCatalogText(rawComment, context.options.maxTextChars);
836
+ return makeObservation(context, {
837
+ sourceId: context.sourceId,
838
+ database: catalogDatabase(context),
839
+ schema,
840
+ relation,
841
+ kind: "column",
842
+ name: column
843
+ }, {
844
+ name: column,
845
+ path: `${catalogDatabase(context)}.${schema}.${relation}.${column}`,
846
+ parentId: catalogAssetId(context.connection.type, parentIdentity),
847
+ ...type.value.length > 0 ? { dataType: type.value } : {},
848
+ ...nullable !== void 0 ? { nullable } : {},
849
+ ...ordinal !== void 0 ? { ordinal } : {},
850
+ ...comment.value.length > 0 ? { comment: comment.value } : {},
851
+ ...type.truncated || comment.truncated ? { truncatedFields: [type.truncated ? "dataType" : "", comment.truncated ? "comment" : ""].filter(Boolean) } : {},
852
+ capabilities: contextCapabilities(context)
853
+ }, "observed");
854
+ }
855
+ function makeObservation(context, rawIdentity, values, status) {
856
+ const identity = canonicalCatalogIdentity(context.connection.type, rawIdentity);
857
+ const payload = {
858
+ ...values,
859
+ identity,
860
+ provenance: {
861
+ source: "database",
862
+ dialect: context.connection.type,
863
+ runId: context.runId
864
+ }
865
+ };
866
+ return {
867
+ runId: context.runId,
868
+ sourceId: context.sourceId,
869
+ assetId: catalogAssetId(context.connection.type, identity),
870
+ status,
871
+ fingerprint: catalogTechnicalFingerprint(payload, status),
872
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
873
+ payload
874
+ };
875
+ }
876
+ function contextCapabilities(context) {
877
+ return { ...createCatalogAdapterRegistry()[context.connection.type].capabilities };
878
+ }
879
+ function parseNullable(value) {
880
+ if (/^(yes|y|true|1)$/i.test(value)) return true;
881
+ if (/^(no|n|false|0)$/i.test(value)) return false;
882
+ }
883
+ function sqlLiteral(value) {
884
+ return `'${normalizeCatalogText(value, 256).value.replace(/'/g, "''")}'`;
885
+ }
886
+ function dedupeObservations(values) {
887
+ return [...new Map(values.map((value) => [value.assetId, value])).values()].sort((a, b) => a.payload.path.localeCompare(b.payload.path) || a.assetId.localeCompare(b.assetId));
888
+ }
889
+ function dedupeRelations(values) {
890
+ return [...new Map(values.map((value) => [value.id, value])).values()].sort((a, b) => a.id.localeCompare(b.id));
891
+ }
892
+ /** Deterministic bounded worker pool that stops scheduling after the first failure. */
893
+ async function mapLimit(values, limit, task, signal) {
894
+ if (!Number.isInteger(limit) || limit < 1) throw new Error("Catalog adapter concurrency must be a positive integer");
895
+ const output = new Array(values.length);
896
+ let cursor = 0;
897
+ let failure;
898
+ const worker = async () => {
899
+ while (failure === void 0) {
900
+ signal.throwIfAborted();
901
+ const index = cursor;
902
+ cursor += 1;
903
+ if (index >= values.length) return;
904
+ try {
905
+ output[index] = await task(values[index], index);
906
+ } catch (error) {
907
+ failure ??= error;
908
+ }
909
+ }
910
+ };
911
+ await Promise.all(Array.from({ length: Math.min(limit, values.length) }, worker));
912
+ if (failure !== void 0) throw failure;
913
+ return output;
914
+ }
915
+ //#endregion
916
+ //#region src/catalog.ts
917
+ /** Shared Catalog service: scan lifecycle, version projections, search, and review. */
918
+ const ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([
919
+ "queued",
920
+ "running",
921
+ "applying"
922
+ ]);
923
+ const ACTIVE_ENRICHMENT_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
924
+ var CatalogVersionConflictError = class extends Error {
925
+ current;
926
+ constructor(current) {
927
+ super(`Catalog semantic version conflict; current version is ${current.version}`);
928
+ this.current = current;
929
+ this.name = "CatalogVersionConflictError";
930
+ }
931
+ };
932
+ async function createCatalogService(connections, persistence, options) {
933
+ const now = () => (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
934
+ const randomId = options.randomId ?? (() => crypto.randomUUID());
935
+ const adapters = options.adapters ?? createCatalogAdapterRegistry();
936
+ const controllers = /* @__PURE__ */ new Map();
937
+ const runtimeRuns = /* @__PURE__ */ new Map();
938
+ const runActive = (run) => ACTIVE_RUN_STATUSES.has(run.status) || run.enrichment !== void 0 && ACTIVE_ENRICHMENT_STATUSES.has(run.enrichment.status);
939
+ const successfulRuns = (sourceId) => persistence.listRuns(sourceId).filter((run) => run.status === "succeeded").sort(compareRun);
940
+ const runVisible = (runId) => persistence.getRun(runId)?.status === "succeeded";
941
+ const currentRevision = (assetId) => {
942
+ const head = persistence.getAssetHead(assetId);
943
+ if (head === void 0) return void 0;
944
+ for (const revisionId of [...head.revisionIds].reverse()) {
945
+ const revision = persistence.getAssetRevision(revisionId);
946
+ if (revision !== void 0 && runVisible(revision.runId)) return revision;
947
+ }
948
+ };
949
+ const revisionAtRun = (assetId, target) => {
950
+ const targetKey = runOrderKey(target);
951
+ return persistence.listAssetRevisions(assetId).filter((revision) => {
952
+ const run = persistence.getRun(revision.runId);
953
+ return run?.status === "succeeded" && runOrderKey(run) <= targetKey;
954
+ }).sort((a, b) => b.revision - a.revision)[0];
955
+ };
956
+ const currentSemantic = (entry) => {
957
+ const revision = persistence.getSemanticRevision(catalogSemanticRevisionId(entry.id, entry.currentVersion));
958
+ if (revision === void 0) throw new Error(`Catalog semantic ${entry.id} has no current revision`);
959
+ return revision;
960
+ };
961
+ const resolvePageSize = (value) => {
962
+ if (value === void 0) return options.pageSize;
963
+ if (!Number.isInteger(value) || value < 1 || value > options.maxPageSize) throw new Error(`pageSize must be an integer between 1 and ${options.maxPageSize}`);
964
+ return value;
965
+ };
966
+ const read = {
967
+ listSources() {
968
+ return persistence.listSources();
969
+ },
970
+ listRuns(sourceId, limit = 50) {
971
+ requireKnownSource(sourceId);
972
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200) throw new Error("limit must be between 1 and 200");
973
+ return persistence.listRuns(sourceId).sort((a, b) => compareRun(b, a)).slice(0, limit).map((run) => runtimeRuns.get(run.id) ?? run);
974
+ },
975
+ async resolveSource(sessionId, requestedSourceId) {
976
+ if (requestedSourceId !== void 0) {
977
+ const requested = persistence.getSource(requestedSourceId);
978
+ if (requested === void 0) throw new Error(`Unknown Catalog source: ${requestedSourceId}`);
979
+ const summary = connections.get(sessionId);
980
+ if (summary?.profileId !== void 0 && summary.profileId !== requestedSourceId) throw new Error("Requested Catalog source does not match the current session connection");
981
+ return requested;
982
+ }
983
+ const summary = connections.get(sessionId);
984
+ if (summary?.profileId !== void 0) {
985
+ const connected = persistence.getSource(summary.profileId);
986
+ if (connected !== void 0) return connected;
987
+ }
988
+ const sources = persistence.listSources();
989
+ if (sources.length === 1) return sources[0];
990
+ if (sources.length === 0) throw new Error("Catalog is empty; run /catalog scan after connecting a saved profile");
991
+ throw new Error(`Catalog source is ambiguous; specify sourceId (${sources.map((source) => `${source.id}:${source.name}`).join(", ")})`);
992
+ },
993
+ status(sourceId) {
994
+ const source = persistence.getSource(sourceId);
995
+ if (source === void 0) return void 0;
996
+ const runs = persistence.listRuns(sourceId).sort((a, b) => compareRun(b, a)).map((run) => runtimeRuns.get(run.id) ?? run);
997
+ const revisions = persistence.listAssetHeads(sourceId).flatMap((head) => {
998
+ const revision = currentRevision(head.assetId);
999
+ return revision === void 0 ? [] : [revision];
1000
+ });
1001
+ const semantics = persistence.listSemanticEntries(sourceId).map(currentSemantic);
1002
+ return {
1003
+ source,
1004
+ ...runs.find(runActive) !== void 0 ? { activeRun: runs.find(runActive) } : {},
1005
+ ...runs[0] !== void 0 ? { latestRun: runs[0] } : {},
1006
+ ...runs.find((run) => run.status === "succeeded") !== void 0 ? { latestSuccessfulRun: runs.find((run) => run.status === "succeeded") } : {},
1007
+ counts: {
1008
+ assets: revisions.filter((revision) => revision.status !== "missing").length,
1009
+ fields: revisions.filter((revision) => revision.payload.identity.kind === "column" && revision.status !== "missing").length,
1010
+ needsReview: semantics.filter((revision) => revision.definition.status === "inferred" || revision.definition.status === "needs_review").length
1011
+ }
1012
+ };
1013
+ },
1014
+ async search(rawRequest) {
1015
+ const request = catalogSearchRequestSchema.parse(rawRequest);
1016
+ const sourceId = request.filters.sourceId;
1017
+ if (sourceId === void 0) throw new Error("Catalog search requires sourceId");
1018
+ requireKnownSource(sourceId);
1019
+ await ensureIndex(sourceId);
1020
+ const normalizedQuery = normalizeCatalogText(request.query, 512).value.toLocaleLowerCase("en-US");
1021
+ const words = normalizedQuery === "*" ? [] : normalizedQuery.split(/\s+/).filter(Boolean);
1022
+ const matches = persistence.listIndex(sourceId).filter((record) => words.every((word) => record.searchText.includes(word))).map((record) => ({
1023
+ ...record.searchItem,
1024
+ matchReasons: searchMatchReasons(record.searchItem, normalizedQuery)
1025
+ })).filter((item) => filterSearchItem(item, request)).sort(compareSearchItems);
1026
+ const cursorKey = stableJson({
1027
+ query: normalizedQuery,
1028
+ filters: request.filters
1029
+ });
1030
+ const cursor = decodeCursor(request.cursor, sourceId, cursorKey);
1031
+ const size = resolvePageSize(request.pageSize);
1032
+ const items = matches.slice(cursor, cursor + size);
1033
+ const nextOffset = cursor + items.length;
1034
+ const includeInferred = request.filters.includeInferred;
1035
+ return {
1036
+ sourceId,
1037
+ query: request.query,
1038
+ items,
1039
+ ...nextOffset < matches.length ? { nextCursor: encodeCursor(nextOffset, sourceId, cursorKey) } : {},
1040
+ truncated: nextOffset < matches.length,
1041
+ warnings: includeInferred && items.some((item) => item.status === "inferred") ? ["Results include inferred definitions that have not been verified by a human."] : []
1042
+ };
1043
+ },
1044
+ getAsset(sourceId, assetId, cursor, pageSize) {
1045
+ requireKnownSource(sourceId);
1046
+ const revision = currentRevision(assetId);
1047
+ if (revision === void 0 || revision.sourceId !== sourceId) throw new Error(`Unknown Catalog asset: ${assetId}`);
1048
+ const size = resolvePageSize(pageSize);
1049
+ const offset = decodeCursor(cursor, sourceId, assetId);
1050
+ const allFields = persistence.listAssetHeads(sourceId).flatMap((head) => {
1051
+ const item = currentRevision(head.assetId);
1052
+ return item !== void 0 && item.payload.parentId === assetId ? [item] : [];
1053
+ }).sort((a, b) => (a.payload.ordinal ?? Number.MAX_SAFE_INTEGER) - (b.payload.ordinal ?? Number.MAX_SAFE_INTEGER) || a.payload.name.localeCompare(b.payload.name));
1054
+ const fields = allFields.slice(offset, offset + size);
1055
+ const allRelations = currentRelations(sourceId).filter((relation) => relation.fromAssetId === assetId || relation.toAssetId === assetId);
1056
+ const relatedAssetIds = /* @__PURE__ */ new Set([assetId, ...allFields.map((field) => field.assetId)]);
1057
+ const allSemantics = persistence.listSemanticEntries(sourceId).map(currentSemantic).filter((item) => item.definition.status !== "retired" && item.definition.sourceAssetIds.some((relatedAssetId) => relatedAssetIds.has(relatedAssetId)));
1058
+ const allHistory = persistence.listAssetRevisions(assetId).filter((item) => runVisible(item.runId)).sort((a, b) => b.revision - a.revision);
1059
+ const relations = allRelations.slice(offset, offset + size);
1060
+ const semantics = allSemantics.slice(offset, offset + size);
1061
+ const history = allHistory.slice(offset, offset + size);
1062
+ const nextOffset = offset + size;
1063
+ const truncated = [
1064
+ allFields,
1065
+ allRelations,
1066
+ allSemantics,
1067
+ allHistory
1068
+ ].some((values) => nextOffset < values.length);
1069
+ return {
1070
+ asset: revision,
1071
+ fields,
1072
+ relations,
1073
+ semantics,
1074
+ history,
1075
+ ...truncated ? { nextCursor: encodeCursor(nextOffset, sourceId, assetId) } : {},
1076
+ truncated,
1077
+ untrusted: true
1078
+ };
1079
+ },
1080
+ getSemantic(sourceId, semanticId, version) {
1081
+ requireKnownSource(sourceId);
1082
+ const entry = persistence.getSemanticEntry(semanticId);
1083
+ if (entry === void 0 || entry.sourceId !== sourceId) throw new Error(`Unknown Catalog semantic: ${semanticId}`);
1084
+ if (version === void 0) return currentSemantic(entry);
1085
+ if (!Number.isInteger(version) || version < 1) throw new Error("version must be a positive integer");
1086
+ const revision = persistence.getSemanticRevision(catalogSemanticRevisionId(semanticId, version));
1087
+ if (revision === void 0) throw new Error(`Unknown Catalog semantic version: ${semanticId}@${version}`);
1088
+ return revision;
1089
+ },
1090
+ getMetric(sourceId, metricId, version) {
1091
+ const revision = read.getSemantic(sourceId, metricId, version);
1092
+ if (revision.definition.kind !== "metric") throw new Error(`${metricId} is not a metric`);
1093
+ return revision;
1094
+ },
1095
+ diff(sourceId, fromRunId, toRunId, cursor, pageSize) {
1096
+ requireKnownSource(sourceId);
1097
+ const runs = successfulRuns(sourceId);
1098
+ let from;
1099
+ let to;
1100
+ if (fromRunId === void 0 && toRunId === void 0) {
1101
+ from = runs.at(-2);
1102
+ to = runs.at(-1);
1103
+ } else {
1104
+ from = fromRunId === void 0 ? void 0 : persistence.getRun(fromRunId);
1105
+ to = toRunId === void 0 ? void 0 : persistence.getRun(toRunId);
1106
+ }
1107
+ if (from?.status !== "succeeded" || to?.status !== "succeeded" || from.sourceId !== sourceId || to.sourceId !== sourceId) throw new Error("Catalog diff requires two successful runs from the same source");
1108
+ const items = buildDiff(sourceId, from, to);
1109
+ const offset = decodeCursor(cursor, sourceId, `${from.id}:${to.id}`);
1110
+ const size = resolvePageSize(pageSize);
1111
+ const page = items.slice(offset, offset + size);
1112
+ const nextOffset = offset + page.length;
1113
+ return {
1114
+ sourceId,
1115
+ fromRunId: from.id,
1116
+ toRunId: to.id,
1117
+ scope: to.scope,
1118
+ items: page,
1119
+ ...nextOffset < items.length ? { nextCursor: encodeCursor(nextOffset, sourceId, `${from.id}:${to.id}`) } : {},
1120
+ truncated: nextOffset < items.length
1121
+ };
1122
+ }
1123
+ };
1124
+ const scanner = {
1125
+ async start(rawInput) {
1126
+ const input = startCatalogScanInputSchema.parse(rawInput);
1127
+ const requestedScope = input.scope;
1128
+ const sessionId = input.sessionId;
1129
+ const modelSelection = options.meaningGenerator?.capture(sessionId);
1130
+ const summary = connections.get(sessionId);
1131
+ if (summary?.profileId === void 0 || summary.profileId.trim().length === 0) throw new Error("Catalog scan requires a connected, stable connection profile");
1132
+ const connection = await connections.resolveForExecution(sessionId);
1133
+ if (connection.profileId !== summary.profileId) throw new Error("Session connection changed while starting Catalog scan");
1134
+ const sourceId = catalogSourceId(summary.profileId);
1135
+ if (sourceId !== summary.profileId) throw new Error("Catalog profileId contains unsupported whitespace or control characters");
1136
+ const scope = normalizeScope(connection.type, requestedScope);
1137
+ const existing = persistence.listRuns(sourceId).find((run) => runActive(runtimeRuns.get(run.id) ?? run));
1138
+ if (existing !== void 0) return runtimeRuns.get(existing.id) ?? existing;
1139
+ const timestamp = now();
1140
+ const source = {
1141
+ id: sourceId,
1142
+ profileId: sourceId,
1143
+ type: connection.type,
1144
+ name: normalizeCatalogText(summary.name ?? summary.database, 256).value,
1145
+ ...summary.host !== void 0 ? { host: normalizeCatalogText(summary.host, 512).value } : {},
1146
+ database: normalizeCatalogText(connection.type === "sqlite" ? basename(summary.database) : summary.database, 512).value,
1147
+ credentialConfigured: true,
1148
+ createdAt: persistence.getSource(sourceId)?.createdAt ?? timestamp,
1149
+ updatedAt: timestamp,
1150
+ ...persistence.getSource(sourceId)?.lastFullScanAt !== void 0 ? { lastFullScanAt: persistence.getSource(sourceId).lastFullScanAt } : {},
1151
+ ...persistence.getSource(sourceId)?.lastPartialScanAt !== void 0 ? { lastPartialScanAt: persistence.getSource(sourceId).lastPartialScanAt } : {}
1152
+ };
1153
+ await persistence.putSource(source);
1154
+ const run = {
1155
+ id: `run_${randomId()}`,
1156
+ sourceId,
1157
+ sessionId,
1158
+ scope,
1159
+ status: "queued",
1160
+ coverageComplete: false,
1161
+ progress: {
1162
+ schemas: 0,
1163
+ relations: 0,
1164
+ fields: 0,
1165
+ assets: 0
1166
+ },
1167
+ createdAt: timestamp,
1168
+ ...modelSelection !== void 0 ? { enrichment: {
1169
+ status: "queued",
1170
+ provider: modelSelection.provider,
1171
+ model: modelSelection.model,
1172
+ ...modelSelection.reasoningEffort !== void 0 ? { reasoningEffort: String(modelSelection.reasoningEffort) } : {},
1173
+ tablesTotal: 0,
1174
+ tablesCompleted: 0,
1175
+ tablesFailed: 0,
1176
+ candidatesGenerated: 0
1177
+ } } : {}
1178
+ };
1179
+ await persistence.putRun(run);
1180
+ runtimeRuns.set(run.id, run);
1181
+ const controller = new AbortController();
1182
+ controllers.set(run.id, controller);
1183
+ queueMicrotask(() => {
1184
+ executeRun(run, connection.type, controller, modelSelection).catch((error) => {
1185
+ options.logger?.warn("data-agent Catalog run %s failed unexpectedly: %s", run.id, error);
1186
+ });
1187
+ });
1188
+ return run;
1189
+ },
1190
+ async cancel(sourceId, runId) {
1191
+ requireKnownSource(sourceId);
1192
+ const active = persistence.listRuns(sourceId).map((run) => runtimeRuns.get(run.id) ?? run).find((run) => runActive(run) && (runId === void 0 || run.id === runId));
1193
+ if (active === void 0) throw new Error("No matching active Catalog run");
1194
+ controllers.get(active.id)?.abort(/* @__PURE__ */ new Error("Catalog scan cancelled by user"));
1195
+ return active;
1196
+ },
1197
+ async interruptActiveRuns() {
1198
+ for (const run of persistence.listRuns()) {
1199
+ if (!runActive(run)) continue;
1200
+ const interrupted = ACTIVE_RUN_STATUSES.has(run.status) ? {
1201
+ ...run,
1202
+ status: "interrupted",
1203
+ completedAt: now(),
1204
+ error: "Catalog scan interrupted by process restart"
1205
+ } : {
1206
+ ...run,
1207
+ enrichment: {
1208
+ ...run.enrichment,
1209
+ status: "cancelled",
1210
+ completedAt: now(),
1211
+ error: "Catalog AI enrichment interrupted by process restart"
1212
+ }
1213
+ };
1214
+ await persistence.putRun(interrupted);
1215
+ runtimeRuns.delete(run.id);
1216
+ await persistence.deleteObservations(run.id);
1217
+ }
1218
+ }
1219
+ };
1220
+ const review = {
1221
+ async saveCandidate(sourceId, rawDefinition, semanticId, expectedVersion) {
1222
+ if (rawDefinition.kind === "meaning") throw new Error("AI business meanings can only be created by Catalog enrichment");
1223
+ const existing = semanticId === void 0 ? void 0 : persistence.getSemanticEntry(semanticId);
1224
+ if (existing !== void 0 && existing.sourceId !== sourceId) throw new Error("Semantic belongs to another Catalog source");
1225
+ const currentStatus = existing === void 0 ? void 0 : currentSemantic(existing).definition.status;
1226
+ if (currentStatus === "retired") throw new Error("Retired semantics cannot be edited");
1227
+ return appendSemantic(sourceId, semanticDefinitionSchema.parse({
1228
+ ...rawDefinition,
1229
+ status: currentStatus === "needs_review" ? "needs_review" : "inferred"
1230
+ }), semanticId, expectedVersion, false);
1231
+ },
1232
+ async verify(sourceId, semanticId, expectedVersion, rawDefinition) {
1233
+ const existing = requireSemanticEntry(sourceId, semanticId);
1234
+ if (currentSemantic(existing).definition.status === "retired") throw new Error("Retired semantics cannot be verified again");
1235
+ const note = rawDefinition.revisionNote?.trim();
1236
+ if (note === void 0 || note.length === 0) throw new Error("Verification requires revisionNote");
1237
+ return appendSemantic(sourceId, semanticDefinitionSchema.parse({
1238
+ ...rawDefinition,
1239
+ status: "verified",
1240
+ verifiedAt: now(),
1241
+ revisionNote: note,
1242
+ needsReviewReason: void 0,
1243
+ triggerRunId: void 0
1244
+ }), semanticId, expectedVersion, true);
1245
+ },
1246
+ async retire(sourceId, semanticId, expectedVersion, revisionNote) {
1247
+ if (revisionNote.trim().length === 0) throw new Error("Retirement requires revisionNote");
1248
+ const entry = requireSemanticEntry(sourceId, semanticId);
1249
+ const current = currentSemantic(entry);
1250
+ if (current.definition.status !== "verified" && current.definition.status !== "needs_review") throw new Error("Only verified or needs_review semantics can be retired");
1251
+ return appendSemantic(sourceId, {
1252
+ ...current.definition,
1253
+ status: "retired",
1254
+ revisionNote: normalizeCatalogText(revisionNote, options.maxTextChars).value
1255
+ }, semanticId, expectedVersion, true);
1256
+ },
1257
+ async dismissMeaning(sourceId, semanticId, expectedVersion) {
1258
+ const entry = requireSemanticEntry(sourceId, semanticId);
1259
+ const current = currentSemantic(entry);
1260
+ if (current.definition.kind !== "meaning" || current.definition.generatedBy.kind !== "ai") throw new Error("Only AI-generated business meanings can be deleted with this action");
1261
+ if (current.definition.status === "retired") throw new Error("Business meaning is already deleted");
1262
+ return appendSemantic(sourceId, {
1263
+ ...current.definition,
1264
+ status: "retired",
1265
+ revisionNote: "AI-generated business meaning deleted by user"
1266
+ }, semanticId, expectedVersion, true);
1267
+ }
1268
+ };
1269
+ async function executeRun(initial, databaseType, controller, modelSelection) {
1270
+ let run = await setRun(initial, {
1271
+ status: "running",
1272
+ startedAt: now()
1273
+ });
1274
+ let resolvedConnection;
1275
+ try {
1276
+ const connection = await connections.resolveForExecution(run.sessionId);
1277
+ resolvedConnection = connection;
1278
+ if (connection.profileId !== run.sourceId || connection.type !== databaseType) throw new Error("Session connection no longer matches the Catalog source");
1279
+ const adapter = adapters[connection.type];
1280
+ if (adapter === void 0) throw new Error(`No Catalog adapter for ${connection.type}`);
1281
+ let assets = 0;
1282
+ const result = await adapter.scan({
1283
+ connections,
1284
+ connection,
1285
+ sessionId: run.sessionId,
1286
+ sourceId: run.sourceId,
1287
+ runId: run.id,
1288
+ scope: run.scope,
1289
+ signal: controller.signal,
1290
+ options: {
1291
+ maxTextChars: options.maxTextChars,
1292
+ schemaConcurrency: options.schemaConcurrency,
1293
+ assetConcurrency: options.assetConcurrency
1294
+ },
1295
+ onProgress(kind) {
1296
+ assets += 1;
1297
+ if (assets > options.maxAssetsPerRun) throw new Error(`Catalog scan exceeded maxAssetsPerRun (${options.maxAssetsPerRun})`);
1298
+ const progress = {
1299
+ ...run.progress,
1300
+ assets
1301
+ };
1302
+ if (kind === "schema") progress.schemas += 1;
1303
+ if (kind === "relation") progress.relations += 1;
1304
+ if (kind === "field") progress.fields += 1;
1305
+ run = {
1306
+ ...run,
1307
+ progress
1308
+ };
1309
+ runtimeRuns.set(run.id, run);
1310
+ }
1311
+ });
1312
+ controller.signal.throwIfAborted();
1313
+ validateAdapterResult(result, run);
1314
+ for (const observation of result.observations) await persistence.putObservation(observation);
1315
+ run = await setRun(run, {
1316
+ status: "applying",
1317
+ coverageComplete: result.coverageComplete,
1318
+ progress: run.progress
1319
+ });
1320
+ await promote(run, result);
1321
+ const completedAt = now();
1322
+ run = await setRun(run, {
1323
+ status: "succeeded",
1324
+ coverageComplete: result.coverageComplete,
1325
+ completedAt
1326
+ });
1327
+ const source = persistence.getSource(run.sourceId);
1328
+ await persistence.putSource({
1329
+ ...source,
1330
+ updatedAt: completedAt,
1331
+ ...run.scope.kind === "source" ? { lastFullScanAt: completedAt } : { lastPartialScanAt: completedAt }
1332
+ });
1333
+ try {
1334
+ await markImpactedSemantics(run);
1335
+ await rebuildIndex(run.sourceId);
1336
+ await persistence.deleteObservations(run.id);
1337
+ } catch (error) {
1338
+ options.logger?.warn("data-agent Catalog run %s committed, but post-commit maintenance failed: %s", run.id, error);
1339
+ }
1340
+ if (modelSelection !== void 0 && options.meaningGenerator !== void 0) run = await enrichBusinessMeanings(run, modelSelection, options.meaningGenerator, controller, resolvedConnection);
1341
+ } catch (error) {
1342
+ const aborted = controller.signal.aborted;
1343
+ const rawMessage = error instanceof Error ? error.message : String(error);
1344
+ const message = normalizeCatalogText(redactSecretText(rawMessage, [resolvedConnection?.password]), options.maxTextChars).value;
1345
+ if (run.status === "succeeded") {
1346
+ if (run.enrichment !== void 0 && ACTIVE_ENRICHMENT_STATUSES.has(run.enrichment.status)) run = await setRun(run, { enrichment: {
1347
+ ...run.enrichment,
1348
+ status: aborted ? "cancelled" : "failed",
1349
+ completedAt: now(),
1350
+ error: message
1351
+ } });
1352
+ return;
1353
+ }
1354
+ run = await setRun(run, {
1355
+ status: aborted ? "cancelled" : "failed",
1356
+ coverageComplete: false,
1357
+ completedAt: now(),
1358
+ error: message
1359
+ });
1360
+ await persistence.deleteObservations(run.id);
1361
+ } finally {
1362
+ controllers.delete(run.id);
1363
+ runtimeRuns.delete(run.id);
1364
+ }
1365
+ }
1366
+ async function setRun(run, changes) {
1367
+ const next = {
1368
+ ...run,
1369
+ ...changes
1370
+ };
1371
+ await persistence.putRun(next);
1372
+ runtimeRuns.set(next.id, next);
1373
+ return next;
1374
+ }
1375
+ async function enrichBusinessMeanings(initial, selection, generator, controller, connection) {
1376
+ const source = requireKnownSource(initial.sourceId);
1377
+ const relations = currentRelations(initial.sourceId);
1378
+ const tables = persistence.listAssetHeads(initial.sourceId).flatMap((head) => {
1379
+ const revision = currentRevision(head.assetId);
1380
+ if (revision === void 0 || revision.status !== "observed" || revision.payload.identity.kind !== "table" && revision.payload.identity.kind !== "view" || !inScope(revision, initial.scope) || !isBusinessSchema(source, revision.payload.identity.schema)) return [];
1381
+ return [revision];
1382
+ }).sort((a, b) => a.payload.path.localeCompare(b.payload.path));
1383
+ let run = await setRun(initial, { enrichment: {
1384
+ ...initial.enrichment,
1385
+ status: "running",
1386
+ tablesTotal: tables.length,
1387
+ startedAt: now()
1388
+ } });
1389
+ let completed = 0;
1390
+ let failed = 0;
1391
+ let generated = 0;
1392
+ const errors = [];
1393
+ try {
1394
+ for (const table of tables) {
1395
+ controller.signal.throwIfAborted();
1396
+ const fields = persistence.listAssetHeads(run.sourceId).flatMap((head) => {
1397
+ const revision = currentRevision(head.assetId);
1398
+ return revision !== void 0 && revision.status === "observed" && revision.payload.parentId === table.assetId ? [revision] : [];
1399
+ }).sort((a, b) => (a.payload.ordinal ?? Number.MAX_SAFE_INTEGER) - (b.payload.ordinal ?? Number.MAX_SAFE_INTEGER) || a.payload.name.localeCompare(b.payload.name));
1400
+ const tableRelations = relations.filter((relation) => relation.fromAssetId === table.assetId || relation.toAssetId === table.assetId);
1401
+ const input = {
1402
+ assetId: table.assetId,
1403
+ schema: table.payload.identity.schema,
1404
+ name: table.payload.name,
1405
+ objectType: table.payload.identity.kind,
1406
+ ...table.payload.comment !== void 0 ? { comment: table.payload.comment } : {},
1407
+ fields: fields.map((field) => ({
1408
+ assetId: field.assetId,
1409
+ name: field.payload.name,
1410
+ ...field.payload.dataType !== void 0 ? { dataType: field.payload.dataType } : {},
1411
+ ...field.payload.nullable !== void 0 ? { nullable: field.payload.nullable } : {},
1412
+ ...field.payload.comment !== void 0 ? { comment: field.payload.comment } : {},
1413
+ keyKinds: tableRelations.filter((relation) => relation.columnAssetIds.includes(field.assetId)).map((relation) => relation.kind)
1414
+ })),
1415
+ relations: tableRelations.map((relation) => ({
1416
+ kind: relation.kind,
1417
+ ...relation.name !== void 0 ? { name: relation.name } : {},
1418
+ fromAssetId: relation.fromAssetId,
1419
+ ...relation.toAssetId !== void 0 ? { toAssetId: relation.toAssetId } : {},
1420
+ columnAssetIds: relation.columnAssetIds,
1421
+ ...relation.referencedColumnAssetIds !== void 0 ? { referencedColumnAssetIds: relation.referencedColumnAssetIds } : {}
1422
+ }))
1423
+ };
1424
+ try {
1425
+ const result = await generator.generate(selection, input, controller.signal);
1426
+ generated += await upsertGeneratedMeaning(run, table, result.table.meaning, selection);
1427
+ const byId = new Map(fields.map((field) => [field.assetId, field]));
1428
+ for (const fieldMeaning of result.fields) generated += await upsertGeneratedMeaning(run, byId.get(fieldMeaning.assetId), fieldMeaning.meaning, selection);
1429
+ completed += 1;
1430
+ } catch (error) {
1431
+ if (controller.signal.aborted) throw error;
1432
+ failed += 1;
1433
+ const message = catalogEnrichmentError(error, connection, table.payload.path);
1434
+ errors.push(message);
1435
+ options.logger?.warn("data-agent Catalog AI enrichment failed for %s: %s", table.payload.path, message);
1436
+ }
1437
+ run = await setRun(run, { enrichment: {
1438
+ ...run.enrichment,
1439
+ tablesCompleted: completed,
1440
+ tablesFailed: failed,
1441
+ candidatesGenerated: generated,
1442
+ ...errors.length > 0 ? { error: errors.slice(-3).join(" | ") } : {}
1443
+ } });
1444
+ }
1445
+ await rebuildIndex(run.sourceId);
1446
+ const status = failed === 0 ? "succeeded" : completed === 0 ? "failed" : "partial";
1447
+ return setRun(run, { enrichment: {
1448
+ ...run.enrichment,
1449
+ status,
1450
+ completedAt: now(),
1451
+ ...errors.length > 0 ? { error: errors.slice(-3).join(" | ") } : {}
1452
+ } });
1453
+ } catch (error) {
1454
+ const cancelled = controller.signal.aborted;
1455
+ const message = catalogEnrichmentError(error, connection);
1456
+ return setRun(run, { enrichment: {
1457
+ ...run.enrichment,
1458
+ status: cancelled ? "cancelled" : completed === 0 ? "failed" : "partial",
1459
+ tablesCompleted: completed,
1460
+ tablesFailed: failed + (cancelled ? 0 : 1),
1461
+ candidatesGenerated: generated,
1462
+ completedAt: now(),
1463
+ error: message
1464
+ } });
1465
+ }
1466
+ }
1467
+ async function upsertGeneratedMeaning(run, asset, description, selection) {
1468
+ const semanticId = `meaning_${asset.assetId}`;
1469
+ const existing = persistence.getSemanticEntry(semanticId);
1470
+ const current = existing === void 0 ? void 0 : currentSemantic(existing);
1471
+ if (current !== void 0) {
1472
+ if (current.definition.kind !== "meaning") throw new Error(`Catalog semantic id collision: ${semanticId}`);
1473
+ if (current.definition.status !== "inferred" || current.definition.description === description) return 0;
1474
+ }
1475
+ const definition = {
1476
+ kind: "meaning",
1477
+ name: asset.payload.name,
1478
+ aliases: [],
1479
+ description,
1480
+ sourceAssetIds: [asset.assetId],
1481
+ status: "inferred",
1482
+ targetAssetId: asset.assetId,
1483
+ targetKind: asset.payload.identity.kind,
1484
+ generatedBy: {
1485
+ kind: "ai",
1486
+ provider: selection.provider,
1487
+ model: selection.model,
1488
+ runId: run.id
1489
+ },
1490
+ triggerRunId: run.id,
1491
+ revisionNote: `AI business meaning candidate generated by Catalog run ${run.id}`
1492
+ };
1493
+ await appendSemantic(run.sourceId, definition, semanticId, existing?.currentVersion, false, false);
1494
+ return 1;
1495
+ }
1496
+ function catalogEnrichmentError(error, connection, path) {
1497
+ const raw = error instanceof Error ? error.message : String(error);
1498
+ const prefix = path === void 0 ? "" : `${path}: `;
1499
+ return normalizeCatalogText(redactSecretText(`${prefix}${raw}`, [connection.password]), options.maxTextChars).value;
1500
+ }
1501
+ function validateAdapterResult(result, run) {
1502
+ if (result.observations.length > options.maxAssetsPerRun) throw new Error(`Catalog scan exceeded maxAssetsPerRun (${options.maxAssetsPerRun})`);
1503
+ result.observations.forEach((value) => catalogObservationSchema.parse(value));
1504
+ result.relations.forEach((value) => catalogRelationSchema.parse(value));
1505
+ const ids = new Set(result.observations.map((value) => value.assetId));
1506
+ if (ids.size !== result.observations.length) throw new Error("Catalog adapter returned duplicate asset ids");
1507
+ for (const observation of result.observations) {
1508
+ if (observation.runId !== run.id || observation.sourceId !== run.sourceId) throw new Error("Catalog adapter returned an observation for another run or source");
1509
+ const parentId = observation.payload.parentId;
1510
+ if (parentId !== void 0 && !ids.has(parentId) && currentRevision(parentId) === void 0) throw new Error(`Catalog observation has unknown parent ${parentId}`);
1511
+ }
1512
+ if (new Set(result.relations.map((value) => value.id)).size !== result.relations.length) throw new Error("Catalog adapter returned duplicate relation ids");
1513
+ const knownAsset = (assetId) => ids.has(assetId) || currentRevision(assetId) !== void 0;
1514
+ for (const relation of result.relations) {
1515
+ if (relation.runId !== run.id || relation.sourceId !== run.sourceId) throw new Error("Catalog adapter returned a relation for another run or source");
1516
+ const unknown = [
1517
+ relation.fromAssetId,
1518
+ relation.toAssetId,
1519
+ ...relation.columnAssetIds,
1520
+ ...relation.referencedColumnAssetIds ?? []
1521
+ ].filter((value) => value !== void 0).find((assetId) => !knownAsset(assetId));
1522
+ if (unknown !== void 0) throw new Error(`Catalog relation has unknown asset reference ${unknown}`);
1523
+ }
1524
+ }
1525
+ async function promote(run, result) {
1526
+ const observations = [...result.observations];
1527
+ const observedIds = new Set(observations.map((value) => value.assetId));
1528
+ if (result.coverageComplete) for (const head of persistence.listAssetHeads(run.sourceId)) {
1529
+ const current = currentRevision(head.assetId);
1530
+ if (current === void 0 || current.status === "missing" || !inScope(current, run.scope) || observedIds.has(head.assetId)) continue;
1531
+ const payload = {
1532
+ ...current.payload,
1533
+ provenance: {
1534
+ ...current.payload.provenance,
1535
+ runId: run.id
1536
+ }
1537
+ };
1538
+ observations.push({
1539
+ runId: run.id,
1540
+ sourceId: run.sourceId,
1541
+ assetId: current.assetId,
1542
+ status: "missing",
1543
+ fingerprint: catalogTechnicalFingerprint(payload, "missing"),
1544
+ observedAt: now(),
1545
+ payload
1546
+ });
1547
+ }
1548
+ for (const observation of observations) await promoteObservation(observation);
1549
+ for (const relation of result.relations) await persistence.putRelation(relation);
1550
+ }
1551
+ async function promoteObservation(observation) {
1552
+ const current = currentRevision(observation.assetId);
1553
+ const existingHead = persistence.getAssetHead(observation.assetId);
1554
+ if (current?.fingerprint === observation.fingerprint && current.status === observation.status) {
1555
+ if (existingHead !== void 0) await persistence.putAssetHead({
1556
+ ...existingHead,
1557
+ lastSeenAt: observation.observedAt
1558
+ });
1559
+ return;
1560
+ }
1561
+ const revisionNumber = (existingHead?.revisionIds.length ?? 0) + 1;
1562
+ const revision = {
1563
+ id: catalogRevisionId(observation.assetId, revisionNumber),
1564
+ assetId: observation.assetId,
1565
+ sourceId: observation.sourceId,
1566
+ runId: observation.runId,
1567
+ revision: revisionNumber,
1568
+ status: observation.status,
1569
+ fingerprint: observation.fingerprint,
1570
+ observedAt: observation.observedAt,
1571
+ ...current !== void 0 ? { previousRevisionId: current.id } : {},
1572
+ changeSummary: summarizeTechnicalChange(current, observation),
1573
+ payload: observation.payload
1574
+ };
1575
+ await persistence.putAssetRevision(revision);
1576
+ const head = existingHead === void 0 ? {
1577
+ assetId: observation.assetId,
1578
+ sourceId: observation.sourceId,
1579
+ revisionIds: [revision.id],
1580
+ firstSeenAt: observation.observedAt,
1581
+ lastSeenAt: observation.observedAt
1582
+ } : {
1583
+ ...existingHead,
1584
+ revisionIds: [...existingHead.revisionIds, revision.id],
1585
+ lastSeenAt: observation.observedAt
1586
+ };
1587
+ await persistence.putAssetHead(head);
1588
+ }
1589
+ async function appendSemantic(sourceId, rawDefinition, semanticId, expectedVersion, requireExisting, rebuild = true) {
1590
+ requireKnownSource(sourceId);
1591
+ const definition = semanticDefinitionSchema.parse(normalizeSemanticDefinition(rawDefinition, options.maxTextChars));
1592
+ validateSemanticReferences(sourceId, definition);
1593
+ const id = semanticId ?? (definition.kind === "meaning" ? `meaning_${definition.targetAssetId}` : catalogSemanticId(sourceId, definition.kind, definition.name));
1594
+ const existing = persistence.getSemanticEntry(id);
1595
+ if (requireExisting && existing === void 0) throw new Error(`Unknown Catalog semantic: ${id}`);
1596
+ if (existing !== void 0 && existing.sourceId !== sourceId) throw new Error("Semantic belongs to another Catalog source");
1597
+ if (existing !== void 0 && expectedVersion !== existing.currentVersion) throw new CatalogVersionConflictError(currentSemantic(existing));
1598
+ if (existing === void 0 && expectedVersion !== void 0 && expectedVersion !== 0) throw new Error("New semantic expectedVersion must be 0 or omitted");
1599
+ const version = (existing?.currentVersion ?? 0) + 1;
1600
+ const timestamp = now();
1601
+ const revision = {
1602
+ id: catalogSemanticRevisionId(id, version),
1603
+ semanticId: id,
1604
+ sourceId,
1605
+ version,
1606
+ createdAt: timestamp,
1607
+ definition
1608
+ };
1609
+ await persistence.putSemanticRevision(revision);
1610
+ await persistence.putSemanticEntry({
1611
+ id,
1612
+ sourceId,
1613
+ kind: definition.kind,
1614
+ currentVersion: version,
1615
+ createdAt: existing?.createdAt ?? timestamp,
1616
+ updatedAt: timestamp
1617
+ });
1618
+ if (rebuild) await rebuildIndex(sourceId);
1619
+ return revision;
1620
+ }
1621
+ function validateSemanticReferences(sourceId, definition) {
1622
+ for (const assetId of definition.sourceAssetIds) {
1623
+ const revision = currentRevision(assetId);
1624
+ if (revision === void 0 || revision.sourceId !== sourceId) throw new Error(`Unknown or cross-source asset reference: ${assetId}`);
1625
+ }
1626
+ if (definition.kind === "metric" && definition.timeFieldAssetId !== void 0) {
1627
+ const field = currentRevision(definition.timeFieldAssetId);
1628
+ if (field === void 0 || field.sourceId !== sourceId || field.payload.identity.kind !== "column") throw new Error(`Invalid metric time field: ${definition.timeFieldAssetId}`);
1629
+ }
1630
+ if (definition.kind === "meaning") {
1631
+ const target = currentRevision(definition.targetAssetId);
1632
+ if (target === void 0 || target.sourceId !== sourceId || target.payload.identity.kind !== definition.targetKind) throw new Error(`Invalid business meaning target: ${definition.targetAssetId}`);
1633
+ if (definition.sourceAssetIds.length !== 1 || definition.sourceAssetIds[0] !== definition.targetAssetId) throw new Error("Business meaning sourceAssetIds must contain only its target asset");
1634
+ const generatedRun = persistence.getRun(definition.generatedBy.runId);
1635
+ if (generatedRun === void 0 || generatedRun.sourceId !== sourceId) throw new Error(`Invalid business meaning generation run: ${definition.generatedBy.runId}`);
1636
+ }
1637
+ }
1638
+ async function markImpactedSemantics(run) {
1639
+ const changed = persistence.listAssetRevisions().filter((revision) => revision.runId === run.id && (revision.status === "missing" || incompatibleTypeChange(revision)));
1640
+ if (changed.length === 0) return;
1641
+ const changedIds = new Set(changed.map((revision) => revision.assetId));
1642
+ for (const entry of persistence.listSemanticEntries(run.sourceId)) {
1643
+ const current = currentSemantic(entry);
1644
+ if (current.definition.status === "retired" || current.definition.status === "needs_review") continue;
1645
+ const impacted = current.definition.sourceAssetIds.filter((id) => changedIds.has(id));
1646
+ if (current.definition.kind === "metric" && current.definition.timeFieldAssetId !== void 0 && changedIds.has(current.definition.timeFieldAssetId)) impacted.push(current.definition.timeFieldAssetId);
1647
+ if (impacted.length === 0) continue;
1648
+ await appendSemantic(run.sourceId, {
1649
+ ...current.definition,
1650
+ status: "needs_review",
1651
+ needsReviewReason: `Referenced Catalog assets changed: ${[...new Set(impacted)].join(", ")}`,
1652
+ triggerRunId: run.id,
1653
+ revisionNote: `Automatically marked needs_review after Catalog run ${run.id}`
1654
+ }, entry.id, entry.currentVersion, true);
1655
+ }
1656
+ }
1657
+ async function ensureIndex(sourceId) {
1658
+ if (persistence.getIndexState()?.version !== 1 || persistence.listIndex(sourceId).length === 0) await rebuildIndex(sourceId);
1659
+ }
1660
+ async function rebuildIndex(sourceId) {
1661
+ await persistence.clearIndex(sourceId);
1662
+ const timestamp = now();
1663
+ for (const head of persistence.listAssetHeads(sourceId)) {
1664
+ const revision = currentRevision(head.assetId);
1665
+ if (revision === void 0) continue;
1666
+ const payload = revision.payload;
1667
+ const item = {
1668
+ id: revision.assetId,
1669
+ sourceId,
1670
+ resultType: "asset",
1671
+ kind: payload.identity.kind,
1672
+ name: payload.name,
1673
+ path: payload.path,
1674
+ summary: payload.comment ?? payload.dataType ?? "",
1675
+ matchReasons: [],
1676
+ status: revision.status,
1677
+ provenance: "database",
1678
+ untrusted: true
1679
+ };
1680
+ await persistence.putIndex(indexRecord(item, [
1681
+ payload.name,
1682
+ payload.path,
1683
+ payload.comment,
1684
+ payload.dataType
1685
+ ], timestamp));
1686
+ }
1687
+ for (const entry of persistence.listSemanticEntries(sourceId)) {
1688
+ const revision = currentSemantic(entry);
1689
+ const definition = revision.definition;
1690
+ if (definition.status === "retired") continue;
1691
+ const item = {
1692
+ id: entry.id,
1693
+ sourceId,
1694
+ resultType: "semantic",
1695
+ kind: definition.kind,
1696
+ name: definition.name,
1697
+ path: `${definition.kind}:${definition.name}`,
1698
+ summary: definition.description,
1699
+ matchReasons: [],
1700
+ status: definition.status,
1701
+ version: revision.version,
1702
+ provenance: definition.status === "inferred" ? "inferred" : "human",
1703
+ untrusted: true
1704
+ };
1705
+ await persistence.putIndex(indexRecord(item, [
1706
+ definition.name,
1707
+ ...definition.aliases,
1708
+ definition.description,
1709
+ definition.kind === "metric" ? definition.formula : void 0
1710
+ ], timestamp));
1711
+ }
1712
+ await persistence.putIndexState({
1713
+ version: 1,
1714
+ rebuiltAt: timestamp
1715
+ });
1716
+ }
1717
+ function indexRecord(item, values, timestamp) {
1718
+ const searchText = values.filter((value) => value !== void 0).join(" ").normalize("NFKC").toLocaleLowerCase("en-US");
1719
+ return {
1720
+ id: `${item.resultType}:${item.id}`,
1721
+ sourceId: item.sourceId,
1722
+ resultType: item.resultType,
1723
+ searchText,
1724
+ searchItem: item,
1725
+ updatedAt: timestamp
1726
+ };
1727
+ }
1728
+ function buildDiff(sourceId, from, to) {
1729
+ const items = [];
1730
+ for (const head of persistence.listAssetHeads(sourceId)) {
1731
+ const before = revisionAtRun(head.assetId, from);
1732
+ const after = revisionAtRun(head.assetId, to);
1733
+ if (before?.id === after?.id || before === void 0 && after === void 0) continue;
1734
+ const kind = diffKind(before, after);
1735
+ if (kind === void 0) continue;
1736
+ const revision = after ?? before;
1737
+ items.push({
1738
+ kind,
1739
+ assetId: head.assetId,
1740
+ name: revision.payload.name,
1741
+ path: revision.payload.path,
1742
+ ...before !== void 0 ? { fromRevisionId: before.id } : {},
1743
+ ...after !== void 0 ? { toRevisionId: after.id } : {},
1744
+ summary: after?.changeSummary ?? ["asset removed from the target snapshot"]
1745
+ });
1746
+ }
1747
+ return items.sort((a, b) => diffOrder(a.kind) - diffOrder(b.kind) || a.path.localeCompare(b.path) || a.assetId.localeCompare(b.assetId));
1748
+ }
1749
+ function currentRelations(sourceId) {
1750
+ const runs = successfulRuns(sourceId);
1751
+ const latestApplicableRun = /* @__PURE__ */ new Map();
1752
+ for (const head of persistence.listAssetHeads(sourceId)) {
1753
+ const revision = currentRevision(head.assetId);
1754
+ if (revision === void 0) continue;
1755
+ const run = [...runs].reverse().find((candidate) => inScope(revision, candidate.scope));
1756
+ if (run !== void 0) latestApplicableRun.set(head.assetId, run.id);
1757
+ }
1758
+ return persistence.listRelations(sourceId).filter((relation) => latestApplicableRun.get(relation.fromAssetId) === relation.runId).sort((a, b) => a.kind.localeCompare(b.kind) || (a.name ?? "").localeCompare(b.name ?? "") || a.id.localeCompare(b.id));
1759
+ }
1760
+ function requireKnownSource(sourceId) {
1761
+ const source = persistence.getSource(nonEmpty(sourceId, "sourceId"));
1762
+ if (source === void 0) throw new Error(`Unknown Catalog source: ${sourceId}`);
1763
+ return source;
1764
+ }
1765
+ function requireSemanticEntry(sourceId, semanticId) {
1766
+ const entry = persistence.getSemanticEntry(semanticId);
1767
+ if (entry === void 0 || entry.sourceId !== sourceId) throw new Error(`Unknown Catalog semantic: ${semanticId}`);
1768
+ return entry;
1769
+ }
1770
+ await scanner.interruptActiveRuns();
1771
+ return {
1772
+ read,
1773
+ scanner,
1774
+ review
1775
+ };
1776
+ }
1777
+ function compareRun(a, b) {
1778
+ return a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);
1779
+ }
1780
+ function runOrderKey(run) {
1781
+ return `${run.createdAt}\0${run.id}`;
1782
+ }
1783
+ function inScope(revision, scope) {
1784
+ const identity = revision.payload.identity;
1785
+ if (scope.kind === "source") return true;
1786
+ if (identity.schema.toLocaleLowerCase("en-US") !== scope.schema.toLocaleLowerCase("en-US")) return false;
1787
+ if (scope.kind === "schema") return true;
1788
+ return (identity.kind === "table" || identity.kind === "view" ? identity.name : identity.relation)?.toLocaleLowerCase("en-US") === scope.table.toLocaleLowerCase("en-US");
1789
+ }
1790
+ function isBusinessSchema(source, schema) {
1791
+ const normalized = schema.toLocaleLowerCase("en-US");
1792
+ if (source.type === "mysql" || source.type === "doris" || source.type === "clickhouse") return normalized === source.database.toLocaleLowerCase("en-US");
1793
+ if (source.type === "sqlite") return normalized === "main";
1794
+ if (normalized === "information_schema" || normalized === "sys" || normalized === "system") return false;
1795
+ if (source.type === "postgres" && (normalized === "pg_catalog" || normalized.startsWith("pg_toast"))) return false;
1796
+ if (source.type === "oracle" && [
1797
+ "sys",
1798
+ "system",
1799
+ "xdb",
1800
+ "outln"
1801
+ ].includes(normalized)) return false;
1802
+ return true;
1803
+ }
1804
+ function normalizeScope(type, scope) {
1805
+ if (scope.kind === "source") return scope;
1806
+ const schema = normalizeCatalogIdentifier(type, scope.schema);
1807
+ if (scope.kind === "schema") return {
1808
+ kind: "schema",
1809
+ schema
1810
+ };
1811
+ return {
1812
+ kind: "table",
1813
+ schema,
1814
+ table: normalizeCatalogIdentifier(type, scope.table)
1815
+ };
1816
+ }
1817
+ function summarizeTechnicalChange(current, next) {
1818
+ if (current === void 0) return ["added"];
1819
+ if (current.status === "missing" && next.status === "observed") return ["restored"];
1820
+ if (next.status === "missing") return ["missing"];
1821
+ if (next.status === "unavailable") return ["unavailable"];
1822
+ const fields = [];
1823
+ for (const key of [
1824
+ "dataType",
1825
+ "nullable",
1826
+ "comment",
1827
+ "parentId",
1828
+ "objectType"
1829
+ ]) if (stableJson(current.payload[key]) !== stableJson(next.payload[key])) fields.push(`${key} changed`);
1830
+ return fields.length > 0 ? fields : ["technical metadata changed"];
1831
+ }
1832
+ function incompatibleTypeChange(revision) {
1833
+ if (revision.payload.identity.kind !== "column" || revision.previousRevisionId === void 0) return false;
1834
+ return revision.changeSummary.includes("dataType changed");
1835
+ }
1836
+ function diffKind(before, after) {
1837
+ if (before === void 0 && after !== void 0) return "added";
1838
+ if (after === void 0) return void 0;
1839
+ if (after.status === "missing" && before?.status !== "missing") return "missing";
1840
+ if (after.status === "unavailable" && before?.status !== "unavailable") return "unavailable";
1841
+ if (before?.status === "missing" && after.status === "observed") return "restored";
1842
+ if (before?.fingerprint !== after.fingerprint) return "changed";
1843
+ }
1844
+ function diffOrder(kind) {
1845
+ return [
1846
+ "added",
1847
+ "changed",
1848
+ "missing",
1849
+ "restored",
1850
+ "unavailable"
1851
+ ].indexOf(kind);
1852
+ }
1853
+ function filterSearchItem(item, request) {
1854
+ const filters = request.filters;
1855
+ if (item.resultType === "asset") {
1856
+ if (filters.assetKinds !== void 0 && !filters.assetKinds.some((value) => value === item.kind)) return false;
1857
+ if (filters.assetStatuses !== void 0 && !filters.assetStatuses.some((value) => value === item.status)) return false;
1858
+ if (filters.schema !== void 0 && !item.path.toLocaleLowerCase("en-US").includes(`.${filters.schema.toLocaleLowerCase("en-US")}.`)) return false;
1859
+ return true;
1860
+ }
1861
+ if (filters.semanticKinds !== void 0 && !filters.semanticKinds.some((value) => value === item.kind)) return false;
1862
+ if (item.status === "inferred" && !filters.includeInferred) return false;
1863
+ if (filters.semanticStatuses !== void 0 && !filters.semanticStatuses.some((value) => value === item.status)) return false;
1864
+ return true;
1865
+ }
1866
+ function compareSearchItems(a, b) {
1867
+ return searchRank(a) - searchRank(b) || a.name.localeCompare(b.name) || a.id.localeCompare(b.id);
1868
+ }
1869
+ function searchRank(item) {
1870
+ if (item.resultType === "semantic" && item.status === "verified") return 0;
1871
+ if (item.resultType === "asset" && item.status === "observed") return 10;
1872
+ if (item.status === "needs_review") return 20;
1873
+ if (item.status === "inferred") return 30;
1874
+ if (item.status === "missing") return 40;
1875
+ return 50;
1876
+ }
1877
+ function searchMatchReasons(item, query) {
1878
+ if (query === "*") return ["browse"];
1879
+ const reasons = [];
1880
+ if (item.name.toLocaleLowerCase("en-US").includes(query)) reasons.push("name");
1881
+ if (item.path.toLocaleLowerCase("en-US").includes(query)) reasons.push("path");
1882
+ if (item.summary.toLocaleLowerCase("en-US").includes(query)) reasons.push("description");
1883
+ return reasons.length > 0 ? reasons : ["definition or alias"];
1884
+ }
1885
+ function encodeCursor(offset, sourceId, query) {
1886
+ return Buffer.from(JSON.stringify({
1887
+ offset,
1888
+ sourceId,
1889
+ query
1890
+ }), "utf8").toString("base64url");
1891
+ }
1892
+ function decodeCursor(cursor, sourceId, query) {
1893
+ if (cursor === void 0) return 0;
1894
+ if (cursor.length > 512) throw new Error("Invalid Catalog cursor");
1895
+ try {
1896
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
1897
+ if (!Number.isInteger(parsed.offset) || parsed.offset < 0 || parsed.sourceId !== sourceId || parsed.query !== query) throw new Error("mismatch");
1898
+ return parsed.offset;
1899
+ } catch {
1900
+ throw new Error("Invalid Catalog cursor");
1901
+ }
1902
+ }
1903
+ function nonEmpty(value, label) {
1904
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > 256) throw new Error(`${label} must be a non-empty bounded string`);
1905
+ return value;
1906
+ }
1907
+ function normalizeSemanticDefinition(definition, maxTextChars) {
1908
+ const text = (value, max = maxTextChars) => normalizeCatalogText(value, max).value;
1909
+ const common = {
1910
+ ...definition,
1911
+ name: text(definition.name, 256),
1912
+ aliases: definition.aliases.map((value) => text(value, 256)),
1913
+ description: text(definition.description),
1914
+ ...definition.owner !== void 0 ? { owner: text(definition.owner, 256) } : {},
1915
+ ...definition.revisionNote !== void 0 ? { revisionNote: text(definition.revisionNote) } : {},
1916
+ ...definition.needsReviewReason !== void 0 ? { needsReviewReason: text(definition.needsReviewReason) } : {}
1917
+ };
1918
+ if (definition.kind === "meaning") return {
1919
+ ...common,
1920
+ kind: "meaning",
1921
+ targetAssetId: definition.targetAssetId,
1922
+ targetKind: definition.targetKind,
1923
+ generatedBy: {
1924
+ kind: "ai",
1925
+ provider: text(definition.generatedBy.provider, 256),
1926
+ model: text(definition.generatedBy.model, 512),
1927
+ runId: definition.generatedBy.runId
1928
+ }
1929
+ };
1930
+ if (definition.kind === "term") return {
1931
+ ...common,
1932
+ kind: "term"
1933
+ };
1934
+ return {
1935
+ ...common,
1936
+ kind: "metric",
1937
+ formula: text(definition.formula, 8192),
1938
+ grain: text(definition.grain, 512),
1939
+ filters: definition.filters.map((value) => text(value, 2048)),
1940
+ exclusions: definition.exclusions.map((value) => text(value, 2048))
1941
+ };
1942
+ }
1943
+ //#endregion
1944
+ export { catalogDateTimeSchema as a, catalogRunSchema as c, catalogSearchRequestSchema as d, catalogSemanticEntrySchema as f, semanticDefinitionSchema as h, catalogAssetRevisionSchema as i, catalogScopeSchema as l, catalogSourceSchema as m, createCatalogService as n, catalogObservationSchema as o, catalogSemanticRevisionSchema as p, catalogAssetHeadSchema as r, catalogRelationSchema as s, CatalogVersionConflictError as t, catalogSearchItemSchema as u };