@duckcodeailabs/dql-cli 1.6.4 → 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.
Files changed (42) hide show
  1. package/dist/apps-api.d.ts +19 -0
  2. package/dist/apps-api.d.ts.map +1 -1
  3. package/dist/apps-api.js +335 -15
  4. package/dist/apps-api.js.map +1 -1
  5. package/dist/args.d.ts +11 -0
  6. package/dist/args.d.ts.map +1 -1
  7. package/dist/args.js +21 -0
  8. package/dist/args.js.map +1 -1
  9. package/dist/assets/dql-notebook/assets/index-D_tpetmE.js +3790 -0
  10. package/dist/assets/dql-notebook/index.html +1 -1
  11. package/dist/block-studio-import.js +23 -4
  12. package/dist/block-studio-import.js.map +1 -1
  13. package/dist/commands/agent.d.ts +2 -2
  14. package/dist/commands/agent.d.ts.map +1 -1
  15. package/dist/commands/agent.js +78 -13
  16. package/dist/commands/agent.js.map +1 -1
  17. package/dist/commands/app.d.ts.map +1 -1
  18. package/dist/commands/app.js +3 -2
  19. package/dist/commands/app.js.map +1 -1
  20. package/dist/commands/compile.d.ts +2 -0
  21. package/dist/commands/compile.d.ts.map +1 -1
  22. package/dist/commands/compile.js +33 -1
  23. package/dist/commands/compile.js.map +1 -1
  24. package/dist/commands/import.js +6 -6
  25. package/dist/commands/import.js.map +1 -1
  26. package/dist/commands/sync.d.ts.map +1 -1
  27. package/dist/commands/sync.js +17 -3
  28. package/dist/commands/sync.js.map +1 -1
  29. package/dist/index.js +7 -7
  30. package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
  31. package/dist/llm/providers/dql-agent-provider.js +113 -10
  32. package/dist/llm/providers/dql-agent-provider.js.map +1 -1
  33. package/dist/local-runtime.d.ts +8 -1
  34. package/dist/local-runtime.d.ts.map +1 -1
  35. package/dist/local-runtime.js +439 -37
  36. package/dist/local-runtime.js.map +1 -1
  37. package/dist/package.json +10 -10
  38. package/dist/promote-from-draft.d.ts +4 -4
  39. package/dist/promote-from-draft.js +8 -8
  40. package/dist/promote-from-draft.js.map +1 -1
  41. package/package.json +11 -11
  42. package/dist/assets/dql-notebook/assets/index-BFUUTIWF.js +0 -3618
@@ -8,7 +8,7 @@ import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, getDialect, Parser
8
8
  import { load as loadYaml } from 'js-yaml';
9
9
  import { listBlockTemplates } from './block-templates.js';
10
10
  import { getRunner as getLLMRunner } from './llm/index.js';
11
- import { ClaudeProvider, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, defaultMemoryPath, ensureDefaultMemoryFiles, } from '@duckcodeailabs/dql-agent';
11
+ import { ClaudeProvider, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildLocalContextPack, defaultMemoryPath, ensureDefaultMemoryFiles, ensureMetadataCatalogFresh, recordRuntimeSchemaSnapshot, } from '@duckcodeailabs/dql-agent';
12
12
  import { handleAppsApi } from './apps-api.js';
13
13
  import { getEffectiveProviderConfig, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
14
14
  import { DQLAccessDeniedError, activePersonaAppId, assertAppAccess, loadRuntimeApp, runtimeVariables, } from './governance-runtime.js';
@@ -17,6 +17,7 @@ import { Certifier } from '@duckcodeailabs/dql-governance';
17
17
  import { buildSemanticObjectDetail, buildSemanticTree, computeSyncDiff, loadSemanticImportManifest, performSemanticImport, previewSemanticImport, syncSemanticImport, } from './semantic-import.js';
18
18
  import { clearBlockStudioImportSessions, createBlockStudioImportSession, deleteBlockStudioImportSession, listBlockStudioImportSessions, loadBlockStudioImportSession, readBlockStudioImportCandidate, updateBlockStudioImportCandidate, writeBlockStudioImportSession, writeBlockStudioImportCandidate, } from './block-studio-import.js';
19
19
  import { MetricFlowUnavailableError, compileMetricFlowQuery, hasDbtSemanticManifest, } from './metricflow.js';
20
+ const NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT = 500;
20
21
  export async function startLocalServer(opts) {
21
22
  const { rootDir, executor, connection: rawConnection, preferredPort, projectRoot = process.cwd() } = opts;
22
23
  const bindHost = opts.host ?? process.env.DQL_HOST ?? '127.0.0.1';
@@ -50,6 +51,7 @@ export async function startLocalServer(opts) {
50
51
  catch { /* continue without */ }
51
52
  }
52
53
  }
54
+ await refreshLocalMetadataCatalog(projectRoot);
53
55
  // Auto-register data/ CSV and Parquet files as DuckDB views so semantic layer
54
56
  // queries like `FROM orders` resolve without requiring read_csv_auto() in SQL.
55
57
  if (connection.driver === 'file' || connection.driver === 'duckdb') {
@@ -234,13 +236,22 @@ export async function startLocalServer(opts) {
234
236
  };
235
237
  };
236
238
  const getSchemaContextForAgent = async (question) => {
239
+ const catalogContext = await buildAgentSchemaContextFromCatalog(projectRoot, question).catch(() => []);
240
+ if (catalogContext.length > 0) {
241
+ const enriched = await enrichAgentSchemaContextWithValueMatches(question, catalogContext, executor, connection);
242
+ recordAgentRuntimeSchemaSnapshot(projectRoot, enriched, 'catalog enriched runtime schema');
243
+ return enriched;
244
+ }
237
245
  try {
238
246
  const result = await executor.executeQuery(`SELECT table_schema, table_name, column_name, data_type
239
247
  FROM information_schema.columns
240
248
  WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
241
- ORDER BY table_schema, table_name, ordinal_position`, [], runtimeVariables({}), connection);
249
+ ORDER BY table_schema, table_name, ordinal_position
250
+ LIMIT 2000`, [], runtimeVariables({}), connection);
242
251
  const schemaContext = buildAgentSchemaContext(question, result.rows);
243
- return enrichAgentSchemaContextWithValueMatches(question, schemaContext, executor, connection);
252
+ const enriched = await enrichAgentSchemaContextWithValueMatches(question, schemaContext, executor, connection);
253
+ recordAgentRuntimeSchemaSnapshot(projectRoot, enriched, 'information_schema runtime scan');
254
+ return enriched;
244
255
  }
245
256
  catch {
246
257
  return [];
@@ -400,10 +411,10 @@ export async function startLocalServer(opts) {
400
411
  throw new Error(message);
401
412
  }
402
413
  const plan = buildExecutionPlan({ id: 'block-studio', type: 'dql', source, title: 'Block Studio' }, { semanticLayer, driver: targetConnection.driver, tableMapping });
403
- const sql = resolveProjectRelativeSqlPaths(semanticCompose?.sql ?? plan?.sql ?? executableSql, projectRoot, projectConfig.dataDir);
404
- const result = await executor.executeQuery(sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}), targetConnection);
414
+ const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan?.sql ?? executableSql, targetConnection, projectRoot, projectConfig);
415
+ const result = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}), prepared.connection);
405
416
  return {
406
- sql: plan?.sql ?? executableSql,
417
+ sql: prepared.sql,
407
418
  result: normalizeQueryResult(result),
408
419
  chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
409
420
  };
@@ -1383,6 +1394,7 @@ export async function startLocalServer(opts) {
1383
1394
  return;
1384
1395
  }
1385
1396
  setBlockStudioStatus(projectRoot, blockPath, newStatus);
1397
+ await refreshLocalMetadataCatalog(projectRoot);
1386
1398
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1387
1399
  res.end(serializeJSON({ ok: true, status: newStatus }));
1388
1400
  }
@@ -1502,10 +1514,7 @@ export async function startLocalServer(opts) {
1502
1514
  return;
1503
1515
  }
1504
1516
  const result = await certifyBlockStudioSource(source, blockPath);
1505
- const blockers = [
1506
- ...result.checklist.blockers,
1507
- ...result.certification.errors.map((error) => `${error.rule}: ${error.message}`),
1508
- ];
1517
+ const blockers = Array.from(new Set(result.checklist.blockers));
1509
1518
  if (!result.certification.certified || blockers.length > 0) {
1510
1519
  res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
1511
1520
  res.end(serializeJSON({ ok: false, ...result, blockers }));
@@ -1513,6 +1522,7 @@ export async function startLocalServer(opts) {
1513
1522
  }
1514
1523
  if (blockPath)
1515
1524
  setBlockStudioStatus(projectRoot, blockPath, 'certified');
1525
+ await refreshLocalMetadataCatalog(projectRoot);
1516
1526
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1517
1527
  res.end(serializeJSON({ ok: true, status: 'certified', ...result }));
1518
1528
  }
@@ -1616,6 +1626,8 @@ export async function startLocalServer(opts) {
1616
1626
  }
1617
1627
  const nextSession = { ...session, candidates: nextCandidates, updatedAt: new Date().toISOString() };
1618
1628
  writeBlockStudioImportSession(projectRoot, nextSession);
1629
+ if (saved.length > 0)
1630
+ await refreshLocalMetadataCatalog(projectRoot);
1619
1631
  res.writeHead(errors.length > 0 ? 207 : 200, { 'Content-Type': 'application/json; charset=utf-8' });
1620
1632
  res.end(serializeJSON({ ok: errors.length === 0, session: nextSession, saved, errors }));
1621
1633
  }
@@ -1733,6 +1745,7 @@ export async function startLocalServer(opts) {
1733
1745
  });
1734
1746
  const next = { ...readiness.candidate, reviewStatus: 'saved', savedPath };
1735
1747
  writeBlockStudioImportCandidate(projectRoot, importId, next);
1748
+ await refreshLocalMetadataCatalog(projectRoot);
1736
1749
  const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
1737
1750
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1738
1751
  res.end(serializeJSON({ candidate: next, block: payload }));
@@ -1871,6 +1884,7 @@ export async function startLocalServer(opts) {
1871
1884
  }
1872
1885
  : undefined,
1873
1886
  });
1887
+ await refreshLocalMetadataCatalog(projectRoot);
1874
1888
  const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
1875
1889
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1876
1890
  res.end(serializeJSON(payload));
@@ -2939,23 +2953,20 @@ export async function startLocalServer(opts) {
2939
2953
  return;
2940
2954
  }
2941
2955
  if (req.method === 'POST' && path === '/api/test-connection') {
2956
+ let target = connection;
2942
2957
  try {
2943
2958
  const body = await readJSON(req);
2944
- const target = normalizeProjectConnection(isConnectionConfig(body.connection) ? body.connection : connection, projectRoot);
2959
+ target = normalizeProjectConnection(isConnectionConfig(body.connection) ? body.connection : connection, projectRoot);
2945
2960
  const connector = await executor.getConnector(target);
2946
- const ok = await connector.ping();
2947
- const driver = target.driver ?? 'unknown';
2948
- res.writeHead(ok ? 200 : 400, { 'Content-Type': 'application/json; charset=utf-8' });
2949
- res.end(serializeJSON({
2950
- ok,
2951
- message: ok ? `Connected to ${driver} successfully` : `Connection to ${driver} failed`,
2952
- }));
2961
+ const result = await validateConnectionForTest(connector, target);
2962
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
2963
+ res.end(serializeJSON(result));
2953
2964
  }
2954
2965
  catch (error) {
2955
- res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
2966
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
2956
2967
  res.end(serializeJSON({
2957
2968
  ok: false,
2958
- message: error instanceof Error ? error.message : String(error),
2969
+ message: formatConnectionTestError(target, error),
2959
2970
  }));
2960
2971
  }
2961
2972
  return;
@@ -3207,7 +3218,9 @@ export async function startLocalServer(opts) {
3207
3218
  const resolved = resolveNotebookBlockReferenceCell(cell, projectRoot);
3208
3219
  const executableCell = resolved.cell;
3209
3220
  const cellConnection = isConnectionConfig(body.connection) ? body.connection : connection;
3210
- const tableMapping = await resolveSemanticTableMapping(executor, cellConnection, semanticLayer);
3221
+ const tableMapping = needsSemanticTableMapping(executableCell)
3222
+ ? await resolveSemanticTableMapping(executor, cellConnection, semanticLayer)
3223
+ : undefined;
3211
3224
  const plan = buildExecutionPlan(executableCell, { semanticLayer, driver: cellConnection.driver, tableMapping });
3212
3225
  if (!plan) {
3213
3226
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -3283,7 +3296,10 @@ table: ${table}${tagList}
3283
3296
  return;
3284
3297
  }
3285
3298
  const content = readFileSync(filePath);
3286
- res.writeHead(200, { 'Content-Type': contentTypeFor(filePath) });
3299
+ res.writeHead(200, {
3300
+ 'Content-Type': contentTypeFor(filePath),
3301
+ 'Cache-Control': 'no-store, max-age=0',
3302
+ });
3287
3303
  res.end(content);
3288
3304
  });
3289
3305
  return new Promise((resolvePromise, reject) => {
@@ -3328,6 +3344,102 @@ export function formatLocalQueryRuntimeError(connection, error) {
3328
3344
  }
3329
3345
  return `Local query runtime is unavailable for driver "${driver}": ${detail}`;
3330
3346
  }
3347
+ export async function validateConnectionForTest(connector, connection) {
3348
+ if (connection.driver === 'snowflake') {
3349
+ return validateSnowflakeConnectionForTest(connector, connection);
3350
+ }
3351
+ const ok = await connector.ping();
3352
+ const label = connectionDriverLabel(connection);
3353
+ return {
3354
+ ok,
3355
+ message: ok
3356
+ ? `Connected to ${label} successfully.`
3357
+ : `Connection to ${label} failed. Check credentials, network access, and database availability.`,
3358
+ };
3359
+ }
3360
+ function formatConnectionTestError(connection, error) {
3361
+ const detail = error instanceof Error ? error.message : String(error);
3362
+ const label = connectionDriverLabel(connection);
3363
+ if (connection.driver === 'snowflake') {
3364
+ const cleaned = detail.replace(/^Snowflake (?:connection|query) failed:\s*/i, '').trim();
3365
+ return `Snowflake connection failed: ${cleaned || 'Check account, user, password/auth method, role, and network access.'}`;
3366
+ }
3367
+ return `Connection to ${label} failed: ${detail}`;
3368
+ }
3369
+ async function validateSnowflakeConnectionForTest(connector, connection) {
3370
+ const warehouse = connection.warehouse?.trim();
3371
+ if (!warehouse) {
3372
+ return {
3373
+ ok: false,
3374
+ message: 'Snowflake connection requires a warehouse before it can be tested.',
3375
+ };
3376
+ }
3377
+ const warehouseRow = await findSnowflakeWarehouse(connector, warehouse);
3378
+ if (!warehouseRow) {
3379
+ return {
3380
+ ok: false,
3381
+ message: `Snowflake warehouse "${warehouse}" was not found or is not visible to this role.`,
3382
+ };
3383
+ }
3384
+ const state = String(readRowField(warehouseRow, 'state') ?? '').trim();
3385
+ const normalizedState = state.toUpperCase();
3386
+ if (normalizedState && normalizedState !== 'STARTED') {
3387
+ return {
3388
+ ok: false,
3389
+ message: `Snowflake warehouse "${warehouse}" is ${state}. Start or resume it, then test again.`,
3390
+ details: {
3391
+ warehouse,
3392
+ state,
3393
+ },
3394
+ };
3395
+ }
3396
+ const context = await connector.execute(`SELECT
3397
+ CURRENT_ACCOUNT() AS account_name,
3398
+ CURRENT_USER() AS user_name,
3399
+ CURRENT_ROLE() AS role_name,
3400
+ CURRENT_DATABASE() AS database_name,
3401
+ CURRENT_SCHEMA() AS schema_name,
3402
+ CURRENT_WAREHOUSE() AS warehouse_name`);
3403
+ const row = context.rows[0] ?? {};
3404
+ const user = String(readRowField(row, 'user_name') ?? connection.username ?? '').trim();
3405
+ const role = String(readRowField(row, 'role_name') ?? connection.role ?? '').trim();
3406
+ const activeWarehouse = String(readRowField(row, 'warehouse_name') ?? warehouse).trim();
3407
+ return {
3408
+ ok: true,
3409
+ message: `Connected to Snowflake${user ? ` as ${user}` : ''} using warehouse ${activeWarehouse || warehouse}.`,
3410
+ details: {
3411
+ warehouse: activeWarehouse || warehouse,
3412
+ warehouseState: state || 'STARTED',
3413
+ role: role || undefined,
3414
+ database: readRowField(row, 'database_name') ?? connection.database,
3415
+ schema: readRowField(row, 'schema_name') ?? connection.schema,
3416
+ },
3417
+ };
3418
+ }
3419
+ async function findSnowflakeWarehouse(connector, warehouse) {
3420
+ const candidates = Array.from(new Set([warehouse, warehouse.toUpperCase()]));
3421
+ for (const candidate of candidates) {
3422
+ const result = await connector.execute(`SHOW WAREHOUSES LIKE '${escapeSqlString(candidate)}'`);
3423
+ const row = result.rows.find((item) => {
3424
+ const name = String(readRowField(item, 'name') ?? '').trim();
3425
+ return name.localeCompare(warehouse, undefined, { sensitivity: 'accent' }) === 0;
3426
+ });
3427
+ if (row)
3428
+ return row;
3429
+ }
3430
+ return null;
3431
+ }
3432
+ function readRowField(row, field) {
3433
+ const expected = field.toLowerCase();
3434
+ const entry = Object.entries(row).find(([key]) => key.toLowerCase() === expected);
3435
+ return entry?.[1];
3436
+ }
3437
+ function escapeSqlString(value) {
3438
+ return value.replace(/'/g, "''");
3439
+ }
3440
+ function connectionDriverLabel(connection) {
3441
+ return connection.driver === 'snowflake' ? 'Snowflake' : connection.driver ?? 'database';
3442
+ }
3331
3443
  /**
3332
3444
  * Normalize connector QueryResult → SPA-friendly shape.
3333
3445
  * Connector returns columns as ColumnMeta[] ({name,type,driverType}).
@@ -3336,16 +3448,19 @@ export function formatLocalQueryRuntimeError(connection, error) {
3336
3448
  function normalizeQueryResult(result, semanticRefs) {
3337
3449
  const rawCols = Array.isArray(result?.columns) ? result.columns : [];
3338
3450
  const columns = rawCols.map((c) => typeof c === 'string' ? c : typeof c?.name === 'string' ? c.name : String(c));
3451
+ const rawRows = Array.isArray(result?.rows) ? result.rows : [];
3452
+ const rows = rawRows.slice(0, NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT);
3339
3453
  const hasRefs = semanticRefs && (semanticRefs.metrics.length > 0 || semanticRefs.dimensions.length > 0);
3340
3454
  return {
3341
3455
  columns,
3342
- rows: Array.isArray(result?.rows) ? result.rows : [],
3343
- rowCount: typeof result?.rowCount === 'number' ? result.rowCount : (result?.rows?.length ?? 0),
3456
+ rows,
3457
+ rowCount: typeof result?.rowCount === 'number' ? result.rowCount : rawRows.length,
3344
3458
  executionTime: typeof result?.executionTimeMs === 'number'
3345
3459
  ? result.executionTimeMs
3346
3460
  : typeof result?.executionTime === 'number'
3347
3461
  ? result.executionTime
3348
3462
  : 0,
3463
+ ...(rawRows.length > rows.length ? { truncated: true } : {}),
3349
3464
  ...(hasRefs ? { semanticRefs } : {}),
3350
3465
  };
3351
3466
  }
@@ -3414,6 +3529,15 @@ export function serializeJSON(value) {
3414
3529
  return current;
3415
3530
  });
3416
3531
  }
3532
+ async function refreshLocalMetadataCatalog(projectRoot) {
3533
+ try {
3534
+ await ensureMetadataCatalogFresh(projectRoot, { force: true });
3535
+ }
3536
+ catch {
3537
+ // The catalog is a rebuildable local cache. Save/certify flows should not
3538
+ // fail only because metadata refresh hit a stale dbt or semantic config.
3539
+ }
3540
+ }
3417
3541
  function renderNotFound(path) {
3418
3542
  return `<!doctype html>
3419
3543
  <html lang="en">
@@ -3551,13 +3675,112 @@ function isPlaceholderLocalConnection(value) {
3551
3675
  }
3552
3676
  export function prepareLocalExecution(sql, connection, projectRoot, projectConfig) {
3553
3677
  const normalizedConnection = normalizeProjectConnection(connection, projectRoot);
3678
+ const dbtResolvedSql = resolveDbtMacrosForExecution(sql, projectRoot, projectConfig);
3554
3679
  return {
3555
3680
  sql: shouldResolveProjectPaths(normalizedConnection)
3556
- ? resolveProjectRelativeSqlPaths(sql, projectRoot, projectConfig.dataDir)
3557
- : sql,
3681
+ ? resolveProjectRelativeSqlPaths(dbtResolvedSql, projectRoot, projectConfig.dataDir)
3682
+ : dbtResolvedSql,
3558
3683
  connection: normalizedConnection,
3559
3684
  };
3560
3685
  }
3686
+ export function resolveDbtMacrosForExecution(sql, projectRoot, projectConfig = {}) {
3687
+ if (!/\{\{\s*(?:ref|source)\s*\(/i.test(sql))
3688
+ return sql;
3689
+ const manifestPath = resolveDbtManifestPath(projectRoot, projectConfig);
3690
+ if (!manifestPath) {
3691
+ throw new Error('dbt ref/source macros were found, but target/manifest.json was not available. Run dbt parse or dbt compile, then retry.');
3692
+ }
3693
+ const manifest = readJsonFile(manifestPath);
3694
+ const refs = buildDbtRelationLookup(manifest);
3695
+ const unresolved = new Set();
3696
+ let rendered = sql.replace(/\{\{\s*ref\(\s*(?:(['"])([^'"]+)\1\s*,\s*)?(['"])([^'"]+)\3(?:\s*,[^)]*)?\)\s*\}\}/gi, (match, _pkgQuote, packageName, _modelQuote, modelName) => {
3697
+ const key = normalizeDbtLookupKey(modelName);
3698
+ const scopedKey = packageName ? normalizeDbtLookupKey(`${packageName}.${modelName}`) : key;
3699
+ const relation = refs.models.get(scopedKey) ?? refs.models.get(key);
3700
+ if (!relation) {
3701
+ unresolved.add(packageName ? `ref('${packageName}', '${modelName}')` : `ref('${modelName}')`);
3702
+ return match;
3703
+ }
3704
+ return relation;
3705
+ });
3706
+ rendered = rendered.replace(/\{\{\s*source\(\s*(['"])([^'"]+)\1\s*,\s*(['"])([^'"]+)\3\s*\)\s*\}\}/gi, (match, _sourceQuote, sourceName, _tableQuote, tableName) => {
3707
+ const key = normalizeDbtLookupKey(`${sourceName}.${tableName}`);
3708
+ const relation = refs.sources.get(key) ?? refs.sources.get(normalizeDbtLookupKey(tableName));
3709
+ if (!relation) {
3710
+ unresolved.add(`source('${sourceName}', '${tableName}')`);
3711
+ return match;
3712
+ }
3713
+ return relation;
3714
+ });
3715
+ if (unresolved.size > 0) {
3716
+ throw new Error(`Could not resolve dbt macro${unresolved.size === 1 ? '' : 's'} from manifest.json: ${Array.from(unresolved).join(', ')}.`);
3717
+ }
3718
+ return rendered;
3719
+ }
3720
+ function buildDbtRelationLookup(manifest) {
3721
+ const models = new Map();
3722
+ const sources = new Map();
3723
+ const root = manifest && typeof manifest === 'object' ? manifest : {};
3724
+ const nodes = root.nodes && typeof root.nodes === 'object' ? root.nodes : {};
3725
+ const manifestSources = root.sources && typeof root.sources === 'object' ? root.sources : {};
3726
+ for (const [uniqueId, rawNode] of Object.entries(nodes)) {
3727
+ const node = rawNode && typeof rawNode === 'object' ? rawNode : null;
3728
+ if (!node || node.resource_type !== 'model')
3729
+ continue;
3730
+ const relation = dbtRelationName(node);
3731
+ if (!relation)
3732
+ continue;
3733
+ const name = stringField(node, 'name');
3734
+ const alias = stringField(node, 'alias');
3735
+ const packageName = uniqueId.split('.')[1];
3736
+ for (const key of [name, alias, packageName && name ? `${packageName}.${name}` : null, uniqueId]) {
3737
+ if (key)
3738
+ models.set(normalizeDbtLookupKey(key), relation);
3739
+ }
3740
+ }
3741
+ for (const [uniqueId, rawSource] of Object.entries(manifestSources)) {
3742
+ const source = rawSource && typeof rawSource === 'object' ? rawSource : null;
3743
+ if (!source)
3744
+ continue;
3745
+ const relation = dbtRelationName(source);
3746
+ if (!relation)
3747
+ continue;
3748
+ const sourceName = stringField(source, 'source_name');
3749
+ const name = stringField(source, 'name');
3750
+ const identifier = stringField(source, 'identifier');
3751
+ for (const key of [
3752
+ sourceName && name ? `${sourceName}.${name}` : null,
3753
+ sourceName && identifier ? `${sourceName}.${identifier}` : null,
3754
+ name,
3755
+ identifier,
3756
+ uniqueId,
3757
+ ]) {
3758
+ if (key)
3759
+ sources.set(normalizeDbtLookupKey(key), relation);
3760
+ }
3761
+ }
3762
+ return { models, sources };
3763
+ }
3764
+ function dbtRelationName(node) {
3765
+ const relationName = stringField(node, 'relation_name');
3766
+ if (relationName)
3767
+ return relationName;
3768
+ const database = stringField(node, 'database');
3769
+ const schema = stringField(node, 'schema');
3770
+ const alias = stringField(node, 'alias') ?? stringField(node, 'identifier') ?? stringField(node, 'name');
3771
+ if (database && schema && alias)
3772
+ return `${database}.${schema}.${alias}`;
3773
+ if (schema && alias)
3774
+ return `${schema}.${alias}`;
3775
+ return alias ?? null;
3776
+ }
3777
+ function stringField(source, key) {
3778
+ const value = source[key];
3779
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
3780
+ }
3781
+ function normalizeDbtLookupKey(value) {
3782
+ return value.trim().replace(/^['"]|['"]$/g, '').toLowerCase();
3783
+ }
3561
3784
  const AGENT_PREVIEW_FORBIDDEN_SQL = [
3562
3785
  'alter',
3563
3786
  'analyze',
@@ -3677,6 +3900,13 @@ export function prepareSemanticSql(sql, semanticLayer) {
3677
3900
  unresolvedRefs: resolution.unresolvedRefs,
3678
3901
  };
3679
3902
  }
3903
+ function needsSemanticTableMapping(cell) {
3904
+ if (cell.type === 'sql')
3905
+ return hasSemanticRefs(cell.source);
3906
+ if (cell.type !== 'dql')
3907
+ return false;
3908
+ return hasSemanticRefs(cell.source) || /\btype\s*=\s*"semantic"/i.test(cell.source);
3909
+ }
3680
3910
  export function normalizeProjectConnection(connection, projectRoot) {
3681
3911
  const normalized = expandConnectionEnvPlaceholders({ ...connection });
3682
3912
  if ((normalized.driver === 'file' || normalized.driver === 'duckdb') && normalized.filepath && normalized.filepath !== ':memory:' && !isAbsoluteLikePath(normalized.filepath)) {
@@ -4129,7 +4359,9 @@ export async function resolveSemanticTableMapping(executor, connection, semantic
4129
4359
  try {
4130
4360
  const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name
4131
4361
  FROM information_schema.tables
4132
- WHERE table_schema NOT IN ('information_schema', 'pg_catalog')`, [], {}, connection);
4362
+ WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
4363
+ ORDER BY table_schema, table_name
4364
+ LIMIT 2000`, [], {}, connection);
4133
4365
  return buildSemanticTableMapping(semanticLayer, tablesResult.rows);
4134
4366
  }
4135
4367
  catch {
@@ -4580,12 +4812,6 @@ function buildBlockStudioCertificationChecklist(input) {
4580
4812
  blockers.add(`${error.rule}: ${error.message}`);
4581
4813
  for (const blocker of input.extraBlockers ?? [])
4582
4814
  blockers.add(blocker);
4583
- if (!parsed.domain.trim())
4584
- blockers.add('Missing domain');
4585
- if (!parsed.owner.trim())
4586
- blockers.add('Missing owner');
4587
- if (!parsed.description.trim())
4588
- blockers.add('Missing description');
4589
4815
  if (!input.previewSucceeded)
4590
4816
  blockers.add('Block has not run successfully');
4591
4817
  if (!input.testResults || input.testResults.failed > 0)
@@ -5280,7 +5506,7 @@ function buildProjectLineageGraphUncached(projectRoot, semanticLayer) {
5280
5506
  // Fall back to a live build.
5281
5507
  }
5282
5508
  }
5283
- const dbtManifestPath = resolveDbtManifestPath(projectRoot);
5509
+ const dbtManifestPath = resolveDbtManifestPath(projectRoot, {});
5284
5510
  try {
5285
5511
  const manifest = buildManifest({
5286
5512
  projectRoot,
@@ -5337,9 +5563,14 @@ function buildProjectLineageGraphUncached(projectRoot, semanticLayer) {
5337
5563
  return buildLineageGraph(blocks, metrics, dimensions);
5338
5564
  }
5339
5565
  }
5340
- function resolveDbtManifestPath(projectRoot) {
5341
- const candidate = join(projectRoot, 'target', 'manifest.json');
5342
- return existsSync(candidate) ? candidate : undefined;
5566
+ function resolveDbtManifestPath(projectRoot, projectConfig = {}) {
5567
+ const candidates = [];
5568
+ if (projectConfig.dbt?.projectDir || projectConfig.semanticLayer?.provider === 'dbt') {
5569
+ const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
5570
+ candidates.push(resolve(dbtProjectPath, projectConfig.dbt?.manifestPath ?? 'target/manifest.json'));
5571
+ }
5572
+ candidates.push(join(projectRoot, 'target', 'manifest.json'), join(resolve(projectRoot, '..'), 'target', 'manifest.json'), join(resolve(projectRoot, '../dbt'), 'target', 'manifest.json'), join(resolve(projectRoot, '../../dbt'), 'target', 'manifest.json'));
5573
+ return candidates.find((candidate, index, list) => list.indexOf(candidate) === index && existsSync(candidate));
5343
5574
  }
5344
5575
  export function discoverDbtProfileConnections(projectRoot, projectConfig) {
5345
5576
  const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
@@ -6301,6 +6532,177 @@ function isAiPinRefreshDue(lastRefreshedAt) {
6301
6532
  return true;
6302
6533
  return Date.now() - last >= 24 * 60 * 60 * 1000;
6303
6534
  }
6535
+ async function buildAgentSchemaContextFromCatalog(projectRoot, question) {
6536
+ const contextPack = await buildLocalContextPack(projectRoot, { question, limit: 80 });
6537
+ return buildAgentSchemaContextFromContextPack(question, contextPack);
6538
+ }
6539
+ function recordAgentRuntimeSchemaSnapshot(projectRoot, schemaContext, source) {
6540
+ if (schemaContext.length === 0)
6541
+ return;
6542
+ try {
6543
+ recordRuntimeSchemaSnapshot(projectRoot, {
6544
+ source,
6545
+ tables: schemaContext.slice(0, 80).map((table) => ({
6546
+ relation: table.relation,
6547
+ schema: table.schema,
6548
+ name: table.name,
6549
+ description: table.description,
6550
+ source: table.source,
6551
+ columns: table.columns.slice(0, 120).map((column) => ({
6552
+ name: column.name,
6553
+ type: column.type,
6554
+ description: column.description,
6555
+ sampleValues: column.sampleValues?.slice(0, 8),
6556
+ })),
6557
+ })),
6558
+ });
6559
+ }
6560
+ catch {
6561
+ // Runtime schema snapshots are advisory local metadata and must not block answers.
6562
+ }
6563
+ }
6564
+ function buildAgentSchemaContextFromContextPack(question, contextPack) {
6565
+ const byRelation = new Map();
6566
+ const objectsByKey = new Map(contextPack.objects.map((object) => [object.objectKey, object]));
6567
+ const upsert = (table) => {
6568
+ if (!table.relation || !table.name)
6569
+ return;
6570
+ const key = table.relation.toLowerCase();
6571
+ const existing = byRelation.get(key);
6572
+ if (!existing) {
6573
+ byRelation.set(key, {
6574
+ ...table,
6575
+ columns: dedupeAgentSchemaColumns(table.columns).slice(0, 80),
6576
+ });
6577
+ return;
6578
+ }
6579
+ byRelation.set(key, {
6580
+ ...existing,
6581
+ description: existing.description ?? table.description,
6582
+ source: existing.source === table.source ? existing.source : 'local metadata catalog',
6583
+ columns: dedupeAgentSchemaColumns([...existing.columns, ...table.columns]).slice(0, 80),
6584
+ });
6585
+ };
6586
+ for (const object of contextPack.objects) {
6587
+ const table = metadataObjectToAgentSchemaTable(object);
6588
+ if (table)
6589
+ upsert(table);
6590
+ }
6591
+ for (const edge of contextPack.edges) {
6592
+ if (edge.edgeType !== 'maps_to_dbt_model' && edge.edgeType !== 'uses_dbt_model')
6593
+ continue;
6594
+ const from = objectsByKey.get(edge.fromKey);
6595
+ const to = objectsByKey.get(edge.toKey);
6596
+ const warehouse = from?.objectType === 'warehouse_table' ? from : null;
6597
+ const dbtModel = to && (to.objectType === 'dbt_model' || to.objectType === 'dbt_source') ? to : null;
6598
+ if (!warehouse || !dbtModel)
6599
+ continue;
6600
+ const warehouseTable = metadataObjectToAgentSchemaTable(warehouse);
6601
+ const modelTable = metadataObjectToAgentSchemaTable(dbtModel);
6602
+ if (!warehouseTable || !modelTable)
6603
+ continue;
6604
+ upsert({
6605
+ ...warehouseTable,
6606
+ description: warehouseTable.description ?? modelTable.description,
6607
+ columns: modelTable.columns,
6608
+ source: 'local metadata catalog',
6609
+ });
6610
+ }
6611
+ const tokens = agentSchemaTokens(question);
6612
+ const shouldProbeValues = extractAgentValueSearchTerms(question).length > 0;
6613
+ return Array.from(byRelation.values())
6614
+ .map((table) => ({
6615
+ table,
6616
+ score: scoreAgentSchemaTable(table, tokens) + (shouldProbeValues ? scoreAgentValueProbeTable(table) : 0),
6617
+ }))
6618
+ .filter((entry) => entry.table.columns.length > 0 && entry.score > 0)
6619
+ .sort((a, b) => b.score - a.score || a.table.relation.localeCompare(b.table.relation))
6620
+ .slice(0, 12)
6621
+ .map((entry) => entry.table);
6622
+ }
6623
+ function metadataObjectToAgentSchemaTable(object) {
6624
+ if (object.objectType === 'dbt_column' || object.objectType === 'runtime_column') {
6625
+ const relation = metadataPayloadString(object, 'relation');
6626
+ const model = metadataPayloadString(object, 'model') ?? relation;
6627
+ if (!model)
6628
+ return null;
6629
+ return {
6630
+ relation: relation ?? model,
6631
+ schema: relation ? relation.split('.').slice(-2, -1)[0] : undefined,
6632
+ name: relation ? relation.split('.').at(-1) ?? model : model,
6633
+ source: 'local metadata catalog',
6634
+ columns: [{
6635
+ name: object.name,
6636
+ type: metadataPayloadString(object, 'type'),
6637
+ description: object.description,
6638
+ }],
6639
+ };
6640
+ }
6641
+ if (object.objectType !== 'dbt_model' && object.objectType !== 'dbt_source' && object.objectType !== 'warehouse_table' && object.objectType !== 'runtime_table') {
6642
+ return null;
6643
+ }
6644
+ const relation = metadataObjectRelation(object);
6645
+ if (!relation)
6646
+ return null;
6647
+ const relationParts = relation.split('.').filter(Boolean);
6648
+ const schema = metadataPayloadString(object, 'schema') ?? (relationParts.length >= 2 ? relationParts[relationParts.length - 2] : undefined);
6649
+ const name = relationParts.at(-1) ?? object.name;
6650
+ const columns = metadataObjectColumns(object);
6651
+ return {
6652
+ relation,
6653
+ schema,
6654
+ name,
6655
+ description: object.description,
6656
+ columns,
6657
+ source: 'local metadata catalog',
6658
+ };
6659
+ }
6660
+ function metadataObjectRelation(object) {
6661
+ const relation = metadataPayloadString(object, 'relation');
6662
+ if (relation)
6663
+ return relation;
6664
+ const database = metadataPayloadString(object, 'database');
6665
+ const schema = metadataPayloadString(object, 'schema');
6666
+ if (database && schema)
6667
+ return [database, schema, object.name].join('.');
6668
+ return object.fullName ?? object.name;
6669
+ }
6670
+ function metadataObjectColumns(object) {
6671
+ const columns = object.payload?.columns;
6672
+ if (!Array.isArray(columns))
6673
+ return [];
6674
+ return columns.flatMap((column) => {
6675
+ if (!column || typeof column !== 'object')
6676
+ return [];
6677
+ const record = column;
6678
+ const name = stringFromRecord(record, 'name') ?? stringFromRecord(record, 'column_name');
6679
+ if (!name)
6680
+ return [];
6681
+ return [{
6682
+ name,
6683
+ type: stringFromRecord(record, 'type') ?? stringFromRecord(record, 'data_type'),
6684
+ description: stringFromRecord(record, 'description'),
6685
+ }];
6686
+ });
6687
+ }
6688
+ function metadataPayloadString(object, key) {
6689
+ const value = object.payload?.[key];
6690
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
6691
+ }
6692
+ function dedupeAgentSchemaColumns(columns) {
6693
+ const byName = new Map();
6694
+ for (const column of columns) {
6695
+ const key = column.name.toLowerCase();
6696
+ const existing = byName.get(key);
6697
+ byName.set(key, existing ? {
6698
+ ...existing,
6699
+ type: existing.type ?? column.type,
6700
+ description: existing.description ?? column.description,
6701
+ sampleValues: uniqueStrings([...(existing.sampleValues ?? []), ...(column.sampleValues ?? [])]).slice(0, 5),
6702
+ } : column);
6703
+ }
6704
+ return Array.from(byName.values());
6705
+ }
6304
6706
  export function buildAgentSchemaContext(question, rows) {
6305
6707
  const byRelation = new Map();
6306
6708
  for (const row of rows) {