@duckcodeailabs/dql-agent 1.6.5 → 1.6.6

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.
@@ -0,0 +1,1991 @@
1
+ /**
2
+ * Project-local metadata catalog for OSS agentic analytics.
3
+ *
4
+ * Git/DQL/dbt files remain the source of truth. This SQLite database is a
5
+ * rebuildable local catalog at `.dql/cache/metadata.sqlite` used by agents,
6
+ * MCP tools, app builder, and notebook/block AI to retrieve one consistent
7
+ * context pack before answering.
8
+ */
9
+ import { createHash } from 'node:crypto';
10
+ import { mkdirSync } from 'node:fs';
11
+ import { createRequire } from 'node:module';
12
+ import { dirname, join } from 'node:path';
13
+ import { buildManifest, loadProjectConfig, resolveDbtManifestPath, resolveSemanticLayerWithDiagnostics, } from '@duckcodeailabs/dql-core';
14
+ import { buildKGFromManifest, buildKGFromSemanticLayer } from '../kg/build.js';
15
+ const require = createRequire(import.meta.url);
16
+ let databaseCtor = null;
17
+ function loadDatabase() {
18
+ databaseCtor ??= require('better-sqlite3');
19
+ return databaseCtor;
20
+ }
21
+ const OBJECT_PRIORITY = {
22
+ dql_block: 1,
23
+ semantic_metric: 2,
24
+ dql_term: 3,
25
+ business_view: 4,
26
+ semantic_dimension: 5,
27
+ semantic_measure: 6,
28
+ semantic_entity: 7,
29
+ semantic_model: 8,
30
+ dbt_model: 9,
31
+ dbt_source: 10,
32
+ dbt_column: 11,
33
+ warehouse_table: 12,
34
+ notebook: 13,
35
+ dashboard: 14,
36
+ app: 15,
37
+ domain: 16,
38
+ };
39
+ export function defaultMetadataPath(projectRoot) {
40
+ return join(projectRoot, '.dql', 'cache', 'metadata.sqlite');
41
+ }
42
+ export function openMetadataCatalog(projectRoot, dbPath = defaultMetadataPath(projectRoot)) {
43
+ return new MetadataCatalog(dbPath);
44
+ }
45
+ export async function ensureMetadataCatalogFresh(projectRoot, options = {}) {
46
+ const semanticLayer = options.semanticLayer !== undefined
47
+ ? options.semanticLayer ?? undefined
48
+ : loadAgentSemanticLayer(projectRoot);
49
+ const manifest = options.manifest ?? loadAgentManifest(projectRoot);
50
+ const snapshot = buildMetadataSnapshot(projectRoot, manifest, semanticLayer);
51
+ const catalog = openMetadataCatalog(projectRoot);
52
+ try {
53
+ const existing = catalog.state('fingerprint');
54
+ if (!options.force && existing === snapshot.fingerprint) {
55
+ return {
56
+ path: defaultMetadataPath(projectRoot),
57
+ refreshed: false,
58
+ objectCount: catalog.objectCount(),
59
+ edgeCount: catalog.edgeCount(),
60
+ diagnostics: catalog.diagnostics(),
61
+ fingerprint: snapshot.fingerprint,
62
+ };
63
+ }
64
+ catalog.rebuild(snapshot);
65
+ return {
66
+ path: defaultMetadataPath(projectRoot),
67
+ refreshed: true,
68
+ objectCount: snapshot.objects.length,
69
+ edgeCount: snapshot.edges.length,
70
+ diagnostics: snapshot.diagnostics,
71
+ fingerprint: snapshot.fingerprint,
72
+ };
73
+ }
74
+ finally {
75
+ catalog.close();
76
+ }
77
+ }
78
+ export function upsertMetadataSnapshot(projectRoot, snapshot) {
79
+ const catalog = openMetadataCatalog(projectRoot);
80
+ try {
81
+ catalog.rebuild(snapshot);
82
+ return {
83
+ path: defaultMetadataPath(projectRoot),
84
+ refreshed: true,
85
+ objectCount: snapshot.objects.length,
86
+ edgeCount: snapshot.edges.length,
87
+ diagnostics: snapshot.diagnostics,
88
+ fingerprint: snapshot.fingerprint,
89
+ };
90
+ }
91
+ finally {
92
+ catalog.close();
93
+ }
94
+ }
95
+ export async function buildLocalContextPack(projectRoot, request) {
96
+ await ensureMetadataCatalogFresh(projectRoot);
97
+ const catalog = openMetadataCatalog(projectRoot);
98
+ try {
99
+ const mode = request.mode ?? 'question';
100
+ const runtimeSnapshot = request.runtimeSchemaSnapshot ?? catalog.latestRuntimeSchemaSnapshot();
101
+ const runtimeObjects = runtimeSnapshot ? runtimeSchemaObjects(runtimeSnapshot) : [];
102
+ const selectedObjects = selectedContextObjects(request.selectedContext);
103
+ const selectedReferencedObjects = catalog
104
+ .getObjectsByKeys(selectedContextReferenceKeys(request.selectedContext, selectedObjects))
105
+ .map((object) => ({ ...object, score: Math.max(object.score ?? 0, 20) }));
106
+ const searchRows = catalog.searchObjects({
107
+ query: request.question,
108
+ objectTypes: request.objectTypes,
109
+ limit: Math.max(request.limit ?? 80, 20),
110
+ });
111
+ const exact = request.focusObjectKey ? catalog.getObject(request.focusObjectKey) : null;
112
+ const ranked = rankMetadataObjects({
113
+ rows: mergeObjects(exact
114
+ ? [exact, ...selectedReferencedObjects, ...searchRows, ...runtimeObjects, ...selectedObjects]
115
+ : [...selectedReferencedObjects, ...searchRows, ...runtimeObjects, ...selectedObjects]),
116
+ question: request.question,
117
+ limit: request.limit ?? 80,
118
+ });
119
+ const selected = ranked.selected;
120
+ const focusObjectKey = request.focusObjectKey ?? selectedReferencedObjects[0]?.objectKey ?? selected[0]?.objectKey ?? null;
121
+ const edgeWalk = catalog.edgesForKeys(selected.map((row) => row.objectKey), 3);
122
+ const edgeObjectKeys = Array.from(new Set(edgeWalk.flatMap((edge) => [edge.fromKey, edge.toKey])));
123
+ const graphObjects = catalog.getObjectsByKeys(edgeObjectKeys);
124
+ const objects = rankMetadataObjects({
125
+ rows: mergeObjects([...selectedReferencedObjects, ...selected, ...graphObjects, ...runtimeObjects, ...selectedObjects]),
126
+ question: request.question,
127
+ limit: request.limit ?? 120,
128
+ }).selected;
129
+ const objectKeys = objects.map((row) => row.objectKey);
130
+ const queryRuns = catalog.queryRunsForObjectKeys(objectKeys, 20);
131
+ const diagnostics = catalog.diagnostics();
132
+ const warnings = buildWarnings(diagnostics, objects);
133
+ const trustLabel = deriveTrust(objects);
134
+ const citations = buildCitations(objects, edgeWalk);
135
+ const evidenceSummaries = buildEvidenceSummaries(objects, edgeWalk, queryRuns, diagnostics);
136
+ const allowedSqlContext = buildAllowedSqlContext(objects, edgeWalk);
137
+ const evidenceRoles = buildEvidenceRoles(objects, queryRuns);
138
+ const reranked = rankMetadataObjects({
139
+ rows: mergeObjects([...searchRows, ...objects]),
140
+ question: request.question,
141
+ limit: request.limit ?? 120,
142
+ });
143
+ const conflicts = buildCandidateConflicts(reranked.ranked);
144
+ const routeDecision = planContextPackRoute({
145
+ request,
146
+ objects,
147
+ allowedSqlContext,
148
+ evidenceRoles,
149
+ diagnostics,
150
+ trustLabel,
151
+ });
152
+ const payload = {
153
+ id: '',
154
+ question: request.question,
155
+ focusObjectKey,
156
+ mode,
157
+ trustLabel,
158
+ objects,
159
+ edges: edgeWalk,
160
+ queryRuns,
161
+ citations,
162
+ evidenceSummaries,
163
+ warnings,
164
+ routeDecision,
165
+ evidenceRoles,
166
+ allowedSqlContext,
167
+ missingContext: routeDecision.missingContext,
168
+ conflicts,
169
+ retrievalDiagnostics: {
170
+ strategy: 'sqlite_fts',
171
+ selectedObjects: objects.length,
172
+ selectedEvidence: reranked.ranked.slice(0, 20).map((item) => ({
173
+ objectKey: item.row.objectKey,
174
+ objectType: item.row.objectType,
175
+ name: item.row.name,
176
+ reason: item.reason,
177
+ rank: item.rank,
178
+ score: item.score,
179
+ priorityTier: item.priorityTier,
180
+ })),
181
+ topRejected: reranked.rejected,
182
+ candidateConflicts: conflicts,
183
+ },
184
+ freshness: {
185
+ catalogPath: defaultMetadataPath(projectRoot),
186
+ builtAt: catalog.state('built_at'),
187
+ fingerprint: catalog.state('fingerprint'),
188
+ },
189
+ };
190
+ const packPayload = { ...payload };
191
+ delete packPayload.id;
192
+ const id = catalog.insertContextPack(packPayload);
193
+ return { ...payload, id };
194
+ }
195
+ finally {
196
+ catalog.close();
197
+ }
198
+ }
199
+ export async function planAgentAnswer(projectRoot, request) {
200
+ const contextPack = await buildLocalContextPack(projectRoot, request);
201
+ return {
202
+ contextPackId: contextPack.id,
203
+ contextPack,
204
+ routeDecision: contextPack.routeDecision,
205
+ evidenceRoles: contextPack.evidenceRoles,
206
+ allowedSqlContext: contextPack.allowedSqlContext,
207
+ missingContext: contextPack.missingContext,
208
+ warnings: contextPack.warnings,
209
+ freshness: contextPack.freshness,
210
+ };
211
+ }
212
+ export function recordQueryRun(projectRoot, run) {
213
+ const catalog = openMetadataCatalog(projectRoot);
214
+ try {
215
+ return catalog.recordQueryRun(run);
216
+ }
217
+ finally {
218
+ catalog.close();
219
+ }
220
+ }
221
+ export function recordRuntimeSchemaSnapshot(projectRoot, snapshot) {
222
+ const catalog = openMetadataCatalog(projectRoot);
223
+ try {
224
+ return catalog.recordRuntimeSchemaSnapshot(snapshot);
225
+ }
226
+ finally {
227
+ catalog.close();
228
+ }
229
+ }
230
+ export function buildMetadataSnapshot(projectRoot, manifest, semanticLayer) {
231
+ const manifestGraph = buildKGFromManifest(manifest);
232
+ const semanticGraph = buildKGFromSemanticLayer(semanticLayer);
233
+ const objects = new Map();
234
+ const edges = new Map();
235
+ const diagnostics = [
236
+ ...(manifest.diagnostics ?? []).map(manifestDiagnosticToMetadataDiagnostic),
237
+ ];
238
+ for (const node of [...manifestGraph.nodes, ...semanticGraph.nodes]) {
239
+ const object = objectFromKGNode(node);
240
+ const existing = objects.get(object.objectKey);
241
+ if (existing) {
242
+ diagnostics.push({
243
+ kind: 'metadata',
244
+ severity: 'warning',
245
+ objectKey: object.objectKey,
246
+ message: `duplicate metadata object key "${object.objectKey}" from ${existing.sourcePath ?? existing.sourceSystem ?? 'unknown'} and ${object.sourcePath ?? object.sourceSystem ?? 'unknown'}`,
247
+ });
248
+ objects.set(object.objectKey, mergeObject(existing, object));
249
+ }
250
+ else {
251
+ objects.set(object.objectKey, object);
252
+ }
253
+ }
254
+ addManifestBlockDetails(manifest, objects);
255
+ addDbtDagObjects(manifest, objects, edges, diagnostics);
256
+ addBlockDependencyEdges(manifest, edges);
257
+ const nodeKeyMap = new Map();
258
+ for (const node of [...manifestGraph.nodes, ...semanticGraph.nodes]) {
259
+ nodeKeyMap.set(node.nodeId, objectKeyFromKGNode(node));
260
+ }
261
+ for (const edge of [...manifestGraph.edges, ...semanticGraph.edges]) {
262
+ const fromKey = nodeKeyMap.get(edge.src) ?? edge.src;
263
+ const toKey = nodeKeyMap.get(edge.dst) ?? edge.dst;
264
+ const normalized = normalizeEdge(edge, fromKey, toKey);
265
+ const key = `${normalized.edgeType}\u0000${normalized.fromKey}\u0000${normalized.toKey}`;
266
+ if (!edges.has(key))
267
+ edges.set(key, normalized);
268
+ }
269
+ addProjectDiagnostics(manifest, semanticLayer, diagnostics);
270
+ const snapshot = {
271
+ projectRoot,
272
+ manifest,
273
+ objects: Array.from(objects.values()).sort((a, b) => a.objectKey.localeCompare(b.objectKey)),
274
+ edges: Array.from(edges.values()).sort((a, b) => `${a.edgeType}|${a.fromKey}|${a.toKey}`.localeCompare(`${b.edgeType}|${b.fromKey}|${b.toKey}`)),
275
+ diagnostics,
276
+ generatedAt: new Date().toISOString(),
277
+ fingerprint: '',
278
+ };
279
+ snapshot.fingerprint = fingerprintSnapshot(snapshot);
280
+ return snapshot;
281
+ }
282
+ export class MetadataCatalog {
283
+ dbPath;
284
+ db;
285
+ constructor(dbPath) {
286
+ this.dbPath = dbPath;
287
+ mkdirSync(dirname(dbPath), { recursive: true });
288
+ const Database = loadDatabase();
289
+ this.db = new Database(dbPath);
290
+ this.db.pragma('journal_mode = WAL');
291
+ this.db.pragma('foreign_keys = ON');
292
+ this.initSchema();
293
+ }
294
+ initSchema() {
295
+ this.db.exec(`
296
+ CREATE TABLE IF NOT EXISTS metadata_objects (
297
+ object_key TEXT PRIMARY KEY,
298
+ object_type TEXT NOT NULL,
299
+ name TEXT NOT NULL,
300
+ full_name TEXT,
301
+ domain TEXT,
302
+ owner TEXT,
303
+ status TEXT,
304
+ description TEXT,
305
+ source_path TEXT,
306
+ source_system TEXT,
307
+ payload_json TEXT NOT NULL DEFAULT '{}',
308
+ updated_at TEXT NOT NULL
309
+ );
310
+ CREATE INDEX IF NOT EXISTS idx_metadata_objects_type ON metadata_objects(object_type);
311
+ CREATE INDEX IF NOT EXISTS idx_metadata_objects_domain ON metadata_objects(domain);
312
+ CREATE INDEX IF NOT EXISTS idx_metadata_objects_status ON metadata_objects(status);
313
+
314
+ CREATE VIRTUAL TABLE IF NOT EXISTS metadata_fts USING fts5(
315
+ object_key UNINDEXED,
316
+ name,
317
+ full_name,
318
+ description,
319
+ domain,
320
+ owner,
321
+ payload,
322
+ tokenize = 'porter unicode61'
323
+ );
324
+
325
+ CREATE TABLE IF NOT EXISTS metadata_edges (
326
+ edge_type TEXT NOT NULL,
327
+ from_key TEXT NOT NULL,
328
+ to_key TEXT NOT NULL,
329
+ confidence REAL NOT NULL DEFAULT 1.0,
330
+ payload_json TEXT NOT NULL DEFAULT '{}',
331
+ created_at TEXT NOT NULL,
332
+ PRIMARY KEY (edge_type, from_key, to_key)
333
+ );
334
+ CREATE INDEX IF NOT EXISTS idx_metadata_edges_from ON metadata_edges(from_key, edge_type);
335
+ CREATE INDEX IF NOT EXISTS idx_metadata_edges_to ON metadata_edges(to_key, edge_type);
336
+
337
+ CREATE TABLE IF NOT EXISTS context_packs (
338
+ id TEXT PRIMARY KEY,
339
+ question TEXT NOT NULL,
340
+ focus_object_key TEXT,
341
+ mode TEXT NOT NULL,
342
+ trust_label TEXT NOT NULL,
343
+ payload_json TEXT NOT NULL DEFAULT '{}',
344
+ created_at TEXT NOT NULL
345
+ );
346
+ CREATE INDEX IF NOT EXISTS idx_context_packs_created ON context_packs(created_at DESC);
347
+ CREATE INDEX IF NOT EXISTS idx_context_packs_focus ON context_packs(focus_object_key);
348
+
349
+ CREATE TABLE IF NOT EXISTS query_runs (
350
+ id TEXT PRIMARY KEY,
351
+ object_key TEXT,
352
+ source TEXT NOT NULL,
353
+ status TEXT NOT NULL,
354
+ row_count INTEGER,
355
+ duration_ms INTEGER,
356
+ error_code TEXT,
357
+ payload_json TEXT NOT NULL DEFAULT '{}',
358
+ created_at TEXT NOT NULL
359
+ );
360
+ CREATE INDEX IF NOT EXISTS idx_query_runs_object ON query_runs(object_key, created_at DESC);
361
+ CREATE INDEX IF NOT EXISTS idx_query_runs_created ON query_runs(created_at DESC);
362
+
363
+ CREATE TABLE IF NOT EXISTS runtime_schema_snapshots (
364
+ id TEXT PRIMARY KEY,
365
+ source TEXT,
366
+ payload_json TEXT NOT NULL DEFAULT '{}',
367
+ captured_at TEXT NOT NULL
368
+ );
369
+ CREATE INDEX IF NOT EXISTS idx_runtime_schema_snapshots_captured ON runtime_schema_snapshots(captured_at DESC);
370
+
371
+ CREATE TABLE IF NOT EXISTS metadata_state (
372
+ key TEXT PRIMARY KEY,
373
+ value TEXT NOT NULL
374
+ );
375
+
376
+ CREATE TABLE IF NOT EXISTS metadata_diagnostics (
377
+ id TEXT PRIMARY KEY,
378
+ kind TEXT NOT NULL,
379
+ severity TEXT NOT NULL CHECK (severity IN ('error', 'warning')),
380
+ message TEXT NOT NULL,
381
+ object_key TEXT,
382
+ file_path TEXT,
383
+ created_at TEXT NOT NULL
384
+ );
385
+ CREATE INDEX IF NOT EXISTS idx_metadata_diagnostics_severity ON metadata_diagnostics(severity);
386
+ `);
387
+ }
388
+ rebuild(snapshot) {
389
+ const now = new Date().toISOString();
390
+ const insertObject = this.db.prepare(`
391
+ INSERT INTO metadata_objects (
392
+ object_key, object_type, name, full_name, domain, owner, status,
393
+ description, source_path, source_system, payload_json, updated_at
394
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
395
+ `);
396
+ const insertFts = this.db.prepare(`
397
+ INSERT INTO metadata_fts (object_key, name, full_name, description, domain, owner, payload)
398
+ VALUES (?, ?, ?, ?, ?, ?, ?)
399
+ `);
400
+ const insertEdge = this.db.prepare(`
401
+ INSERT OR IGNORE INTO metadata_edges (
402
+ edge_type, from_key, to_key, confidence, payload_json, created_at
403
+ ) VALUES (?, ?, ?, ?, ?, ?)
404
+ `);
405
+ const insertDiagnostic = this.db.prepare(`
406
+ INSERT INTO metadata_diagnostics (
407
+ id, kind, severity, message, object_key, file_path, created_at
408
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
409
+ `);
410
+ const setState = this.db.prepare(`
411
+ INSERT OR REPLACE INTO metadata_state (key, value) VALUES (?, ?)
412
+ `);
413
+ const txn = this.db.transaction(() => {
414
+ this.db.prepare('DELETE FROM metadata_edges').run();
415
+ this.db.prepare('DELETE FROM metadata_fts').run();
416
+ this.db.prepare('DELETE FROM metadata_objects').run();
417
+ this.db.prepare('DELETE FROM metadata_diagnostics').run();
418
+ for (const object of snapshot.objects) {
419
+ const payload = object.payload ?? {};
420
+ insertObject.run(object.objectKey, object.objectType, object.name, object.fullName ?? null, object.domain ?? null, object.owner ?? null, object.status ?? null, object.description ?? null, object.sourcePath ?? null, object.sourceSystem ?? null, JSON.stringify(payload), object.updatedAt ?? now);
421
+ insertFts.run(object.objectKey, object.name, object.fullName ?? '', object.description ?? '', object.domain ?? '', object.owner ?? '', JSON.stringify(payload));
422
+ }
423
+ for (const edge of snapshot.edges) {
424
+ insertEdge.run(edge.edgeType, edge.fromKey, edge.toKey, edge.confidence ?? 1, JSON.stringify(edge.payload ?? {}), now);
425
+ }
426
+ for (const diagnostic of snapshot.diagnostics) {
427
+ insertDiagnostic.run(diagnosticId(diagnostic), diagnostic.kind, diagnostic.severity, diagnostic.message, diagnostic.objectKey ?? null, diagnostic.filePath ?? null, now);
428
+ }
429
+ setState.run('built_at', now);
430
+ setState.run('fingerprint', snapshot.fingerprint);
431
+ setState.run('project_root', snapshot.projectRoot);
432
+ setState.run('object_count', String(snapshot.objects.length));
433
+ setState.run('edge_count', String(snapshot.edges.length));
434
+ setState.run('diagnostics_json', JSON.stringify(snapshot.diagnostics));
435
+ setState.run('manifest_generated_at', snapshot.manifest.generatedAt);
436
+ });
437
+ txn();
438
+ }
439
+ searchObjects(options) {
440
+ const { query, objectTypes, domain, limit = 40 } = options;
441
+ const sanitized = sanitizeFtsQuery(query);
442
+ if (!sanitized)
443
+ return this.listObjects({ objectTypes, domain, limit });
444
+ const filters = [];
445
+ const params = [sanitized];
446
+ if (objectTypes && objectTypes.length > 0) {
447
+ filters.push(`o.object_type IN (${objectTypes.map(() => '?').join(', ')})`);
448
+ params.push(...objectTypes);
449
+ }
450
+ if (domain) {
451
+ filters.push('o.domain = ?');
452
+ params.push(domain);
453
+ }
454
+ const whereExtra = filters.length > 0 ? ` AND ${filters.join(' AND ')}` : '';
455
+ const rows = this.db.prepare(`
456
+ SELECT o.*,
457
+ bm25(metadata_fts) AS rank,
458
+ snippet(metadata_fts, -1, '<mark>', '</mark>', '...', 12) AS snip
459
+ FROM metadata_fts
460
+ JOIN metadata_objects AS o ON o.object_key = metadata_fts.object_key
461
+ WHERE metadata_fts MATCH ?${whereExtra}
462
+ ORDER BY rank
463
+ LIMIT ?
464
+ `).all(...params, limit);
465
+ return rows.map((row) => ({
466
+ ...rowToObject(row),
467
+ score: row.rank ? 1 / (1 + Math.max(0, row.rank)) : 1,
468
+ snippet: row.snip ?? undefined,
469
+ }));
470
+ }
471
+ listObjects(options = {}) {
472
+ const filters = [];
473
+ const params = [];
474
+ if (options.objectTypes && options.objectTypes.length > 0) {
475
+ filters.push(`object_type IN (${options.objectTypes.map(() => '?').join(', ')})`);
476
+ params.push(...options.objectTypes);
477
+ }
478
+ if (options.domain) {
479
+ filters.push('domain = ?');
480
+ params.push(options.domain);
481
+ }
482
+ const where = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
483
+ const rows = this.db.prepare(`
484
+ SELECT * FROM metadata_objects
485
+ ${where}
486
+ ORDER BY updated_at DESC, name
487
+ LIMIT ?
488
+ `).all(...params, options.limit ?? 100);
489
+ return rows.map(rowToObject);
490
+ }
491
+ getObject(objectKey) {
492
+ const row = this.db.prepare('SELECT * FROM metadata_objects WHERE object_key = ?').get(objectKey);
493
+ return row ? rowToObject(row) : null;
494
+ }
495
+ getObjectsByKeys(keys) {
496
+ const unique = Array.from(new Set(keys.filter(Boolean)));
497
+ if (unique.length === 0)
498
+ return [];
499
+ const rows = [];
500
+ for (let i = 0; i < unique.length; i += 100) {
501
+ const chunk = unique.slice(i, i + 100);
502
+ const fetched = this.db.prepare(`
503
+ SELECT * FROM metadata_objects
504
+ WHERE object_key IN (${chunk.map(() => '?').join(', ')})
505
+ ORDER BY name
506
+ `).all(...chunk);
507
+ rows.push(...fetched.map(rowToObject));
508
+ }
509
+ return rows;
510
+ }
511
+ edgesForKeys(keys, hops = 1) {
512
+ let frontier = new Set(keys.filter(Boolean));
513
+ const seenKeys = new Set(frontier);
514
+ const edges = new Map();
515
+ for (let hop = 0; hop < hops && frontier.size > 0; hop += 1) {
516
+ const current = Array.from(frontier).slice(0, 100);
517
+ frontier = new Set();
518
+ const rows = this.db.prepare(`
519
+ SELECT * FROM metadata_edges
520
+ WHERE from_key IN (${current.map(() => '?').join(', ')})
521
+ OR to_key IN (${current.map(() => '?').join(', ')})
522
+ LIMIT 500
523
+ `).all(...current, ...current);
524
+ for (const row of rows) {
525
+ const edge = rowToEdge(row);
526
+ const edgeKey = `${edge.edgeType}\u0000${edge.fromKey}\u0000${edge.toKey}`;
527
+ if (!edges.has(edgeKey))
528
+ edges.set(edgeKey, edge);
529
+ for (const key of [edge.fromKey, edge.toKey]) {
530
+ if (!seenKeys.has(key)) {
531
+ seenKeys.add(key);
532
+ frontier.add(key);
533
+ }
534
+ }
535
+ }
536
+ }
537
+ return Array.from(edges.values());
538
+ }
539
+ insertContextPack(pack) {
540
+ const id = `ctx_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
541
+ const now = new Date().toISOString();
542
+ this.db.prepare(`
543
+ INSERT INTO context_packs (
544
+ id, question, focus_object_key, mode, trust_label, payload_json, created_at
545
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
546
+ `).run(id, pack.question, pack.focusObjectKey, pack.mode, pack.trustLabel, JSON.stringify(pack), now);
547
+ return id;
548
+ }
549
+ recordQueryRun(run) {
550
+ const id = run.id ?? `run_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
551
+ const createdAt = run.createdAt ?? new Date().toISOString();
552
+ this.db.prepare(`
553
+ INSERT OR REPLACE INTO query_runs (
554
+ id, object_key, source, status, row_count, duration_ms, error_code, payload_json, created_at
555
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
556
+ `).run(id, run.objectKey ?? null, run.source, run.status, run.rowCount ?? null, run.durationMs ?? null, run.errorCode ?? null, JSON.stringify(run.payload ?? {}), createdAt);
557
+ return { ...run, id, createdAt };
558
+ }
559
+ queryRunsForObjectKeys(keys, limit = 20) {
560
+ const unique = Array.from(new Set(keys.filter(Boolean)));
561
+ if (unique.length === 0)
562
+ return [];
563
+ const rows = this.db.prepare(`
564
+ SELECT * FROM query_runs
565
+ WHERE object_key IN (${unique.slice(0, 80).map(() => '?').join(', ')})
566
+ ORDER BY created_at DESC
567
+ LIMIT ?
568
+ `).all(...unique.slice(0, 80), limit);
569
+ return rows.map(rowToQueryRun);
570
+ }
571
+ recordRuntimeSchemaSnapshot(snapshot) {
572
+ const capturedAt = snapshot.capturedAt ?? new Date().toISOString();
573
+ const cleanSnapshot = {
574
+ source: snapshot.source,
575
+ capturedAt,
576
+ tables: normalizeRuntimeSchemaTables(snapshot.tables).slice(0, 500),
577
+ };
578
+ const id = `schema_${Date.parse(capturedAt) || Date.now()}`;
579
+ this.db.prepare(`
580
+ INSERT OR REPLACE INTO runtime_schema_snapshots (
581
+ id, source, payload_json, captured_at
582
+ ) VALUES (?, ?, ?, ?)
583
+ `).run(id, cleanSnapshot.source ?? null, JSON.stringify(cleanSnapshot), capturedAt);
584
+ return cleanSnapshot;
585
+ }
586
+ latestRuntimeSchemaSnapshot() {
587
+ const row = this.db.prepare(`
588
+ SELECT payload_json
589
+ FROM runtime_schema_snapshots
590
+ ORDER BY captured_at DESC
591
+ LIMIT 1
592
+ `).get();
593
+ return row ? safeRuntimeSchemaSnapshot(safeJson(row.payload_json, null)) : null;
594
+ }
595
+ getContextPack(id) {
596
+ const row = this.db.prepare('SELECT payload_json FROM context_packs WHERE id = ?').get(id);
597
+ if (!row)
598
+ return null;
599
+ const payload = safeJson(row.payload_json, null);
600
+ return payload ? { ...payload, id } : null;
601
+ }
602
+ objectCount() {
603
+ const row = this.db.prepare('SELECT COUNT(*) AS n FROM metadata_objects').get();
604
+ return row.n;
605
+ }
606
+ edgeCount() {
607
+ const row = this.db.prepare('SELECT COUNT(*) AS n FROM metadata_edges').get();
608
+ return row.n;
609
+ }
610
+ diagnostics() {
611
+ const rows = this.db.prepare(`
612
+ SELECT kind, severity, message, object_key, file_path
613
+ FROM metadata_diagnostics
614
+ ORDER BY severity, kind, message
615
+ `).all();
616
+ return rows.map((row) => ({
617
+ kind: row.kind,
618
+ severity: row.severity,
619
+ message: row.message,
620
+ objectKey: row.object_key ?? undefined,
621
+ filePath: row.file_path ?? undefined,
622
+ }));
623
+ }
624
+ state(key) {
625
+ const row = this.db.prepare('SELECT value FROM metadata_state WHERE key = ?').get(key);
626
+ return row?.value ?? null;
627
+ }
628
+ close() {
629
+ this.db.close();
630
+ }
631
+ }
632
+ function loadAgentManifest(projectRoot) {
633
+ return buildManifest({
634
+ projectRoot,
635
+ dbtManifestPath: resolveDbtManifestPath(projectRoot) ?? undefined,
636
+ });
637
+ }
638
+ function loadAgentSemanticLayer(projectRoot) {
639
+ try {
640
+ const config = loadProjectConfig(projectRoot);
641
+ const semanticConfig = config.semanticLayer?.provider
642
+ ? config.semanticLayer
643
+ : config.semanticLayer?.path
644
+ ? { provider: 'dql', path: config.semanticLayer.path }
645
+ : undefined;
646
+ const configured = resolveSemanticLayerWithDiagnostics(semanticConfig, projectRoot).layer;
647
+ if (configured)
648
+ return configured;
649
+ if (config.dbt?.projectDir) {
650
+ return resolveSemanticLayerWithDiagnostics({
651
+ provider: 'dbt',
652
+ projectPath: config.dbt.projectDir,
653
+ }, projectRoot).layer ?? undefined;
654
+ }
655
+ }
656
+ catch {
657
+ return undefined;
658
+ }
659
+ return undefined;
660
+ }
661
+ function manifestDiagnosticToMetadataDiagnostic(diagnostic) {
662
+ return {
663
+ kind: diagnostic.kind,
664
+ severity: diagnostic.severity,
665
+ message: diagnostic.message,
666
+ filePath: diagnostic.filePath,
667
+ };
668
+ }
669
+ function objectFromKGNode(node) {
670
+ const payload = {
671
+ kgNodeId: node.nodeId,
672
+ tags: node.tags ?? [],
673
+ examples: node.examples ?? [],
674
+ sourceTier: node.sourceTier,
675
+ certification: node.certification,
676
+ provenance: node.provenance,
677
+ freshness: node.freshness,
678
+ businessOutcome: node.businessOutcome,
679
+ businessOwner: node.businessOwner,
680
+ decisionUse: node.decisionUse,
681
+ reviewCadence: node.reviewCadence,
682
+ businessRules: node.businessRules ?? [],
683
+ caveats: node.caveats ?? [],
684
+ llmContext: node.llmContext,
685
+ };
686
+ return {
687
+ objectKey: objectKeyFromKGNode(node),
688
+ objectType: objectTypeFromKGNode(node),
689
+ name: node.name,
690
+ fullName: node.name,
691
+ domain: node.domain,
692
+ owner: node.owner,
693
+ status: node.status ?? node.certification,
694
+ description: node.description ?? node.llmContext,
695
+ sourcePath: node.sourcePath,
696
+ sourceSystem: node.provenance ?? node.sourceTier,
697
+ payload: compactObject(payload),
698
+ };
699
+ }
700
+ function objectKeyFromKGNode(node) {
701
+ switch (node.kind) {
702
+ case 'block': return `dql:block:${node.name}`;
703
+ case 'term': return `dql:term:${node.name}`;
704
+ case 'business_view': return `dql:business_view:${node.name}`;
705
+ case 'metric': return `semantic:metric:${node.name}`;
706
+ case 'dimension': return `semantic:dimension:${node.name}`;
707
+ case 'measure': return `semantic:measure:${node.name}`;
708
+ case 'entity': return `semantic:entity:${node.name}`;
709
+ case 'semantic_model': return `semantic:model:${node.name}`;
710
+ case 'saved_query': return `semantic:saved_query:${node.name}`;
711
+ case 'dbt_model': return `dbt:model:${node.name}`;
712
+ case 'dbt_source':
713
+ return node.sourceTier === 'dbt_manifest'
714
+ ? `dbt:source:${node.name}`
715
+ : `warehouse:table:${node.name}`;
716
+ case 'notebook': return `notebook:${node.name}`;
717
+ case 'dashboard': return `dashboard:${node.name}`;
718
+ case 'app': return `app:${node.name}`;
719
+ case 'domain': return `domain:${node.name}`;
720
+ default: return `${node.kind}:${node.name}`;
721
+ }
722
+ }
723
+ function objectTypeFromKGNode(node) {
724
+ switch (node.kind) {
725
+ case 'block': return 'dql_block';
726
+ case 'term': return 'dql_term';
727
+ case 'business_view': return 'business_view';
728
+ case 'metric': return 'semantic_metric';
729
+ case 'dimension': return 'semantic_dimension';
730
+ case 'measure': return 'semantic_measure';
731
+ case 'entity': return 'semantic_entity';
732
+ case 'semantic_model': return 'semantic_model';
733
+ case 'saved_query': return 'semantic_saved_query';
734
+ case 'dbt_model': return 'dbt_model';
735
+ case 'dbt_source': return node.sourceTier === 'dbt_manifest' ? 'dbt_source' : 'warehouse_table';
736
+ case 'notebook': return 'notebook';
737
+ case 'dashboard': return 'dashboard';
738
+ case 'app': return 'app';
739
+ case 'domain': return 'domain';
740
+ default: return node.kind;
741
+ }
742
+ }
743
+ function normalizeEdge(edge, fromKey, toKey) {
744
+ return {
745
+ edgeType: edge.kind,
746
+ fromKey,
747
+ toKey,
748
+ confidence: edge.weight ?? 1,
749
+ payload: { kgSource: edge.src, kgTarget: edge.dst },
750
+ };
751
+ }
752
+ function addDbtDagObjects(manifest, objects, edges, diagnostics) {
753
+ const models = manifest.dbtImport?.dbtDag?.models ?? [];
754
+ for (const model of models) {
755
+ const objectType = model.type === 'source' ? 'dbt_source' : 'dbt_model';
756
+ const objectKey = `dbt:${model.type}:${model.name}`;
757
+ const relation = [model.database, model.schema, model.name].filter(Boolean).join('.');
758
+ const object = {
759
+ objectKey,
760
+ objectType,
761
+ name: model.name,
762
+ fullName: relation || model.name,
763
+ description: model.description,
764
+ status: 'dbt_imported',
765
+ sourceSystem: 'dbt manifest.json',
766
+ payload: compactObject({
767
+ uniqueId: model.uniqueId,
768
+ relation,
769
+ database: model.database,
770
+ schema: model.schema,
771
+ materialized: model.materialized,
772
+ dependsOn: model.dependsOn,
773
+ columns: model.columns ?? [],
774
+ }),
775
+ };
776
+ objects.set(objectKey, mergeObject(objects.get(objectKey), object));
777
+ for (const column of model.columns ?? []) {
778
+ const columnKey = `dbt:column:${model.name}.${column.name}`;
779
+ objects.set(columnKey, mergeObject(objects.get(columnKey), {
780
+ objectKey: columnKey,
781
+ objectType: 'dbt_column',
782
+ name: column.name,
783
+ fullName: `${model.name}.${column.name}`,
784
+ description: column.description,
785
+ status: 'dbt_imported',
786
+ sourceSystem: 'dbt manifest.json',
787
+ payload: compactObject({
788
+ model: model.name,
789
+ uniqueId: model.uniqueId,
790
+ type: column.type,
791
+ relation,
792
+ }),
793
+ }));
794
+ const edge = {
795
+ edgeType: 'contains',
796
+ fromKey: objectKey,
797
+ toKey: columnKey,
798
+ confidence: 1,
799
+ payload: { source: 'dbt manifest column' },
800
+ };
801
+ edges.set(`${edge.edgeType}\u0000${edge.fromKey}\u0000${edge.toKey}`, edge);
802
+ }
803
+ for (const dep of model.dependsOn ?? []) {
804
+ const edge = {
805
+ edgeType: 'depends_on',
806
+ fromKey: objectKey,
807
+ toKey: dbtDependencyKey(dep),
808
+ confidence: 1,
809
+ payload: { source: 'dbt manifest depends_on', uniqueId: dep },
810
+ };
811
+ edges.set(`${edge.edgeType}\u0000${edge.fromKey}\u0000${edge.toKey}`, edge);
812
+ }
813
+ }
814
+ if (manifest.dbtImport && (manifest.dbtImport.totalDbtModels ?? 0) > 0 && manifest.dbtImport.modelsImported === 0) {
815
+ diagnostics.push({
816
+ kind: 'dbt',
817
+ severity: 'warning',
818
+ message: `dbt manifest loaded from ${manifest.dbtImport.manifestPath}, but 0 of ${manifest.dbtImport.totalDbtModels} models matched DQL table references. Check database/schema aliases or dbtImport anchors.`,
819
+ });
820
+ }
821
+ }
822
+ function addManifestBlockDetails(manifest, objects) {
823
+ for (const block of Object.values(manifest.blocks ?? {})) {
824
+ const objectKey = `dql:block:${block.name}`;
825
+ const existing = objects.get(objectKey);
826
+ if (!existing)
827
+ continue;
828
+ objects.set(objectKey, mergeObject(existing, {
829
+ ...existing,
830
+ payload: compactObject({
831
+ ...(existing.payload ?? {}),
832
+ sql: block.sql,
833
+ tableDependencies: block.tableDependencies,
834
+ rawTableRefs: block.rawTableRefs,
835
+ refDependencies: block.refDependencies,
836
+ metricRefs: block.metricRefs,
837
+ dimensionRefs: block.dimensionRefs,
838
+ chartType: block.chartType,
839
+ blockType: block.blockType,
840
+ tests: block.tests,
841
+ }),
842
+ }));
843
+ }
844
+ }
845
+ function addBlockDependencyEdges(manifest, edges) {
846
+ const dbtLookup = buildDbtModelLookup(manifest);
847
+ for (const block of Object.values(manifest.blocks ?? {})) {
848
+ const blockKey = `dql:block:${block.name}`;
849
+ for (const table of block.tableDependencies ?? []) {
850
+ const tableKey = `warehouse:table:${table}`;
851
+ putEdge(edges, {
852
+ edgeType: 'uses_table',
853
+ fromKey: blockKey,
854
+ toKey: tableKey,
855
+ confidence: 1,
856
+ payload: { source: 'dql block table dependency', tableReference: table },
857
+ });
858
+ const dbtKey = resolveDbtModelKeyForTable(table, dbtLookup);
859
+ if (!dbtKey)
860
+ continue;
861
+ putEdge(edges, {
862
+ edgeType: 'uses_dbt_model',
863
+ fromKey: blockKey,
864
+ toKey: dbtKey,
865
+ confidence: 0.86,
866
+ payload: { source: 'dql table dependency matched to dbt model', tableReference: table },
867
+ });
868
+ putEdge(edges, {
869
+ edgeType: 'maps_to_dbt_model',
870
+ fromKey: tableKey,
871
+ toKey: dbtKey,
872
+ confidence: 0.86,
873
+ payload: { source: 'warehouse table name matched to dbt model', tableReference: table },
874
+ });
875
+ }
876
+ }
877
+ }
878
+ function putEdge(edges, edge) {
879
+ edges.set(`${edge.edgeType}\u0000${edge.fromKey}\u0000${edge.toKey}`, edge);
880
+ }
881
+ function buildDbtModelLookup(manifest) {
882
+ const lookup = new Map();
883
+ const add = (key, objectKey) => {
884
+ const normalized = key.toLowerCase();
885
+ const existing = lookup.get(normalized);
886
+ if (existing)
887
+ existing.add(objectKey);
888
+ else
889
+ lookup.set(normalized, new Set([objectKey]));
890
+ };
891
+ for (const model of manifest.dbtImport?.dbtDag?.models ?? []) {
892
+ if (model.type !== 'model')
893
+ continue;
894
+ const objectKey = `dbt:model:${model.name}`;
895
+ for (const key of dbtModelLookupKeys(model.name, model.schema, model.database)) {
896
+ add(key, objectKey);
897
+ }
898
+ }
899
+ return lookup;
900
+ }
901
+ function resolveDbtModelKeyForTable(tableRef, lookup) {
902
+ for (const key of tableReferenceLookupKeys(tableRef)) {
903
+ const matches = lookup.get(key);
904
+ if (matches?.size === 1)
905
+ return [...matches][0];
906
+ }
907
+ return undefined;
908
+ }
909
+ function dbtModelLookupKeys(name, schema, database) {
910
+ const aliases = new Set([name.toLowerCase()]);
911
+ const stripped = stripCommonDbtPrefix(name);
912
+ if (stripped)
913
+ aliases.add(stripped);
914
+ const keys = new Set();
915
+ for (const alias of aliases) {
916
+ keys.add(alias);
917
+ if (schema)
918
+ keys.add(`${schema}.${alias}`.toLowerCase());
919
+ if (schema && database)
920
+ keys.add(`${database}.${schema}.${alias}`.toLowerCase());
921
+ }
922
+ return [...keys];
923
+ }
924
+ function tableReferenceLookupKeys(tableRef) {
925
+ const normalized = tableRef.replace(/["`]/g, '').toLowerCase();
926
+ const parts = normalized.split('.').filter(Boolean);
927
+ const keys = new Set([normalized]);
928
+ if (parts.length >= 2)
929
+ keys.add(parts.slice(-2).join('.'));
930
+ const last = parts.at(-1);
931
+ if (last)
932
+ keys.add(last);
933
+ return [...keys];
934
+ }
935
+ function stripCommonDbtPrefix(name) {
936
+ const match = /^(?:src|stg|int|dim|fct)_(.+)$/i.exec(name);
937
+ return match?.[1]?.toLowerCase();
938
+ }
939
+ function dbtDependencyKey(uniqueId) {
940
+ const parts = uniqueId.split('.');
941
+ const kind = parts[0] === 'source' ? 'source' : 'model';
942
+ const name = parts.at(-1) ?? uniqueId;
943
+ return `dbt:${kind}:${name}`;
944
+ }
945
+ function addProjectDiagnostics(manifest, semanticLayer, diagnostics) {
946
+ const semanticCount = Object.keys(manifest.metrics ?? {}).length +
947
+ Object.keys(manifest.dimensions ?? {}).length +
948
+ (semanticLayer?.listMetrics().length ?? 0) +
949
+ (semanticLayer?.listDimensions().length ?? 0) +
950
+ (semanticLayer?.listSemanticModels().length ?? 0);
951
+ if (semanticCount === 0) {
952
+ diagnostics.push({
953
+ kind: 'semantic',
954
+ severity: 'warning',
955
+ message: 'No semantic metrics or dimensions were found. Agents can use DQL/dbt/warehouse metadata, but semantic metric answers require metric definitions.',
956
+ });
957
+ }
958
+ }
959
+ function planContextPackRoute(input) {
960
+ const intent = input.request.intent ?? classifyMetadataIntent(input.request.question, input.request.followUp);
961
+ const exact = findExactCertifiedObject(input.request.question, intent, input.objects);
962
+ const missingContext = buildMissingContext(input.request, intent, input.objects, input.allowedSqlContext);
963
+ const selectedEvidence = input.evidenceRoles.slice(0, 16);
964
+ if (exact && (intent === 'exact_certified_lookup'
965
+ || intent === 'definition_lookup'
966
+ || (intent === 'ad_hoc_ranking' && objectNameInQuestion(input.request.question, exact)))) {
967
+ return {
968
+ route: 'certified',
969
+ intent,
970
+ reason: `Certified ${exact.objectType.replace(/_/g, ' ')} "${exact.name}" exactly matches the requested artifact, definition, or direct KPI grain.`,
971
+ trustLabel: 'certified',
972
+ reviewStatus: 'certified',
973
+ exactObjectKey: exact.objectKey,
974
+ selectedEvidence,
975
+ missingContext: [],
976
+ followUps: buildMetadataFollowUps(intent, input.allowedSqlContext),
977
+ };
978
+ }
979
+ if (intent === 'trust_gap_review') {
980
+ if (input.objects.length === 0) {
981
+ return clarifyDecision(intent, input.trustLabel, selectedEvidence, [{
982
+ kind: 'metadata',
983
+ severity: 'blocking',
984
+ message: 'No local metadata matched this trust question. Re-run dql compile or connect the relevant DQL/dbt project before reviewing trust.',
985
+ }]);
986
+ }
987
+ return {
988
+ route: 'research',
989
+ intent,
990
+ reason: 'Trust questions need a certification, lineage, owner, caveat, and diagnostic review rather than a metric SQL preview.',
991
+ trustLabel: input.trustLabel,
992
+ reviewStatus: 'needs_review',
993
+ selectedEvidence,
994
+ missingContext,
995
+ followUps: buildMetadataFollowUps(intent, input.allowedSqlContext),
996
+ };
997
+ }
998
+ if (intent === 'clarify' || missingContext.some((item) => item.severity === 'blocking')) {
999
+ return clarifyDecision(intent, input.trustLabel, selectedEvidence, missingContext);
1000
+ }
1001
+ const canGenerate = input.allowedSqlContext.relations.length > 0 ||
1002
+ input.allowedSqlContext.sourceBlockSql.length > 0 ||
1003
+ input.objects.some((object) => object.objectType.startsWith('semantic_'));
1004
+ if (canGenerate && isGeneratedMetadataIntent(intent)) {
1005
+ return {
1006
+ route: 'generated_sql',
1007
+ intent,
1008
+ reason: 'The question asks for a different grain, ranking, breakdown, comparison, entity drilldown, or diagnostic analysis, so certified artifacts are context only.',
1009
+ trustLabel: input.trustLabel === 'certified' ? 'mixed' : input.trustLabel,
1010
+ reviewStatus: 'draft_ready',
1011
+ selectedEvidence,
1012
+ missingContext,
1013
+ followUps: buildMetadataFollowUps(intent, input.allowedSqlContext),
1014
+ };
1015
+ }
1016
+ if (exact) {
1017
+ return {
1018
+ route: 'certified',
1019
+ intent: 'exact_certified_lookup',
1020
+ reason: `Certified artifact "${exact.name}" is the closest safe direct answer.`,
1021
+ trustLabel: 'certified',
1022
+ reviewStatus: 'certified',
1023
+ exactObjectKey: exact.objectKey,
1024
+ selectedEvidence,
1025
+ missingContext: [],
1026
+ followUps: buildMetadataFollowUps('exact_certified_lookup', input.allowedSqlContext),
1027
+ };
1028
+ }
1029
+ return clarifyDecision(intent, input.trustLabel, selectedEvidence, missingContext.length > 0 ? missingContext : [{
1030
+ kind: 'metadata',
1031
+ severity: 'blocking',
1032
+ message: 'The local metadata matched some context, but not enough to choose a safe metric, table, or grain.',
1033
+ }]);
1034
+ }
1035
+ function clarifyDecision(intent, trustLabel, selectedEvidence, missingContext) {
1036
+ return {
1037
+ route: 'clarify',
1038
+ intent,
1039
+ reason: 'DQL needs one more business or metadata detail before it can safely generate SQL.',
1040
+ trustLabel,
1041
+ reviewStatus: 'none',
1042
+ selectedEvidence,
1043
+ missingContext: missingContext.length > 0 ? missingContext : [{
1044
+ kind: 'metadata',
1045
+ severity: 'blocking',
1046
+ message: 'No certified block, semantic metric, dbt model, or runtime schema matched strongly enough to answer safely.',
1047
+ }],
1048
+ followUps: [
1049
+ 'Which metric should define the answer?',
1050
+ 'Which table or certified block should be used as the source?',
1051
+ 'What filter or time period should apply?',
1052
+ ],
1053
+ };
1054
+ }
1055
+ function classifyMetadataIntent(question, followUp) {
1056
+ const follow = followUp && typeof followUp === 'object' ? followUp : null;
1057
+ if (follow?.kind === 'drilldown')
1058
+ return 'entity_drilldown';
1059
+ const lower = question.toLowerCase();
1060
+ if (/\b(trust|rely|certif|lineage|owner|caveat|gap|governance)\b/.test(lower))
1061
+ return 'trust_gap_review';
1062
+ if (/\b(define|definition|meaning of|what is|what are|what does .+ mean)\b/.test(lower))
1063
+ return 'definition_lookup';
1064
+ if (/\b(anomal|exception|outlier|spike|dip)\b/.test(lower))
1065
+ return 'anomaly_investigation';
1066
+ if (/\b(compare|versus|vs\.?|segment|cohort)\b/.test(lower))
1067
+ return 'segment_compare';
1068
+ if (/\b(why|changed?|change|drop|dropped|decline|declined|increase|increased|decrease|decreased|delta|variance|what happened)\b/.test(lower))
1069
+ return 'diagnose_change';
1070
+ if (/\b(driver|drivers|drove|break\s*down|breakdown|contribute|contribution|top movers?)\b/.test(lower))
1071
+ return 'driver_breakdown';
1072
+ if (isEntityQuestion(question))
1073
+ return 'entity_drilldown';
1074
+ if (/\b(top|bottom|best|worst|highest|lowest|least|fewest|minimum|min|maximum|max|rank|ranking|most)\b/.test(lower))
1075
+ return 'ad_hoc_ranking';
1076
+ if (/\b(block|certified|saved|existing|approved|governed)\b/.test(lower))
1077
+ return 'exact_certified_lookup';
1078
+ if (/\b(show|list|find|which|who|how many|how much|metric|kpi|dashboard|performance|revenue|sales|points|goals|orders|customers|users)\b/.test(lower))
1079
+ return 'ad_hoc_ranking';
1080
+ return 'clarify';
1081
+ }
1082
+ function isGeneratedMetadataIntent(intent) {
1083
+ return intent === 'ad_hoc_ranking'
1084
+ || intent === 'driver_breakdown'
1085
+ || intent === 'diagnose_change'
1086
+ || intent === 'segment_compare'
1087
+ || intent === 'entity_drilldown'
1088
+ || intent === 'anomaly_investigation';
1089
+ }
1090
+ function findExactCertifiedObject(question, intent, objects) {
1091
+ const candidates = objects.filter((object) => isCertifiedMetadataObject(object));
1092
+ if (intent === 'definition_lookup') {
1093
+ return candidates.find((object) => object.objectType === 'dql_term' || object.objectType === 'business_view')
1094
+ ?? candidates.find((object) => object.objectType === 'dql_block' && objectNameInQuestion(question, object));
1095
+ }
1096
+ const namedExact = candidates.find((object) => object.objectType === 'dql_block' &&
1097
+ objectNameInQuestion(question, object) &&
1098
+ hasCompatibleMetadataRankingDirection(question, object));
1099
+ if (namedExact)
1100
+ return namedExact;
1101
+ if (isGeneratedMetadataIntent(intent))
1102
+ return undefined;
1103
+ return candidates.find((object) => object.objectType === 'dql_block' &&
1104
+ objectNameInQuestion(question, object) &&
1105
+ hasCompatibleMetadataRankingDirection(question, object)) ?? candidates.find((object) => object.objectType === 'dql_block' &&
1106
+ hasMeaningfulObjectOverlap(question, object) &&
1107
+ hasCompatibleMetadataRankingDirection(question, object) &&
1108
+ !looksLikeDifferentGrainQuestion(question));
1109
+ }
1110
+ function isCertifiedMetadataObject(object) {
1111
+ return object.status === 'certified' || object.status === 'approved' || object.payload?.certification === 'certified';
1112
+ }
1113
+ function objectNameInQuestion(question, object) {
1114
+ const q = normalizeSearchText(question);
1115
+ const name = normalizeSearchText(object.name);
1116
+ const fullName = normalizeSearchText(object.fullName ?? '');
1117
+ return Boolean(name && q.includes(name)) || Boolean(fullName && q.includes(fullName));
1118
+ }
1119
+ function hasMeaningfulObjectOverlap(question, object) {
1120
+ const terms = new Set(tokenize(question));
1121
+ if (terms.size === 0)
1122
+ return false;
1123
+ const haystack = tokenize([
1124
+ object.name,
1125
+ object.fullName ?? '',
1126
+ object.domain ?? '',
1127
+ object.description ?? '',
1128
+ JSON.stringify(object.payload ?? {}),
1129
+ ].join(' '));
1130
+ return haystack.some((term) => terms.has(term));
1131
+ }
1132
+ function looksLikeDifferentGrainQuestion(question) {
1133
+ return /\b(for|where|only|specific|single|individual|named|called|by|break\s*down|breakdown|drill|compare|versus|vs\.?|segment|least|lowest|fewest|bottom|why|changed?|driver|anomal|exception)\b/i.test(question);
1134
+ }
1135
+ function hasCompatibleMetadataRankingDirection(question, object) {
1136
+ const questionDirection = rankingDirection(question);
1137
+ if (!questionDirection)
1138
+ return true;
1139
+ const objectDirection = rankingDirection([
1140
+ object.name,
1141
+ object.description ?? '',
1142
+ JSON.stringify(object.payload ?? {}),
1143
+ ].join(' '));
1144
+ if (!objectDirection)
1145
+ return true;
1146
+ return questionDirection === objectDirection;
1147
+ }
1148
+ function rankingDirection(text) {
1149
+ const lower = text.toLowerCase();
1150
+ const bottom = /\b(bottom|least|fewest|lowest|minimum|min|smallest|worst|underperform(?:ing|ed|er|ers)?)\b/.test(lower);
1151
+ const top = /\b(top|most|highest|maximum|max|greatest|best|leader|leaders|leading)\b/.test(lower);
1152
+ if (bottom && !top)
1153
+ return 'bottom';
1154
+ if (top && !bottom)
1155
+ return 'top';
1156
+ return undefined;
1157
+ }
1158
+ function buildMissingContext(request, intent, objects, allowedSqlContext) {
1159
+ const missing = [];
1160
+ const hasSqlContext = allowedSqlContext.relations.length > 0 || allowedSqlContext.sourceBlockSql.length > 0;
1161
+ if (isGeneratedMetadataIntent(intent) && !hasSqlContext && !objects.some((object) => object.objectType.startsWith('semantic_'))) {
1162
+ missing.push({
1163
+ kind: 'table',
1164
+ severity: 'blocking',
1165
+ message: 'No dbt model, warehouse/runtime table, semantic metric, or certified block SQL was available for this generated answer.',
1166
+ });
1167
+ }
1168
+ if (intent === 'diagnose_change' && !hasComparableBaselineContext(request, objects, allowedSqlContext)) {
1169
+ missing.push({
1170
+ kind: 'baseline',
1171
+ severity: 'blocking',
1172
+ message: 'No comparable time/baseline field or selected tile history was found, so DQL needs the baseline period before explaining what changed.',
1173
+ });
1174
+ }
1175
+ if ((intent === 'definition_lookup' || intent === 'trust_gap_review') && objects.length === 0) {
1176
+ missing.push({
1177
+ kind: 'metadata',
1178
+ severity: 'blocking',
1179
+ message: 'No certified term, business view, block, dashboard, app, or lineage metadata matched this question.',
1180
+ });
1181
+ }
1182
+ return missing;
1183
+ }
1184
+ function hasComparableBaselineContext(request, objects, allowedSqlContext) {
1185
+ if (request.focusObjectKey) {
1186
+ const focus = objects.find((object) => object.objectKey === request.focusObjectKey);
1187
+ if (focus) {
1188
+ const dependencyKeys = new Set([
1189
+ ...metadataStringArray(focus.payload?.tableDependencies),
1190
+ ...metadataStringArray(focus.payload?.rawTableRefs),
1191
+ ].flatMap((relation) => relationLookupKeysForCatalog(relation)));
1192
+ const focusedRelations = dependencyKeys.size > 0
1193
+ ? allowedSqlContext.relations.filter((relation) => relationLookupKeysForCatalog(relation.relation).some((key) => dependencyKeys.has(key)))
1194
+ : [];
1195
+ const focusTextHasBaseline = /\b(date|time|day|week|month|quarter|year|season|period|baseline|history|snapshot)\b/i.test([
1196
+ focus.name,
1197
+ focus.description ?? '',
1198
+ String(focus.payload?.sql ?? ''),
1199
+ ].join(' '));
1200
+ return focusTextHasBaseline || focusedRelations.some((relation) => relation.columns.some((column) => isTimeLikeColumn(column.name)));
1201
+ }
1202
+ }
1203
+ if (allowedSqlContext.relations.some((relation) => relation.columns.some((column) => isTimeLikeColumn(column.name))))
1204
+ return true;
1205
+ const rows = selectedRows(request.selectedContext);
1206
+ if (rows.length < 2)
1207
+ return false;
1208
+ const columns = new Set(rows.flatMap((row) => Object.keys(row)));
1209
+ for (const column of columns) {
1210
+ if (!isTimeLikeColumn(column))
1211
+ continue;
1212
+ const values = new Set(rows.map((row) => row[column]).filter((value) => value !== null && value !== undefined).map(String));
1213
+ if (values.size >= 2)
1214
+ return true;
1215
+ }
1216
+ return false;
1217
+ }
1218
+ function relationLookupKeysForCatalog(relation) {
1219
+ const normalized = normalizeRelationKey(relation);
1220
+ const parts = normalized.split('.').filter(Boolean);
1221
+ const keys = new Set();
1222
+ if (normalized)
1223
+ keys.add(normalized);
1224
+ if (parts.length >= 2)
1225
+ keys.add(parts.slice(-2).join('.'));
1226
+ if (parts.length >= 1)
1227
+ keys.add(parts[parts.length - 1]);
1228
+ return Array.from(keys);
1229
+ }
1230
+ function isTimeLikeColumn(name) {
1231
+ return /\b(date|time|day|week|month|quarter|year|season|period|created_at|updated_at)\b/i.test(name);
1232
+ }
1233
+ function selectedRows(value) {
1234
+ const root = normalizeSelectedContextRoot(value);
1235
+ const selected = root?.selectedBlock && typeof root.selectedBlock === 'object' ? root.selectedBlock : root;
1236
+ const candidates = [
1237
+ selected?.resultSample,
1238
+ selected?.rows,
1239
+ selected?.sampleRows,
1240
+ root?.resultSample,
1241
+ root?.rows,
1242
+ ];
1243
+ for (const candidate of candidates) {
1244
+ if (!Array.isArray(candidate))
1245
+ continue;
1246
+ return candidate.filter((row) => Boolean(row) && typeof row === 'object' && !Array.isArray(row));
1247
+ }
1248
+ return [];
1249
+ }
1250
+ function buildMetadataFollowUps(intent, allowedSqlContext) {
1251
+ if (intent === 'clarify')
1252
+ return ['Pick the metric', 'Pick the source table', 'Pick the time window'];
1253
+ if (intent === 'trust_gap_review')
1254
+ return ['Show lineage', 'List caveats', 'Create a certified replacement block'];
1255
+ const relation = allowedSqlContext.relations[0];
1256
+ const dimension = relation?.columns.find((column) => /(segment|type|name|team|player|customer|product|region)/i.test(column.name))?.name ?? 'segment';
1257
+ return [
1258
+ `Break down by ${dimension}`,
1259
+ 'Show the trend over time',
1260
+ 'Save as a draft block for review',
1261
+ ];
1262
+ }
1263
+ function buildEvidenceRoles(objects, queryRuns) {
1264
+ const roles = objects.slice(0, 40).map((object) => ({
1265
+ objectKey: object.objectKey,
1266
+ objectType: object.objectType,
1267
+ name: object.name,
1268
+ role: evidenceRoleForObject(object),
1269
+ reason: evidenceReasonForObject(object),
1270
+ }));
1271
+ for (const run of queryRuns.slice(0, 8)) {
1272
+ if (!run.objectKey)
1273
+ continue;
1274
+ roles.push({
1275
+ objectKey: run.objectKey,
1276
+ objectType: 'query_run',
1277
+ name: run.payload?.question && typeof run.payload.question === 'string' ? run.payload.question : run.id,
1278
+ role: 'prior_query_run',
1279
+ reason: `Prior query run status: ${run.status}`,
1280
+ });
1281
+ }
1282
+ return roles;
1283
+ }
1284
+ function evidenceRoleForObject(object) {
1285
+ if (object.objectType === 'dql_block' && isCertifiedMetadataObject(object))
1286
+ return 'certified_context';
1287
+ if (object.objectType === 'semantic_metric')
1288
+ return 'semantic_metric';
1289
+ if (object.objectType === 'dql_term' || object.objectType === 'business_view')
1290
+ return 'business_context';
1291
+ if (object.objectType === 'dbt_model' || object.objectType === 'dbt_source' || object.objectType === 'dbt_column')
1292
+ return 'dbt_model';
1293
+ if (object.objectType === 'warehouse_table')
1294
+ return 'warehouse_schema';
1295
+ if (object.objectType === 'runtime_table' || object.objectType === 'runtime_column')
1296
+ return 'runtime_schema';
1297
+ if (object.objectType === 'selected_context')
1298
+ return 'selected_context';
1299
+ if (object.objectType === 'skill')
1300
+ return 'skill_guidance';
1301
+ return 'other';
1302
+ }
1303
+ function evidenceReasonForObject(object) {
1304
+ if (object.objectType === 'dql_block' && isCertifiedMetadataObject(object))
1305
+ return 'Certified block can be exact answer only when grain matches; otherwise it is context.';
1306
+ if (object.objectType.startsWith('semantic_'))
1307
+ return 'Semantic definition can ground metric and dimension meaning.';
1308
+ if (object.objectType.startsWith('dbt_'))
1309
+ return 'dbt metadata supplies physical model and column context.';
1310
+ if (object.objectType === 'runtime_table' || object.objectType === 'runtime_column')
1311
+ return 'Runtime schema supplies executable table and column context.';
1312
+ return reasonForObject(object);
1313
+ }
1314
+ function buildAllowedSqlContext(objects, edges) {
1315
+ const byRelation = new Map();
1316
+ const addRelation = (relation) => {
1317
+ const key = normalizeRelationKey(relation.relation);
1318
+ if (!key)
1319
+ return;
1320
+ const existing = byRelation.get(key);
1321
+ if (!existing) {
1322
+ byRelation.set(key, { ...relation, columns: dedupeRuntimeColumns(relation.columns).slice(0, 120) });
1323
+ return;
1324
+ }
1325
+ byRelation.set(key, {
1326
+ ...existing,
1327
+ objectKey: existing.objectKey ?? relation.objectKey,
1328
+ source: existing.source === relation.source ? existing.source : 'local metadata catalog',
1329
+ columns: dedupeRuntimeColumns([...existing.columns, ...relation.columns]).slice(0, 120),
1330
+ });
1331
+ };
1332
+ for (const object of objects) {
1333
+ const relation = metadataRelationFromObject(object);
1334
+ if (relation)
1335
+ addRelation(relation);
1336
+ for (const table of metadataStringArray(object.payload?.tableDependencies)) {
1337
+ addRelation({
1338
+ relation: table,
1339
+ name: table.split('.').at(-1) ?? table,
1340
+ objectKey: object.objectKey,
1341
+ source: 'certified block dependency',
1342
+ columns: [],
1343
+ });
1344
+ }
1345
+ for (const table of metadataStringArray(object.payload?.rawTableRefs)) {
1346
+ addRelation({
1347
+ relation: table,
1348
+ name: table.split('.').at(-1) ?? table,
1349
+ objectKey: object.objectKey,
1350
+ source: 'certified block SQL reference',
1351
+ columns: [],
1352
+ });
1353
+ }
1354
+ }
1355
+ const objectsByKey = new Map(objects.map((object) => [object.objectKey, object]));
1356
+ for (const edge of edges) {
1357
+ if (edge.edgeType !== 'maps_to_dbt_model' && edge.edgeType !== 'uses_dbt_model')
1358
+ continue;
1359
+ const from = objectsByKey.get(edge.fromKey);
1360
+ const to = objectsByKey.get(edge.toKey);
1361
+ const fromRelation = from ? metadataRelationFromObject(from) : null;
1362
+ const toRelation = to ? metadataRelationFromObject(to) : null;
1363
+ if (fromRelation && toRelation)
1364
+ addRelation({ ...fromRelation, columns: toRelation.columns, source: 'dbt mapped warehouse table' });
1365
+ }
1366
+ return {
1367
+ relations: Array.from(byRelation.values()).sort((a, b) => a.relation.localeCompare(b.relation)),
1368
+ sourceBlockSql: objects
1369
+ .filter((object) => object.objectType === 'dql_block' && typeof object.payload?.sql === 'string' && object.payload.sql.trim())
1370
+ .slice(0, 8)
1371
+ .map((object) => ({
1372
+ objectKey: object.objectKey,
1373
+ name: object.name,
1374
+ status: object.status,
1375
+ sql: String(object.payload?.sql ?? ''),
1376
+ })),
1377
+ };
1378
+ }
1379
+ function metadataRelationFromObject(object) {
1380
+ if (object.objectType === 'dbt_column' || object.objectType === 'runtime_column') {
1381
+ const relation = metadataPayloadString(object, 'relation');
1382
+ if (!relation)
1383
+ return null;
1384
+ return {
1385
+ relation,
1386
+ name: relation.split('.').at(-1) ?? relation,
1387
+ objectKey: object.objectKey,
1388
+ source: object.objectType === 'runtime_column' ? 'runtime schema snapshot' : 'dbt manifest',
1389
+ columns: [{
1390
+ name: object.name,
1391
+ type: metadataPayloadString(object, 'type') ?? metadataPayloadString(object, 'data_type'),
1392
+ description: object.description,
1393
+ sampleValues: metadataStringArray(object.payload?.sampleValues),
1394
+ }],
1395
+ };
1396
+ }
1397
+ if (!['dbt_model', 'dbt_source', 'warehouse_table', 'runtime_table'].includes(object.objectType))
1398
+ return null;
1399
+ const relation = metadataPayloadString(object, 'relation') ?? object.fullName ?? object.name;
1400
+ return {
1401
+ relation,
1402
+ name: relation.split('.').at(-1) ?? object.name,
1403
+ objectKey: object.objectKey,
1404
+ source: object.objectType === 'runtime_table' ? 'runtime schema snapshot' : object.sourceSystem ?? 'local metadata catalog',
1405
+ columns: metadataRuntimeColumns(object.payload?.columns),
1406
+ };
1407
+ }
1408
+ function runtimeSchemaObjects(snapshot) {
1409
+ const capturedAt = snapshot.capturedAt ?? new Date().toISOString();
1410
+ return normalizeRuntimeSchemaTables(snapshot.tables).flatMap((table) => {
1411
+ const relation = table.relation;
1412
+ const tableKey = `runtime:table:${relation}`;
1413
+ const tableObject = {
1414
+ objectKey: tableKey,
1415
+ objectType: 'runtime_table',
1416
+ name: table.name ?? relation.split('.').at(-1) ?? relation,
1417
+ fullName: relation,
1418
+ description: table.description,
1419
+ status: 'runtime_observed',
1420
+ sourceSystem: snapshot.source ?? table.source ?? 'runtime schema snapshot',
1421
+ payload: compactObject({
1422
+ relation,
1423
+ schema: table.schema,
1424
+ columns: table.columns,
1425
+ }),
1426
+ updatedAt: capturedAt,
1427
+ };
1428
+ const columns = table.columns.map((column) => ({
1429
+ objectKey: `runtime:column:${relation}.${column.name}`,
1430
+ objectType: 'runtime_column',
1431
+ name: column.name,
1432
+ fullName: `${relation}.${column.name}`,
1433
+ description: column.description,
1434
+ status: 'runtime_observed',
1435
+ sourceSystem: snapshot.source ?? table.source ?? 'runtime schema snapshot',
1436
+ payload: compactObject({
1437
+ relation,
1438
+ type: column.type,
1439
+ sampleValues: column.sampleValues,
1440
+ }),
1441
+ updatedAt: capturedAt,
1442
+ }));
1443
+ return [tableObject, ...columns];
1444
+ });
1445
+ }
1446
+ function selectedContextObjects(value) {
1447
+ const root = normalizeSelectedContextRoot(value);
1448
+ if (!root)
1449
+ return [];
1450
+ const selected = root.selectedBlock && typeof root.selectedBlock === 'object' ? root.selectedBlock : root;
1451
+ const title = stringValue(selected.title) ?? stringValue(root.dashboardTitle) ?? stringValue(root.title) ?? 'Selected app context';
1452
+ const objectKey = `selected:context:${sha256(stableStringify(selected)).slice(0, 16)}`;
1453
+ return [{
1454
+ objectKey,
1455
+ objectType: 'selected_context',
1456
+ name: title,
1457
+ description: stringValue(selected.description) ?? stringValue(root.question),
1458
+ status: stringValue(selected.certificationStatus) ?? stringValue(selected.reviewStatus),
1459
+ sourceSystem: 'selected app/notebook context',
1460
+ payload: compactObject({
1461
+ tileId: selected.tileId,
1462
+ blockId: selected.blockId,
1463
+ blockPath: selected.blockPath,
1464
+ dashboardTitle: root.dashboardTitle,
1465
+ rowCount: selected.rowCount,
1466
+ columns: selected.columns,
1467
+ rows: selectedRows(root).slice(0, 20),
1468
+ }),
1469
+ }];
1470
+ }
1471
+ function selectedContextReferenceKeys(value, selectedObjects) {
1472
+ const root = normalizeSelectedContextRoot(value);
1473
+ const keys = new Set();
1474
+ const addBlock = (blockId) => {
1475
+ if (typeof blockId !== 'string')
1476
+ return;
1477
+ const name = blockId.replace(/^block:/i, '').trim();
1478
+ if (name)
1479
+ keys.add(`dql:block:${name}`);
1480
+ };
1481
+ if (root) {
1482
+ const selected = root.selectedBlock && typeof root.selectedBlock === 'object' ? root.selectedBlock : root;
1483
+ addBlock(selected.blockId);
1484
+ addBlock(root.sourceBlockId);
1485
+ addBlock(root.blockId);
1486
+ }
1487
+ for (const object of selectedObjects) {
1488
+ addBlock(object.payload?.blockId);
1489
+ }
1490
+ return Array.from(keys);
1491
+ }
1492
+ function normalizeSelectedContextRoot(value) {
1493
+ if (!value || typeof value !== 'object' || Array.isArray(value))
1494
+ return null;
1495
+ const original = value;
1496
+ const fromSql = typeof original.sql === 'string' ? parseJsonObject(original.sql) : null;
1497
+ const root = fromSql ?? original;
1498
+ const nested = root.context && typeof root.context === 'object' && !Array.isArray(root.context)
1499
+ ? root.context
1500
+ : null;
1501
+ if (!nested)
1502
+ return root;
1503
+ return {
1504
+ ...root,
1505
+ ...nested,
1506
+ requestTitle: root.title,
1507
+ requestIntent: root.intent,
1508
+ requestMode: root.mode,
1509
+ context: nested,
1510
+ };
1511
+ }
1512
+ function parseJsonObject(raw) {
1513
+ const trimmed = raw.trim();
1514
+ if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('[')))
1515
+ return null;
1516
+ try {
1517
+ const parsed = JSON.parse(trimmed);
1518
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
1519
+ ? parsed
1520
+ : null;
1521
+ }
1522
+ catch {
1523
+ return null;
1524
+ }
1525
+ }
1526
+ function normalizeRuntimeSchemaTables(tables) {
1527
+ const byRelation = new Map();
1528
+ for (const table of tables ?? []) {
1529
+ if (!table?.relation)
1530
+ continue;
1531
+ const relation = table.relation.trim();
1532
+ if (!relation)
1533
+ continue;
1534
+ const key = normalizeRelationKey(relation);
1535
+ const current = byRelation.get(key);
1536
+ const normalized = {
1537
+ relation,
1538
+ schema: table.schema,
1539
+ name: table.name ?? relation.split('.').at(-1) ?? relation,
1540
+ description: table.description,
1541
+ source: table.source,
1542
+ columns: dedupeRuntimeColumns(table.columns ?? []).slice(0, 160),
1543
+ };
1544
+ if (!current)
1545
+ byRelation.set(key, normalized);
1546
+ else
1547
+ byRelation.set(key, {
1548
+ ...current,
1549
+ description: current.description ?? normalized.description,
1550
+ columns: dedupeRuntimeColumns([...current.columns, ...normalized.columns]).slice(0, 160),
1551
+ });
1552
+ }
1553
+ return Array.from(byRelation.values());
1554
+ }
1555
+ function safeRuntimeSchemaSnapshot(value) {
1556
+ if (!value || typeof value !== 'object')
1557
+ return null;
1558
+ const record = value;
1559
+ if (!Array.isArray(record.tables))
1560
+ return null;
1561
+ return {
1562
+ source: stringValue(record.source),
1563
+ capturedAt: stringValue(record.capturedAt),
1564
+ tables: normalizeRuntimeSchemaTables(record.tables),
1565
+ };
1566
+ }
1567
+ function metadataRuntimeColumns(value) {
1568
+ if (!Array.isArray(value))
1569
+ return [];
1570
+ return value.flatMap((entry) => {
1571
+ if (!entry || typeof entry !== 'object')
1572
+ return [];
1573
+ const record = entry;
1574
+ const name = stringValue(record.name) ?? stringValue(record.column_name);
1575
+ if (!name)
1576
+ return [];
1577
+ return [{
1578
+ name,
1579
+ type: stringValue(record.type) ?? stringValue(record.data_type),
1580
+ description: stringValue(record.description),
1581
+ sampleValues: metadataStringArray(record.sampleValues),
1582
+ }];
1583
+ });
1584
+ }
1585
+ function dedupeRuntimeColumns(columns) {
1586
+ const byName = new Map();
1587
+ for (const column of columns) {
1588
+ if (!column?.name)
1589
+ continue;
1590
+ const key = column.name.toLowerCase();
1591
+ const existing = byName.get(key);
1592
+ byName.set(key, existing ? {
1593
+ ...existing,
1594
+ type: existing.type ?? column.type,
1595
+ description: existing.description ?? column.description,
1596
+ sampleValues: Array.from(new Set([...(existing.sampleValues ?? []), ...(column.sampleValues ?? [])])).slice(0, 8),
1597
+ } : {
1598
+ ...column,
1599
+ sampleValues: column.sampleValues?.slice(0, 8),
1600
+ });
1601
+ }
1602
+ return Array.from(byName.values());
1603
+ }
1604
+ function metadataStringArray(value) {
1605
+ if (!Array.isArray(value))
1606
+ return [];
1607
+ return value.filter((item) => typeof item === 'string' && item.trim().length > 0).map((item) => item.trim());
1608
+ }
1609
+ function metadataPayloadString(object, key) {
1610
+ return stringValue(object.payload?.[key]);
1611
+ }
1612
+ function stringValue(value) {
1613
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
1614
+ }
1615
+ function normalizeRelationKey(relation) {
1616
+ return relation.replace(/["`]/g, '').trim().toLowerCase();
1617
+ }
1618
+ function normalizeSearchText(value) {
1619
+ return value.toLowerCase().replace(/[_-]+/g, ' ').replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
1620
+ }
1621
+ function isEntityQuestion(question) {
1622
+ const lower = question.toLowerCase();
1623
+ if (/\b(for|where|only|specific|single|individual|named|called)\b.+\b(account|accounts|customer|customers|player|players|product|products|sku|user|users|team|teams)\b/i.test(lower))
1624
+ return true;
1625
+ if (/\b(account|customer|player|product|sku|user|team)\s+(?:id|name|email)\b/i.test(lower))
1626
+ return true;
1627
+ return /[A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,3}/.test(question)
1628
+ && /\b(revenue|sales|order|orders|spend|value|churn|usage|activity|performance|performed|metric|kpi|points|goals|assists|scoring)\b/i.test(lower);
1629
+ }
1630
+ function mergeObject(a, b) {
1631
+ if (!a)
1632
+ return b;
1633
+ return {
1634
+ ...a,
1635
+ ...b,
1636
+ score: Math.max(a.score ?? 0, b.score ?? 0) || undefined,
1637
+ description: b.description || a.description,
1638
+ payload: compactObject({ ...(a.payload ?? {}), ...(b.payload ?? {}) }),
1639
+ };
1640
+ }
1641
+ function fingerprintSnapshot(snapshot) {
1642
+ return sha256(stableStringify({
1643
+ projectRoot: snapshot.projectRoot,
1644
+ manifest: sanitizeManifestForFingerprint(snapshot.manifest),
1645
+ objects: snapshot.objects.map((object) => ({
1646
+ ...object,
1647
+ score: undefined,
1648
+ snippet: undefined,
1649
+ })),
1650
+ edges: snapshot.edges,
1651
+ diagnostics: snapshot.diagnostics,
1652
+ }));
1653
+ }
1654
+ function sanitizeManifestForFingerprint(manifest) {
1655
+ return {
1656
+ ...manifest,
1657
+ generatedAt: undefined,
1658
+ dbtImport: manifest.dbtImport
1659
+ ? { ...manifest.dbtImport, importedAt: undefined }
1660
+ : undefined,
1661
+ };
1662
+ }
1663
+ function sha256(value) {
1664
+ return createHash('sha256').update(value).digest('hex');
1665
+ }
1666
+ function diagnosticId(diagnostic) {
1667
+ return sha256(stableStringify(diagnostic)).slice(0, 24);
1668
+ }
1669
+ function stableStringify(value, seen = new WeakSet()) {
1670
+ if (value === undefined)
1671
+ return 'null';
1672
+ if (value === null || typeof value !== 'object')
1673
+ return JSON.stringify(value);
1674
+ if (seen.has(value))
1675
+ return '"[Circular]"';
1676
+ seen.add(value);
1677
+ if (Array.isArray(value)) {
1678
+ const out = `[${value.map((item) => stableStringify(item, seen)).join(',')}]`;
1679
+ seen.delete(value);
1680
+ return out;
1681
+ }
1682
+ const record = value;
1683
+ const out = `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key], seen)}`).join(',')}}`;
1684
+ seen.delete(value);
1685
+ return out;
1686
+ }
1687
+ function compactObject(value) {
1688
+ const out = {};
1689
+ for (const [key, raw] of Object.entries(value)) {
1690
+ if (raw === undefined || raw === null)
1691
+ continue;
1692
+ if (Array.isArray(raw) && raw.length === 0)
1693
+ continue;
1694
+ if (typeof raw === 'object' && !Array.isArray(raw) && Object.keys(raw).length === 0)
1695
+ continue;
1696
+ out[key] = raw;
1697
+ }
1698
+ return out;
1699
+ }
1700
+ function rowToObject(row) {
1701
+ return {
1702
+ objectKey: row.object_key,
1703
+ objectType: row.object_type,
1704
+ name: row.name,
1705
+ fullName: row.full_name ?? undefined,
1706
+ domain: row.domain ?? undefined,
1707
+ owner: row.owner ?? undefined,
1708
+ status: row.status ?? undefined,
1709
+ description: row.description ?? undefined,
1710
+ sourcePath: row.source_path ?? undefined,
1711
+ sourceSystem: row.source_system ?? undefined,
1712
+ payload: safeJson(row.payload_json, {}),
1713
+ updatedAt: row.updated_at,
1714
+ };
1715
+ }
1716
+ function rowToEdge(row) {
1717
+ return {
1718
+ edgeType: row.edge_type,
1719
+ fromKey: row.from_key,
1720
+ toKey: row.to_key,
1721
+ confidence: row.confidence,
1722
+ payload: safeJson(row.payload_json, {}),
1723
+ };
1724
+ }
1725
+ function rowToQueryRun(row) {
1726
+ return {
1727
+ id: row.id,
1728
+ objectKey: row.object_key ?? undefined,
1729
+ source: row.source,
1730
+ status: row.status,
1731
+ rowCount: row.row_count ?? undefined,
1732
+ durationMs: row.duration_ms ?? undefined,
1733
+ errorCode: row.error_code ?? undefined,
1734
+ payload: safeJson(row.payload_json, {}),
1735
+ createdAt: row.created_at,
1736
+ };
1737
+ }
1738
+ function safeJson(raw, fallback) {
1739
+ if (!raw)
1740
+ return fallback;
1741
+ try {
1742
+ return JSON.parse(raw);
1743
+ }
1744
+ catch {
1745
+ return fallback;
1746
+ }
1747
+ }
1748
+ function rankMetadataObjects(args) {
1749
+ const terms = tokenize(args.question).slice(0, 12);
1750
+ const ranked = mergeObjects(args.rows)
1751
+ .map((row) => {
1752
+ const score = scoreMetadataObject(row, terms);
1753
+ return {
1754
+ row,
1755
+ rank: 0,
1756
+ score,
1757
+ reason: selectionReason(row, score),
1758
+ priorityTier: priorityTier(row),
1759
+ };
1760
+ })
1761
+ .sort((a, b) => b.score - a.score || objectPriority(a.row) - objectPriority(b.row) || a.row.name.localeCompare(b.row.name))
1762
+ .map((item, index) => ({ ...item, rank: index + 1 }));
1763
+ const cutoff = ranked[Math.max(0, args.limit - 1)];
1764
+ return {
1765
+ selected: ranked.slice(0, args.limit).map((item) => item.row),
1766
+ ranked,
1767
+ rejected: ranked.slice(args.limit).slice(0, 24).map((item) => ({
1768
+ objectKey: item.row.objectKey,
1769
+ objectType: item.row.objectType,
1770
+ name: item.row.name,
1771
+ reason: cutoff
1772
+ ? `Lower retrieval score than selected context window (cutoff ${cutoff.score.toFixed(1)}); ${item.reason}`
1773
+ : item.reason,
1774
+ score: item.score,
1775
+ rejectedRank: item.rank,
1776
+ })),
1777
+ };
1778
+ }
1779
+ function scoreMetadataObject(row, terms) {
1780
+ let score = row.score ? row.score * 10 : 0;
1781
+ score += Math.max(0, 44 - objectPriority(row) * 2);
1782
+ if (row.status === 'certified')
1783
+ score += 36;
1784
+ if (row.status === 'approved')
1785
+ score += 24;
1786
+ if (row.status === 'draft')
1787
+ score -= 8;
1788
+ if (row.objectType === 'dql_block' && row.status !== 'certified')
1789
+ score -= 16;
1790
+ if (row.objectType === 'semantic_metric')
1791
+ score += 10;
1792
+ if (row.objectType === 'dbt_model' || row.objectType === 'dbt_column')
1793
+ score += 4;
1794
+ score += scoreText([
1795
+ row.objectType,
1796
+ row.objectKey,
1797
+ row.name,
1798
+ row.fullName ?? '',
1799
+ row.domain ?? '',
1800
+ row.owner ?? '',
1801
+ row.description ?? '',
1802
+ JSON.stringify(row.payload ?? {}),
1803
+ ].join(' '), terms) * 8;
1804
+ return Number(score.toFixed(3));
1805
+ }
1806
+ function objectPriority(row) {
1807
+ return OBJECT_PRIORITY[row.objectType] ?? 99;
1808
+ }
1809
+ function priorityTier(row) {
1810
+ if (row.objectType === 'dql_block' && row.status === 'certified')
1811
+ return 'certified_block';
1812
+ if (row.objectType === 'semantic_metric')
1813
+ return 'semantic_metric';
1814
+ if (row.objectType === 'dql_term' || row.objectType === 'business_view')
1815
+ return 'business_context';
1816
+ if (row.objectType.startsWith('dbt_') || row.objectType === 'warehouse_table')
1817
+ return 'dbt_warehouse_context';
1818
+ if (row.objectType === 'notebook')
1819
+ return 'notebook_evidence';
1820
+ if (row.objectType === 'app' || row.objectType === 'dashboard')
1821
+ return 'consumption_evidence';
1822
+ return 'metadata';
1823
+ }
1824
+ function selectionReason(row, score) {
1825
+ const reasons = [reasonForObject(row), `priority tier: ${priorityTier(row)}`];
1826
+ if (row.status === 'certified')
1827
+ reasons.push('certified status');
1828
+ reasons.push(`score ${score.toFixed(1)}`);
1829
+ return reasons.join('; ');
1830
+ }
1831
+ function reasonForObject(row) {
1832
+ if (row.objectType === 'dql_block' && row.status === 'certified')
1833
+ return 'Certified reusable answer candidate';
1834
+ if (row.objectType === 'semantic_metric')
1835
+ return 'Semantic metric matched the question';
1836
+ if (row.objectType === 'dql_term' || row.objectType === 'business_view')
1837
+ return 'DQL business context';
1838
+ if (row.objectType.startsWith('dbt_') || row.objectType === 'warehouse_table')
1839
+ return 'dbt or warehouse metadata supplies physical context';
1840
+ if (row.objectType === 'app' || row.objectType === 'dashboard')
1841
+ return 'Published consumption context';
1842
+ return 'Relevant project metadata';
1843
+ }
1844
+ function mergeObjects(rows) {
1845
+ const byKey = new Map();
1846
+ for (const row of rows) {
1847
+ const existing = byKey.get(row.objectKey);
1848
+ byKey.set(row.objectKey, existing ? mergeObject(existing, row) : row);
1849
+ }
1850
+ return Array.from(byKey.values());
1851
+ }
1852
+ function deriveTrust(objects) {
1853
+ if (objects.length === 0)
1854
+ return 'unknown';
1855
+ const statuses = objects.map((row) => row.status ?? '');
1856
+ if (statuses.length > 0 && statuses.every((status) => status === 'certified'))
1857
+ return 'certified';
1858
+ if (statuses.some((status) => status === 'certified') && statuses.some((status) => status && status !== 'certified'))
1859
+ return 'mixed';
1860
+ if (statuses.some((status) => status === 'draft' || status === 'ai_generated' || status === 'analyst_review_required'))
1861
+ return 'draft';
1862
+ return statuses.some((status) => status === 'certified') ? 'mixed' : 'unknown';
1863
+ }
1864
+ function buildCitations(objects, edges) {
1865
+ const citations = objects.slice(0, 24).map((row) => ({
1866
+ objectKey: row.objectKey,
1867
+ objectType: row.objectType,
1868
+ name: row.name,
1869
+ reason: row.objectType === 'dql_block' && row.status === 'certified'
1870
+ ? 'Certified block candidate'
1871
+ : reasonForObject(row),
1872
+ }));
1873
+ for (const edge of edges.slice(0, 8)) {
1874
+ citations.push({
1875
+ objectKey: edge.fromKey,
1876
+ objectType: 'metadata_edge',
1877
+ name: `${edge.fromKey} -> ${edge.toKey}`,
1878
+ reason: `${edge.edgeType} relationship evidence`,
1879
+ });
1880
+ }
1881
+ const seen = new Set();
1882
+ return citations.filter((citation) => {
1883
+ const key = `${citation.objectType}|${citation.objectKey}|${citation.name}`;
1884
+ if (seen.has(key))
1885
+ return false;
1886
+ seen.add(key);
1887
+ return true;
1888
+ }).slice(0, 32);
1889
+ }
1890
+ function buildEvidenceSummaries(objects, edges, queryRuns, diagnostics) {
1891
+ const summaries = objects.slice(0, 10).map((row) => ({
1892
+ title: row.name || row.objectKey,
1893
+ detail: row.objectType.replace(/_/g, ' '),
1894
+ objectKey: row.objectKey,
1895
+ objectType: row.objectType,
1896
+ reason: reasonForObject(row),
1897
+ }));
1898
+ if (edges.length > 0) {
1899
+ summaries.push({
1900
+ title: `${edges.length} metadata relationship${edges.length === 1 ? '' : 's'}`,
1901
+ detail: 'Object relationships were used to connect business, semantic, and physical context.',
1902
+ reason: 'Graph evidence',
1903
+ });
1904
+ }
1905
+ if (queryRuns.length > 0) {
1906
+ summaries.push({
1907
+ title: 'Recent execution history',
1908
+ detail: `Latest run status: ${queryRuns[0]?.status ?? 'unknown'}.`,
1909
+ reason: 'Runtime evidence',
1910
+ });
1911
+ }
1912
+ const warnings = diagnostics.filter((diagnostic) => diagnostic.severity === 'warning').length;
1913
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length;
1914
+ if (warnings || errors) {
1915
+ summaries.push({
1916
+ title: 'Metadata diagnostics',
1917
+ detail: `${errors} error(s), ${warnings} warning(s).`,
1918
+ reason: 'Freshness and completeness checks',
1919
+ });
1920
+ }
1921
+ return summaries.slice(0, 16);
1922
+ }
1923
+ function buildWarnings(diagnostics, objects) {
1924
+ const warnings = diagnostics
1925
+ .filter((diagnostic) => diagnostic.severity === 'warning')
1926
+ .map((diagnostic) => diagnostic.message)
1927
+ .slice(0, 12);
1928
+ if (!objects.some((object) => object.objectType === 'semantic_metric')) {
1929
+ warnings.push('No semantic metric was selected for this context pack.');
1930
+ }
1931
+ return Array.from(new Set(warnings));
1932
+ }
1933
+ function buildCandidateConflicts(ranked) {
1934
+ const conflicts = [];
1935
+ for (const type of ['dql_block', 'semantic_metric', 'dql_term', 'business_view']) {
1936
+ const candidates = ranked
1937
+ .filter((item) => item.row.objectType === type && isGovernedCandidate(item.row))
1938
+ .slice(0, 4);
1939
+ if (candidates.length < 2)
1940
+ continue;
1941
+ const delta = candidates[0].score - candidates[1].score;
1942
+ if (delta <= 12) {
1943
+ const details = candidates.map((item) => ({
1944
+ objectKey: item.row.objectKey,
1945
+ objectType: item.row.objectType,
1946
+ name: item.row.name,
1947
+ domain: item.row.domain ?? null,
1948
+ status: item.row.status ?? null,
1949
+ rank: item.rank,
1950
+ score: item.score,
1951
+ reason: item.reason,
1952
+ }));
1953
+ conflicts.push({
1954
+ objectType: type,
1955
+ objectKeys: candidates.map((item) => item.row.objectKey),
1956
+ reason: `Multiple high-scoring governed ${type.replace(/_/g, ' ')} candidates may need disambiguation.`,
1957
+ prompt: `Which ${type.replace(/_/g, ' ')} should I use: ${details.map((item) => item.name).join(', ')}?`,
1958
+ candidates: details,
1959
+ });
1960
+ }
1961
+ }
1962
+ return conflicts;
1963
+ }
1964
+ function isGovernedCandidate(row) {
1965
+ return row.status === 'certified' || row.status === 'approved';
1966
+ }
1967
+ function tokenize(text) {
1968
+ return Array.from(new Set(text.toLowerCase().replace(/[^a-z0-9_ ]+/g, ' ').split(/\s+/).filter((term) => term.length >= 3))).slice(0, 24);
1969
+ }
1970
+ function scoreText(value, terms) {
1971
+ const lower = value.toLowerCase();
1972
+ return terms.reduce((score, term) => score + (lower.includes(term) ? 1 : 0), 0);
1973
+ }
1974
+ const STOP_WORDS = new Set([
1975
+ 'a', 'about', 'after', 'all', 'an', 'and', 'any', 'are', 'as', 'at', 'be', 'by',
1976
+ 'can', 'could', 'did', 'do', 'does', 'down', 'for', 'from', 'give', 'had', 'has',
1977
+ 'have', 'how', 'i', 'if', 'in', 'into', 'is', 'it', 'me', 'most', 'my', 'of',
1978
+ 'on', 'or', 'please', 'query', 'show', 'sql', 'than', 'that', 'the', 'their',
1979
+ 'them', 'this', 'to', 'up', 'using', 'was', 'we', 'were', 'what', 'when',
1980
+ 'where', 'which', 'who', 'why', 'with', 'would', 'you', 'your',
1981
+ ]);
1982
+ function sanitizeFtsQuery(raw) {
1983
+ return raw
1984
+ .split(/\s+/)
1985
+ .map((term) => term.replace(/[^\p{L}\p{N}_]/gu, ''))
1986
+ .filter((term) => term.length > 1 && !STOP_WORDS.has(term.toLowerCase()))
1987
+ .slice(0, 48)
1988
+ .map((term) => `"${term}"`)
1989
+ .join(' OR ');
1990
+ }
1991
+ //# sourceMappingURL=catalog.js.map