@duckcodeailabs/dql-cli 1.6.3 → 1.6.5

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.
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, wat
4
4
  import { homedir } from 'node:os';
5
5
  import { dirname, extname, join, normalize, relative, resolve } from 'node:path';
6
6
  import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
7
- import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, Parser, buildLineageGraph, buildManifest, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, } from '@duckcodeailabs/dql-core';
7
+ import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, getDialect, Parser, buildLineageGraph, buildManifest, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, } from '@duckcodeailabs/dql-core';
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';
@@ -239,12 +239,67 @@ export async function startLocalServer(opts) {
239
239
  FROM information_schema.columns
240
240
  WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
241
241
  ORDER BY table_schema, table_name, ordinal_position`, [], runtimeVariables({}), connection);
242
- return buildAgentSchemaContext(question, result.rows);
242
+ const schemaContext = buildAgentSchemaContext(question, result.rows);
243
+ return enrichAgentSchemaContextWithValueMatches(question, schemaContext, executor, connection);
243
244
  }
244
245
  catch {
245
246
  return [];
246
247
  }
247
248
  };
249
+ const generateInvestigationSqlForApp = async (input) => {
250
+ const resolvedProvider = resolveDefaultLLMProvider(projectRoot);
251
+ const runner = resolvedProvider ? getLLMRunner(resolvedProvider) : null;
252
+ if (!resolvedProvider || !runner) {
253
+ throw new Error('No AI provider is configured. Configure OpenAI, Gemini, Ollama, or a custom OpenAI-compatible endpoint in Settings.');
254
+ }
255
+ let governedAnswer;
256
+ let providerError;
257
+ const contextEnvelope = {
258
+ mode: 'app_research',
259
+ intent: input.intent,
260
+ appId: input.appId,
261
+ dashboardId: input.dashboardId,
262
+ sourceTileId: input.sourceTileId,
263
+ sourceBlockId: input.sourceBlockId,
264
+ title: input.title,
265
+ instruction: 'Generate review-required read-only SQL when certified blocks do not exactly answer the requested research grain. Execute only through the bounded generated SQL preview path.',
266
+ context: input.context,
267
+ };
268
+ const controller = new AbortController();
269
+ await runner.run({
270
+ provider: resolvedProvider,
271
+ messages: [{ role: 'user', content: input.question }],
272
+ upstream: {
273
+ cellId: `app-research:${input.appId}:${input.dashboardId ?? 'app'}`,
274
+ sql: JSON.stringify(contextEnvelope, null, 2),
275
+ },
276
+ projectRoot,
277
+ executeCertifiedBlock: executeCertifiedBlockForAgent,
278
+ executeGeneratedSql: executeGeneratedSqlForAgent,
279
+ getSchemaContext: getSchemaContextForAgent,
280
+ }, (turn) => {
281
+ if (turn.kind === 'tool_result' && turn.id === 'governed_answer') {
282
+ governedAnswer = turn.output;
283
+ }
284
+ if (turn.kind === 'error') {
285
+ providerError = turn.message;
286
+ }
287
+ }, controller.signal);
288
+ if (!governedAnswer) {
289
+ throw new Error(providerError ?? 'The AI provider did not return a governed answer.');
290
+ }
291
+ return {
292
+ sql: governedAnswer.proposedSql ?? governedAnswer.sql,
293
+ answer: governedAnswer.answer ?? governedAnswer.text,
294
+ result: governedAnswer.result,
295
+ analysisPlan: governedAnswer.analysisPlan,
296
+ evidence: governedAnswer.evidence,
297
+ citations: governedAnswer.citations,
298
+ suggestedViz: governedAnswer.suggestedViz,
299
+ executionError: governedAnswer.executionError,
300
+ providerUsed: governedAnswer.providerUsed,
301
+ };
302
+ };
248
303
  // SSE clients for /api/watch hot-reload
249
304
  const sseClients = new Set();
250
305
  // Watch notebooks/, workbooks/, semantic-layer/, and data/ dirs for changes
@@ -303,6 +358,17 @@ export async function startLocalServer(opts) {
303
358
  ...candidate,
304
359
  validation: validateBlockStudioSource(candidate.dqlSource, semanticLayer),
305
360
  });
361
+ const validateImportCandidateForSave = (candidate) => {
362
+ const validated = validateImportCandidate(candidate);
363
+ const diagnostics = (validated.validation?.diagnostics ?? []);
364
+ const errors = diagnostics
365
+ .filter((diagnostic) => diagnostic.severity === 'error')
366
+ .map((diagnostic) => diagnostic.message || 'Candidate validation failed.');
367
+ if (validated.reviewStatus === 'rejected') {
368
+ errors.unshift('Candidate was rejected.');
369
+ }
370
+ return { candidate: validated, errors };
371
+ };
306
372
  const runBlockStudioPreviewSource = async (source, targetConnection = connection) => {
307
373
  let tableMapping;
308
374
  if (semanticLayer) {
@@ -334,10 +400,10 @@ export async function startLocalServer(opts) {
334
400
  throw new Error(message);
335
401
  }
336
402
  const plan = buildExecutionPlan({ id: 'block-studio', type: 'dql', source, title: 'Block Studio' }, { semanticLayer, driver: targetConnection.driver, tableMapping });
337
- const sql = resolveProjectRelativeSqlPaths(semanticCompose?.sql ?? plan?.sql ?? executableSql, projectRoot, projectConfig.dataDir);
338
- const result = await executor.executeQuery(sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}), targetConnection);
403
+ const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan?.sql ?? executableSql, targetConnection, projectRoot, projectConfig);
404
+ const result = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}), prepared.connection);
339
405
  return {
340
- sql: plan?.sql ?? executableSql,
406
+ sql: prepared.sql,
341
407
  result: normalizeQueryResult(result),
342
408
  chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
343
409
  };
@@ -777,6 +843,7 @@ export async function startLocalServer(opts) {
777
843
  path,
778
844
  projectRoot,
779
845
  executeSql: executeLocalSqlForStoredResult,
846
+ generateInvestigationSql: generateInvestigationSqlForApp,
780
847
  runNotebook: (appId, notebookPath) => runNotebookForApp(appId, notebookPath),
781
848
  });
782
849
  if (handled)
@@ -1435,10 +1502,7 @@ export async function startLocalServer(opts) {
1435
1502
  return;
1436
1503
  }
1437
1504
  const result = await certifyBlockStudioSource(source, blockPath);
1438
- const blockers = [
1439
- ...result.checklist.blockers,
1440
- ...result.certification.errors.map((error) => `${error.rule}: ${error.message}`),
1441
- ];
1505
+ const blockers = Array.from(new Set(result.checklist.blockers));
1442
1506
  if (!result.certification.certified || blockers.length > 0) {
1443
1507
  res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
1444
1508
  res.end(serializeJSON({ ok: false, ...result, blockers }));
@@ -1514,25 +1578,32 @@ export async function startLocalServer(opts) {
1514
1578
  const nextCandidates = [...session.candidates];
1515
1579
  for (let i = 0; i < nextCandidates.length; i += 1) {
1516
1580
  const candidate = nextCandidates[i];
1517
- if (candidate.reviewStatus === 'saved' || candidate.reviewStatus === 'rejected' || candidate.validation?.valid === false)
1581
+ if (candidate.reviewStatus === 'saved' || candidate.reviewStatus === 'rejected')
1582
+ continue;
1583
+ const readiness = validateImportCandidateForSave(candidate);
1584
+ nextCandidates[i] = readiness.candidate;
1585
+ writeBlockStudioImportCandidate(projectRoot, importId, readiness.candidate);
1586
+ if (readiness.errors.length > 0) {
1587
+ errors.push({ candidateId: candidate.id, error: readiness.errors.join(' ') });
1518
1588
  continue;
1589
+ }
1519
1590
  try {
1520
1591
  const savedPath = saveBlockStudioArtifacts(projectRoot, {
1521
- source: candidate.dqlSource,
1522
- name: candidate.name,
1523
- domain: candidate.domain,
1524
- description: candidate.description,
1525
- owner: candidate.owner,
1526
- tags: candidate.tags,
1527
- lineage: candidate.lineage.sourceTables,
1592
+ source: readiness.candidate.dqlSource,
1593
+ name: readiness.candidate.name,
1594
+ domain: readiness.candidate.domain,
1595
+ description: readiness.candidate.description,
1596
+ owner: readiness.candidate.owner,
1597
+ tags: readiness.candidate.tags,
1598
+ lineage: readiness.candidate.lineage.sourceTables,
1528
1599
  importMeta: {
1529
1600
  importId,
1530
- candidateId: candidate.id,
1531
- sourceKind: candidate.sourceKind,
1532
- sourcePath: candidate.sourcePath,
1601
+ candidateId: readiness.candidate.id,
1602
+ sourceKind: readiness.candidate.sourceKind,
1603
+ sourcePath: readiness.candidate.sourcePath,
1533
1604
  },
1534
1605
  });
1535
- nextCandidates[i] = { ...candidate, reviewStatus: 'saved', savedPath };
1606
+ nextCandidates[i] = { ...readiness.candidate, reviewStatus: 'saved', savedPath };
1536
1607
  writeBlockStudioImportCandidate(projectRoot, importId, nextCandidates[i]);
1537
1608
  saved.push({ candidateId: candidate.id, path: savedPath });
1538
1609
  }
@@ -1625,22 +1696,39 @@ export async function startLocalServer(opts) {
1625
1696
  }
1626
1697
  if (req.method === 'POST' && candidateId && action === 'save') {
1627
1698
  const candidate = readBlockStudioImportCandidate(projectRoot, importId, candidateId);
1699
+ if (candidate.reviewStatus === 'saved' && candidate.savedPath) {
1700
+ const payload = openBlockStudioDocument(projectRoot, candidate.savedPath, semanticLayer);
1701
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1702
+ res.end(serializeJSON({ candidate, block: payload }));
1703
+ return;
1704
+ }
1705
+ const readiness = validateImportCandidateForSave(candidate);
1706
+ if (readiness.errors.length > 0) {
1707
+ writeBlockStudioImportCandidate(projectRoot, importId, readiness.candidate);
1708
+ res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
1709
+ res.end(serializeJSON({
1710
+ error: readiness.errors.join(' '),
1711
+ candidate: readiness.candidate,
1712
+ diagnostics: readiness.candidate.validation?.diagnostics ?? [],
1713
+ }));
1714
+ return;
1715
+ }
1628
1716
  const savedPath = saveBlockStudioArtifacts(projectRoot, {
1629
- source: candidate.dqlSource,
1630
- name: candidate.name,
1631
- domain: candidate.domain,
1632
- description: candidate.description,
1633
- owner: candidate.owner,
1634
- tags: candidate.tags,
1635
- lineage: candidate.lineage.sourceTables,
1717
+ source: readiness.candidate.dqlSource,
1718
+ name: readiness.candidate.name,
1719
+ domain: readiness.candidate.domain,
1720
+ description: readiness.candidate.description,
1721
+ owner: readiness.candidate.owner,
1722
+ tags: readiness.candidate.tags,
1723
+ lineage: readiness.candidate.lineage.sourceTables,
1636
1724
  importMeta: {
1637
1725
  importId,
1638
1726
  candidateId,
1639
- sourceKind: candidate.sourceKind,
1640
- sourcePath: candidate.sourcePath,
1727
+ sourceKind: readiness.candidate.sourceKind,
1728
+ sourcePath: readiness.candidate.sourcePath,
1641
1729
  },
1642
1730
  });
1643
- const next = { ...candidate, reviewStatus: 'saved', savedPath };
1731
+ const next = { ...readiness.candidate, reviewStatus: 'saved', savedPath };
1644
1732
  writeBlockStudioImportCandidate(projectRoot, importId, next);
1645
1733
  const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
1646
1734
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -1664,11 +1752,8 @@ export async function startLocalServer(opts) {
1664
1752
  if (req.method === 'GET' && path === '/api/block-studio/catalog') {
1665
1753
  try {
1666
1754
  const cfg = loadProjectConfig(projectRoot);
1667
- const connections = cfg.connections ?? {};
1668
- if (Object.keys(connections).length === 0 && cfg.defaultConnection) {
1669
- connections.default = cfg.defaultConnection;
1670
- }
1671
- const defaultKey = cfg.defaultConnection ? 'default' : Object.keys(connections)[0] ?? 'default';
1755
+ const connections = getProjectConnectionsForApi(cfg);
1756
+ const defaultKey = resolveDefaultConnectionKey(cfg, connections) ?? Object.keys(connections)[0] ?? 'default';
1672
1757
  const userPrefs = readUserPrefs(userPrefsPath);
1673
1758
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1674
1759
  res.end(serializeJSON({
@@ -1800,15 +1885,10 @@ export async function startLocalServer(opts) {
1800
1885
  }
1801
1886
  if (req.method === 'GET' && path === '/api/connections') {
1802
1887
  const cfg = loadProjectConfig(projectRoot);
1803
- const raw = cfg;
1804
- const connections = raw.connections ?? {};
1805
- // If no explicit connections map, surface the defaultConnection as "default"
1806
- if (Object.keys(connections).length === 0 && cfg.defaultConnection) {
1807
- connections['default'] = cfg.defaultConnection;
1808
- }
1809
- const defaultKey = raw.defaultConnection
1810
- ? 'default'
1811
- : Object.keys(connections)[0] ?? 'default';
1888
+ const connections = getProjectConnectionsForApi(cfg);
1889
+ const defaultKey = resolveDefaultConnectionKey(cfg, connections)
1890
+ ?? Object.keys(connections)[0]
1891
+ ?? 'default';
1812
1892
  const dbtProfiles = discoverDbtProfileConnections(projectRoot, cfg);
1813
1893
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1814
1894
  res.end(serializeJSON({ default: defaultKey, connections, dbtProfiles }));
@@ -1826,6 +1906,22 @@ export async function startLocalServer(opts) {
1826
1906
  if (body.connections && typeof body.connections === 'object') {
1827
1907
  raw.connections = body.connections;
1828
1908
  }
1909
+ const connections = getStoredConnections(raw);
1910
+ if (body.connections && typeof body.connections === 'object') {
1911
+ const requestedDefault = typeof body.defaultConnectionName === 'string'
1912
+ ? body.defaultConnectionName
1913
+ : typeof body.default === 'string'
1914
+ ? body.default
1915
+ : undefined;
1916
+ const defaultConnectionName = resolveDefaultConnectionKey(requestedDefault ? { ...raw, defaultConnectionName: requestedDefault } : raw, connections);
1917
+ delete raw.defaultConnection;
1918
+ if (defaultConnectionName) {
1919
+ raw.defaultConnectionName = defaultConnectionName;
1920
+ }
1921
+ else {
1922
+ delete raw.defaultConnectionName;
1923
+ }
1924
+ }
1829
1925
  writeFileSync(configPath, JSON.stringify(raw, null, 2) + '\n', 'utf-8');
1830
1926
  // Hot-swap: re-read the config and re-initialize the active connection
1831
1927
  projectConfig = loadProjectConfig(projectRoot);
@@ -2840,23 +2936,20 @@ export async function startLocalServer(opts) {
2840
2936
  return;
2841
2937
  }
2842
2938
  if (req.method === 'POST' && path === '/api/test-connection') {
2939
+ let target = connection;
2843
2940
  try {
2844
2941
  const body = await readJSON(req);
2845
- const target = normalizeProjectConnection(isConnectionConfig(body.connection) ? body.connection : connection, projectRoot);
2942
+ target = normalizeProjectConnection(isConnectionConfig(body.connection) ? body.connection : connection, projectRoot);
2846
2943
  const connector = await executor.getConnector(target);
2847
- const ok = await connector.ping();
2848
- const driver = target.driver ?? 'unknown';
2849
- res.writeHead(ok ? 200 : 400, { 'Content-Type': 'application/json; charset=utf-8' });
2850
- res.end(serializeJSON({
2851
- ok,
2852
- message: ok ? `Connected to ${driver} successfully` : `Connection to ${driver} failed`,
2853
- }));
2944
+ const result = await validateConnectionForTest(connector, target);
2945
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
2946
+ res.end(serializeJSON(result));
2854
2947
  }
2855
2948
  catch (error) {
2856
- res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
2949
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
2857
2950
  res.end(serializeJSON({
2858
2951
  ok: false,
2859
- message: error instanceof Error ? error.message : String(error),
2952
+ message: formatConnectionTestError(target, error),
2860
2953
  }));
2861
2954
  }
2862
2955
  return;
@@ -3229,6 +3322,102 @@ export function formatLocalQueryRuntimeError(connection, error) {
3229
3322
  }
3230
3323
  return `Local query runtime is unavailable for driver "${driver}": ${detail}`;
3231
3324
  }
3325
+ export async function validateConnectionForTest(connector, connection) {
3326
+ if (connection.driver === 'snowflake') {
3327
+ return validateSnowflakeConnectionForTest(connector, connection);
3328
+ }
3329
+ const ok = await connector.ping();
3330
+ const label = connectionDriverLabel(connection);
3331
+ return {
3332
+ ok,
3333
+ message: ok
3334
+ ? `Connected to ${label} successfully.`
3335
+ : `Connection to ${label} failed. Check credentials, network access, and database availability.`,
3336
+ };
3337
+ }
3338
+ function formatConnectionTestError(connection, error) {
3339
+ const detail = error instanceof Error ? error.message : String(error);
3340
+ const label = connectionDriverLabel(connection);
3341
+ if (connection.driver === 'snowflake') {
3342
+ const cleaned = detail.replace(/^Snowflake (?:connection|query) failed:\s*/i, '').trim();
3343
+ return `Snowflake connection failed: ${cleaned || 'Check account, user, password/auth method, role, and network access.'}`;
3344
+ }
3345
+ return `Connection to ${label} failed: ${detail}`;
3346
+ }
3347
+ async function validateSnowflakeConnectionForTest(connector, connection) {
3348
+ const warehouse = connection.warehouse?.trim();
3349
+ if (!warehouse) {
3350
+ return {
3351
+ ok: false,
3352
+ message: 'Snowflake connection requires a warehouse before it can be tested.',
3353
+ };
3354
+ }
3355
+ const warehouseRow = await findSnowflakeWarehouse(connector, warehouse);
3356
+ if (!warehouseRow) {
3357
+ return {
3358
+ ok: false,
3359
+ message: `Snowflake warehouse "${warehouse}" was not found or is not visible to this role.`,
3360
+ };
3361
+ }
3362
+ const state = String(readRowField(warehouseRow, 'state') ?? '').trim();
3363
+ const normalizedState = state.toUpperCase();
3364
+ if (normalizedState && normalizedState !== 'STARTED') {
3365
+ return {
3366
+ ok: false,
3367
+ message: `Snowflake warehouse "${warehouse}" is ${state}. Start or resume it, then test again.`,
3368
+ details: {
3369
+ warehouse,
3370
+ state,
3371
+ },
3372
+ };
3373
+ }
3374
+ const context = await connector.execute(`SELECT
3375
+ CURRENT_ACCOUNT() AS account_name,
3376
+ CURRENT_USER() AS user_name,
3377
+ CURRENT_ROLE() AS role_name,
3378
+ CURRENT_DATABASE() AS database_name,
3379
+ CURRENT_SCHEMA() AS schema_name,
3380
+ CURRENT_WAREHOUSE() AS warehouse_name`);
3381
+ const row = context.rows[0] ?? {};
3382
+ const user = String(readRowField(row, 'user_name') ?? connection.username ?? '').trim();
3383
+ const role = String(readRowField(row, 'role_name') ?? connection.role ?? '').trim();
3384
+ const activeWarehouse = String(readRowField(row, 'warehouse_name') ?? warehouse).trim();
3385
+ return {
3386
+ ok: true,
3387
+ message: `Connected to Snowflake${user ? ` as ${user}` : ''} using warehouse ${activeWarehouse || warehouse}.`,
3388
+ details: {
3389
+ warehouse: activeWarehouse || warehouse,
3390
+ warehouseState: state || 'STARTED',
3391
+ role: role || undefined,
3392
+ database: readRowField(row, 'database_name') ?? connection.database,
3393
+ schema: readRowField(row, 'schema_name') ?? connection.schema,
3394
+ },
3395
+ };
3396
+ }
3397
+ async function findSnowflakeWarehouse(connector, warehouse) {
3398
+ const candidates = Array.from(new Set([warehouse, warehouse.toUpperCase()]));
3399
+ for (const candidate of candidates) {
3400
+ const result = await connector.execute(`SHOW WAREHOUSES LIKE '${escapeSqlString(candidate)}'`);
3401
+ const row = result.rows.find((item) => {
3402
+ const name = String(readRowField(item, 'name') ?? '').trim();
3403
+ return name.localeCompare(warehouse, undefined, { sensitivity: 'accent' }) === 0;
3404
+ });
3405
+ if (row)
3406
+ return row;
3407
+ }
3408
+ return null;
3409
+ }
3410
+ function readRowField(row, field) {
3411
+ const expected = field.toLowerCase();
3412
+ const entry = Object.entries(row).find(([key]) => key.toLowerCase() === expected);
3413
+ return entry?.[1];
3414
+ }
3415
+ function escapeSqlString(value) {
3416
+ return value.replace(/'/g, "''");
3417
+ }
3418
+ function connectionDriverLabel(connection) {
3419
+ return connection.driver === 'snowflake' ? 'Snowflake' : connection.driver ?? 'database';
3420
+ }
3232
3421
  /**
3233
3422
  * Normalize connector QueryResult → SPA-friendly shape.
3234
3423
  * Connector returns columns as ColumnMeta[] ({name,type,driverType}).
@@ -3361,31 +3550,203 @@ export function loadProjectConfig(projectRoot) {
3361
3550
  }
3362
3551
  const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
3363
3552
  const config = raw;
3364
- // Normalize modern `connections.default` format to `defaultConnection`
3365
- if (!config.defaultConnection && raw.connections) {
3366
- const connections = raw.connections;
3367
- const defaultConn = connections.default;
3368
- if (defaultConn?.driver) {
3369
- // Support both `filepath` (correct) and `path` (legacy/init compat)
3370
- const filepath = (defaultConn.filepath ?? defaultConn.path);
3371
- config.defaultConnection = {
3372
- ...defaultConn,
3373
- driver: defaultConn.driver,
3374
- ...(filepath ? { filepath } : {}),
3375
- };
3553
+ const connections = getStoredConnections(raw);
3554
+ const defaultConnectionName = resolveDefaultConnectionKey(raw, connections);
3555
+ if (defaultConnectionName) {
3556
+ const selected = normalizeStoredConnection(connections[defaultConnectionName]);
3557
+ if (selected) {
3558
+ config.defaultConnection = selected;
3559
+ config.defaultConnectionName = defaultConnectionName;
3560
+ }
3561
+ }
3562
+ else if (config.defaultConnection) {
3563
+ const normalized = normalizeStoredConnection(config.defaultConnection);
3564
+ if (normalized) {
3565
+ config.defaultConnection = normalized;
3376
3566
  }
3377
3567
  }
3378
3568
  return config;
3379
3569
  }
3570
+ function getProjectConnectionsForApi(config) {
3571
+ const connections = getStoredConnections(config);
3572
+ if (Object.keys(connections).length === 0 && isConnectionLike(config.defaultConnection)) {
3573
+ return { default: config.defaultConnection };
3574
+ }
3575
+ return connections;
3576
+ }
3577
+ function getStoredConnections(raw) {
3578
+ const value = raw.connections;
3579
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
3580
+ return {};
3581
+ }
3582
+ return { ...value };
3583
+ }
3584
+ function resolveDefaultConnectionKey(raw, connections) {
3585
+ const keys = Object.keys(connections).filter((key) => isConnectionLike(connections[key]));
3586
+ if (keys.length === 0)
3587
+ return undefined;
3588
+ const configured = readConfiguredDefaultConnectionName(raw);
3589
+ if (configured && keys.includes(configured)) {
3590
+ return configured;
3591
+ }
3592
+ if (keys.includes('default') && !isPlaceholderLocalConnection(connections.default)) {
3593
+ return 'default';
3594
+ }
3595
+ const realConnections = keys.filter((key) => !isPlaceholderLocalConnection(connections[key]));
3596
+ if (keys.includes('default') && isPlaceholderLocalConnection(connections.default) && realConnections.length === 1) {
3597
+ return realConnections[0];
3598
+ }
3599
+ if (keys.length === 1) {
3600
+ return keys[0];
3601
+ }
3602
+ return keys.includes('default') ? 'default' : keys[0];
3603
+ }
3604
+ function readConfiguredDefaultConnectionName(raw) {
3605
+ for (const key of ['defaultConnectionName', 'defaultConnectionKey', 'currentConnection']) {
3606
+ const value = raw[key];
3607
+ if (typeof value === 'string' && value.trim())
3608
+ return value.trim();
3609
+ }
3610
+ return typeof raw.default === 'string' && raw.default.trim() ? raw.default.trim() : undefined;
3611
+ }
3612
+ function normalizeStoredConnection(value) {
3613
+ if (!value || typeof value !== 'object' || Array.isArray(value))
3614
+ return null;
3615
+ const raw = value;
3616
+ const driver = raw.driver ?? raw.type;
3617
+ if (typeof driver !== 'string' || !driver.trim())
3618
+ return null;
3619
+ const { path: legacyPath, type: _type, ...rest } = raw;
3620
+ const filepath = typeof raw.filepath === 'string'
3621
+ ? raw.filepath
3622
+ : typeof legacyPath === 'string'
3623
+ ? legacyPath
3624
+ : undefined;
3625
+ return {
3626
+ ...rest,
3627
+ driver: driver.trim(),
3628
+ ...(filepath ? { filepath } : {}),
3629
+ };
3630
+ }
3631
+ function isConnectionLike(value) {
3632
+ return normalizeStoredConnection(value) !== null;
3633
+ }
3634
+ function isPlaceholderLocalConnection(value) {
3635
+ const connection = normalizeStoredConnection(value);
3636
+ if (!connection)
3637
+ return false;
3638
+ if (connection.driver !== 'duckdb' && connection.driver !== 'file')
3639
+ return false;
3640
+ return !connection.filepath || connection.filepath === ':memory:';
3641
+ }
3380
3642
  export function prepareLocalExecution(sql, connection, projectRoot, projectConfig) {
3381
3643
  const normalizedConnection = normalizeProjectConnection(connection, projectRoot);
3644
+ const dbtResolvedSql = resolveDbtMacrosForExecution(sql, projectRoot, projectConfig);
3382
3645
  return {
3383
3646
  sql: shouldResolveProjectPaths(normalizedConnection)
3384
- ? resolveProjectRelativeSqlPaths(sql, projectRoot, projectConfig.dataDir)
3385
- : sql,
3647
+ ? resolveProjectRelativeSqlPaths(dbtResolvedSql, projectRoot, projectConfig.dataDir)
3648
+ : dbtResolvedSql,
3386
3649
  connection: normalizedConnection,
3387
3650
  };
3388
3651
  }
3652
+ export function resolveDbtMacrosForExecution(sql, projectRoot, projectConfig = {}) {
3653
+ if (!/\{\{\s*(?:ref|source)\s*\(/i.test(sql))
3654
+ return sql;
3655
+ const manifestPath = resolveDbtManifestPath(projectRoot, projectConfig);
3656
+ if (!manifestPath) {
3657
+ throw new Error('dbt ref/source macros were found, but target/manifest.json was not available. Run dbt parse or dbt compile, then retry.');
3658
+ }
3659
+ const manifest = readJsonFile(manifestPath);
3660
+ const refs = buildDbtRelationLookup(manifest);
3661
+ const unresolved = new Set();
3662
+ let rendered = sql.replace(/\{\{\s*ref\(\s*(?:(['"])([^'"]+)\1\s*,\s*)?(['"])([^'"]+)\3(?:\s*,[^)]*)?\)\s*\}\}/gi, (match, _pkgQuote, packageName, _modelQuote, modelName) => {
3663
+ const key = normalizeDbtLookupKey(modelName);
3664
+ const scopedKey = packageName ? normalizeDbtLookupKey(`${packageName}.${modelName}`) : key;
3665
+ const relation = refs.models.get(scopedKey) ?? refs.models.get(key);
3666
+ if (!relation) {
3667
+ unresolved.add(packageName ? `ref('${packageName}', '${modelName}')` : `ref('${modelName}')`);
3668
+ return match;
3669
+ }
3670
+ return relation;
3671
+ });
3672
+ rendered = rendered.replace(/\{\{\s*source\(\s*(['"])([^'"]+)\1\s*,\s*(['"])([^'"]+)\3\s*\)\s*\}\}/gi, (match, _sourceQuote, sourceName, _tableQuote, tableName) => {
3673
+ const key = normalizeDbtLookupKey(`${sourceName}.${tableName}`);
3674
+ const relation = refs.sources.get(key) ?? refs.sources.get(normalizeDbtLookupKey(tableName));
3675
+ if (!relation) {
3676
+ unresolved.add(`source('${sourceName}', '${tableName}')`);
3677
+ return match;
3678
+ }
3679
+ return relation;
3680
+ });
3681
+ if (unresolved.size > 0) {
3682
+ throw new Error(`Could not resolve dbt macro${unresolved.size === 1 ? '' : 's'} from manifest.json: ${Array.from(unresolved).join(', ')}.`);
3683
+ }
3684
+ return rendered;
3685
+ }
3686
+ function buildDbtRelationLookup(manifest) {
3687
+ const models = new Map();
3688
+ const sources = new Map();
3689
+ const root = manifest && typeof manifest === 'object' ? manifest : {};
3690
+ const nodes = root.nodes && typeof root.nodes === 'object' ? root.nodes : {};
3691
+ const manifestSources = root.sources && typeof root.sources === 'object' ? root.sources : {};
3692
+ for (const [uniqueId, rawNode] of Object.entries(nodes)) {
3693
+ const node = rawNode && typeof rawNode === 'object' ? rawNode : null;
3694
+ if (!node || node.resource_type !== 'model')
3695
+ continue;
3696
+ const relation = dbtRelationName(node);
3697
+ if (!relation)
3698
+ continue;
3699
+ const name = stringField(node, 'name');
3700
+ const alias = stringField(node, 'alias');
3701
+ const packageName = uniqueId.split('.')[1];
3702
+ for (const key of [name, alias, packageName && name ? `${packageName}.${name}` : null, uniqueId]) {
3703
+ if (key)
3704
+ models.set(normalizeDbtLookupKey(key), relation);
3705
+ }
3706
+ }
3707
+ for (const [uniqueId, rawSource] of Object.entries(manifestSources)) {
3708
+ const source = rawSource && typeof rawSource === 'object' ? rawSource : null;
3709
+ if (!source)
3710
+ continue;
3711
+ const relation = dbtRelationName(source);
3712
+ if (!relation)
3713
+ continue;
3714
+ const sourceName = stringField(source, 'source_name');
3715
+ const name = stringField(source, 'name');
3716
+ const identifier = stringField(source, 'identifier');
3717
+ for (const key of [
3718
+ sourceName && name ? `${sourceName}.${name}` : null,
3719
+ sourceName && identifier ? `${sourceName}.${identifier}` : null,
3720
+ name,
3721
+ identifier,
3722
+ uniqueId,
3723
+ ]) {
3724
+ if (key)
3725
+ sources.set(normalizeDbtLookupKey(key), relation);
3726
+ }
3727
+ }
3728
+ return { models, sources };
3729
+ }
3730
+ function dbtRelationName(node) {
3731
+ const relationName = stringField(node, 'relation_name');
3732
+ if (relationName)
3733
+ return relationName;
3734
+ const database = stringField(node, 'database');
3735
+ const schema = stringField(node, 'schema');
3736
+ const alias = stringField(node, 'alias') ?? stringField(node, 'identifier') ?? stringField(node, 'name');
3737
+ if (database && schema && alias)
3738
+ return `${database}.${schema}.${alias}`;
3739
+ if (schema && alias)
3740
+ return `${schema}.${alias}`;
3741
+ return alias ?? null;
3742
+ }
3743
+ function stringField(source, key) {
3744
+ const value = source[key];
3745
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
3746
+ }
3747
+ function normalizeDbtLookupKey(value) {
3748
+ return value.trim().replace(/^['"]|['"]$/g, '').toLowerCase();
3749
+ }
3389
3750
  const AGENT_PREVIEW_FORBIDDEN_SQL = [
3390
3751
  'alter',
3391
3752
  'analyze',
@@ -3416,19 +3777,25 @@ export function buildAgentPreviewSql(sql) {
3416
3777
  if (!trimmed)
3417
3778
  throw new Error('Generated SQL preview is empty.');
3418
3779
  const withoutTrailingSemicolon = trimmed.replace(/;\s*$/, '').trim();
3419
- const scanSql = stripSqlStringsAndComments(withoutTrailingSemicolon).trim();
3780
+ const readOnlyError = readOnlySqlValidationError(withoutTrailingSemicolon, 'Generated SQL preview');
3781
+ if (readOnlyError)
3782
+ throw new Error(readOnlyError);
3783
+ return `SELECT * FROM (\n${withoutTrailingSemicolon}\n) AS dql_agent_preview LIMIT 200`;
3784
+ }
3785
+ function readOnlySqlValidationError(sql, subject) {
3786
+ const scanSql = stripSqlStringsAndComments(sql).trim();
3420
3787
  if (!/^(select|with)\b/i.test(scanSql)) {
3421
- throw new Error('Generated SQL preview only supports read-only SELECT or WITH queries.');
3788
+ return `${subject} only supports read-only SELECT or WITH queries.`;
3422
3789
  }
3423
3790
  if (scanSql.includes(';')) {
3424
- throw new Error('Generated SQL preview only supports one statement.');
3791
+ return `${subject} only supports one statement.`;
3425
3792
  }
3426
3793
  const forbiddenPattern = new RegExp(`\\b(${AGENT_PREVIEW_FORBIDDEN_SQL.join('|')})\\b`, 'i');
3427
3794
  const forbidden = scanSql.match(forbiddenPattern)?.[1];
3428
3795
  if (forbidden) {
3429
- throw new Error(`Generated SQL preview rejected unsupported statement keyword: ${forbidden.toUpperCase()}.`);
3796
+ return `${subject} rejected unsupported statement keyword: ${forbidden.toUpperCase()}.`;
3430
3797
  }
3431
- return `SELECT * FROM (\n${withoutTrailingSemicolon}\n) AS dql_agent_preview LIMIT 200`;
3798
+ return null;
3432
3799
  }
3433
3800
  function stripSqlStringsAndComments(sql) {
3434
3801
  let output = '';
@@ -4211,6 +4578,16 @@ export function validateBlockStudioSource(source, semanticLayer) {
4211
4578
  diagnostics.push(...resolvedCustomSql.diagnostics);
4212
4579
  executableSql = resolvedCustomSql.sql;
4213
4580
  }
4581
+ if (executableSql && semanticConfig.blockType !== 'semantic') {
4582
+ const readOnlyError = readOnlySqlValidationError(executableSql.trim().replace(/;\s*$/, '').trim(), 'Block SQL');
4583
+ if (readOnlyError) {
4584
+ diagnostics.push({
4585
+ severity: 'error',
4586
+ code: 'sql_read_only',
4587
+ message: readOnlyError,
4588
+ });
4589
+ }
4590
+ }
4214
4591
  const chartConfig = extractBlockStudioChartConfig(source);
4215
4592
  if (!chartConfig) {
4216
4593
  diagnostics.push({
@@ -4240,7 +4617,7 @@ export function validateBlockStudioSource(source, semanticLayer) {
4240
4617
  executableSql,
4241
4618
  };
4242
4619
  }
4243
- function saveBlockStudioArtifacts(projectRoot, options) {
4620
+ export function saveBlockStudioArtifacts(projectRoot, options) {
4244
4621
  const slug = options.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'block';
4245
4622
  const safeDomain = (options.domain ?? '')
4246
4623
  .trim()
@@ -4392,12 +4769,6 @@ function buildBlockStudioCertificationChecklist(input) {
4392
4769
  blockers.add(`${error.rule}: ${error.message}`);
4393
4770
  for (const blocker of input.extraBlockers ?? [])
4394
4771
  blockers.add(blocker);
4395
- if (!parsed.domain.trim())
4396
- blockers.add('Missing domain');
4397
- if (!parsed.owner.trim())
4398
- blockers.add('Missing owner');
4399
- if (!parsed.description.trim())
4400
- blockers.add('Missing description');
4401
4772
  if (!input.previewSucceeded)
4402
4773
  blockers.add('Block has not run successfully');
4403
4774
  if (!input.testResults || input.testResults.failed > 0)
@@ -5092,7 +5463,7 @@ function buildProjectLineageGraphUncached(projectRoot, semanticLayer) {
5092
5463
  // Fall back to a live build.
5093
5464
  }
5094
5465
  }
5095
- const dbtManifestPath = resolveDbtManifestPath(projectRoot);
5466
+ const dbtManifestPath = resolveDbtManifestPath(projectRoot, {});
5096
5467
  try {
5097
5468
  const manifest = buildManifest({
5098
5469
  projectRoot,
@@ -5149,9 +5520,14 @@ function buildProjectLineageGraphUncached(projectRoot, semanticLayer) {
5149
5520
  return buildLineageGraph(blocks, metrics, dimensions);
5150
5521
  }
5151
5522
  }
5152
- function resolveDbtManifestPath(projectRoot) {
5153
- const candidate = join(projectRoot, 'target', 'manifest.json');
5154
- return existsSync(candidate) ? candidate : undefined;
5523
+ function resolveDbtManifestPath(projectRoot, projectConfig = {}) {
5524
+ const candidates = [];
5525
+ if (projectConfig.dbt?.projectDir || projectConfig.semanticLayer?.provider === 'dbt') {
5526
+ const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
5527
+ candidates.push(resolve(dbtProjectPath, projectConfig.dbt?.manifestPath ?? 'target/manifest.json'));
5528
+ }
5529
+ 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'));
5530
+ return candidates.find((candidate, index, list) => list.indexOf(candidate) === index && existsSync(candidate));
5155
5531
  }
5156
5532
  export function discoverDbtProfileConnections(projectRoot, projectConfig) {
5157
5533
  const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
@@ -6113,7 +6489,7 @@ function isAiPinRefreshDue(lastRefreshedAt) {
6113
6489
  return true;
6114
6490
  return Date.now() - last >= 24 * 60 * 60 * 1000;
6115
6491
  }
6116
- function buildAgentSchemaContext(question, rows) {
6492
+ export function buildAgentSchemaContext(question, rows) {
6117
6493
  const byRelation = new Map();
6118
6494
  for (const row of rows) {
6119
6495
  if (!row || typeof row !== 'object')
@@ -6141,13 +6517,54 @@ function buildAgentSchemaContext(question, rows) {
6141
6517
  byRelation.set(relation, current);
6142
6518
  }
6143
6519
  const tokens = agentSchemaTokens(question);
6520
+ const shouldProbeValues = extractAgentValueSearchTerms(question).length > 0;
6144
6521
  return Array.from(byRelation.values())
6145
- .map((table) => ({ table, score: scoreAgentSchemaTable(table, tokens) }))
6522
+ .map((table) => ({
6523
+ table,
6524
+ score: scoreAgentSchemaTable(table, tokens) + (shouldProbeValues ? scoreAgentValueProbeTable(table) : 0),
6525
+ }))
6146
6526
  .filter((entry) => entry.score > 0)
6147
6527
  .sort((a, b) => b.score - a.score || a.table.relation.localeCompare(b.table.relation))
6148
6528
  .slice(0, 12)
6149
6529
  .map((entry) => entry.table);
6150
6530
  }
6531
+ async function enrichAgentSchemaContextWithValueMatches(question, schemaContext, executor, connection) {
6532
+ const searchTerms = extractAgentValueSearchTerms(question);
6533
+ if (schemaContext.length === 0 || searchTerms.length === 0)
6534
+ return schemaContext;
6535
+ const matches = new Map();
6536
+ for (const candidate of rankAgentValueProbeColumns(schemaContext).slice(0, 12)) {
6537
+ try {
6538
+ const result = await executor.executeQuery(buildAgentValueProbeSql(candidate.table, candidate.column.name, searchTerms, connection), [], runtimeVariables({}), connection);
6539
+ const values = uniqueStrings(result.rows.flatMap(valueProbeRowValues)).slice(0, 5);
6540
+ if (values.length === 0)
6541
+ continue;
6542
+ const tableMatches = matches.get(candidate.table.relation) ?? new Map();
6543
+ tableMatches.set(candidate.column.name, values);
6544
+ matches.set(candidate.table.relation, tableMatches);
6545
+ }
6546
+ catch {
6547
+ // Value probes are advisory. Unsupported casts, privileges, and large-table
6548
+ // failures should not block the metadata-backed answer path.
6549
+ }
6550
+ }
6551
+ if (matches.size === 0)
6552
+ return schemaContext;
6553
+ return schemaContext.map((table) => {
6554
+ const tableMatches = matches.get(table.relation);
6555
+ if (!tableMatches)
6556
+ return table;
6557
+ return {
6558
+ ...table,
6559
+ columns: table.columns.map((column) => {
6560
+ const sampleValues = tableMatches.get(column.name);
6561
+ return sampleValues?.length
6562
+ ? { ...column, sampleValues: uniqueStrings([...(column.sampleValues ?? []), ...sampleValues]).slice(0, 5) }
6563
+ : column;
6564
+ }),
6565
+ };
6566
+ });
6567
+ }
6151
6568
  function scoreAgentSchemaTable(table, tokens) {
6152
6569
  let score = 0;
6153
6570
  const relationTokens = agentSchemaTokens(`${table.schema ?? ''} ${table.name} ${table.relation}`);
@@ -6166,6 +6583,146 @@ function scoreAgentSchemaTable(table, tokens) {
6166
6583
  score += 1;
6167
6584
  return score;
6168
6585
  }
6586
+ function scoreAgentValueProbeTable(table) {
6587
+ let score = 0;
6588
+ if (hasAgentSchemaToken(table.name, ['account', 'customer', 'member', 'order', 'product', 'sku', 'subscriber', 'user']))
6589
+ score += 5;
6590
+ for (const column of table.columns) {
6591
+ if (!isAgentValueProbeColumn(column))
6592
+ continue;
6593
+ score += 2;
6594
+ if (hasAgentSchemaToken(column.name, ['account', 'customer', 'email', 'full', 'member', 'name', 'product', 'sku', 'user']))
6595
+ score += 2;
6596
+ }
6597
+ return Math.min(score, 18);
6598
+ }
6599
+ function rankAgentValueProbeColumns(schemaContext) {
6600
+ const ranked = [];
6601
+ for (const table of schemaContext) {
6602
+ for (const column of table.columns) {
6603
+ if (!isAgentValueProbeColumn(column))
6604
+ continue;
6605
+ ranked.push({
6606
+ table,
6607
+ column,
6608
+ score: scoreAgentValueProbeColumn(table, column),
6609
+ });
6610
+ }
6611
+ }
6612
+ return ranked.sort((a, b) => b.score - a.score || a.table.relation.localeCompare(b.table.relation) || a.column.name.localeCompare(b.column.name));
6613
+ }
6614
+ function scoreAgentValueProbeColumn(table, column) {
6615
+ let score = 0;
6616
+ if (hasAgentSchemaToken(table.name, ['account', 'customer', 'member', 'product', 'sku', 'subscriber', 'user']))
6617
+ score += 4;
6618
+ if (hasAgentSchemaToken(column.name, ['full', 'name', 'email', 'account', 'customer', 'member', 'product', 'sku', 'subscriber', 'user']))
6619
+ score += 8;
6620
+ if (hasAgentSchemaToken(column.name, ['id', 'key', 'code', 'number', 'status', 'segment', 'region', 'category', 'type']))
6621
+ score += 3;
6622
+ return score;
6623
+ }
6624
+ function isAgentValueProbeColumn(column) {
6625
+ const name = column.name.toLowerCase();
6626
+ if (/\b(password|secret|token|credential|hash|salt)\b/.test(name))
6627
+ return false;
6628
+ if (!hasAgentSchemaToken(name, [
6629
+ 'account',
6630
+ 'category',
6631
+ 'channel',
6632
+ 'city',
6633
+ 'code',
6634
+ 'country',
6635
+ 'customer',
6636
+ 'email',
6637
+ 'full',
6638
+ 'id',
6639
+ 'key',
6640
+ 'member',
6641
+ 'name',
6642
+ 'number',
6643
+ 'product',
6644
+ 'region',
6645
+ 'segment',
6646
+ 'sku',
6647
+ 'state',
6648
+ 'status',
6649
+ 'subscriber',
6650
+ 'type',
6651
+ 'user',
6652
+ ])) {
6653
+ return false;
6654
+ }
6655
+ const type = column.type?.toLowerCase() ?? '';
6656
+ if (!type)
6657
+ return true;
6658
+ return /\b(char|character|clob|email|string|text|uuid|varchar)\b/.test(type);
6659
+ }
6660
+ function buildAgentValueProbeSql(table, column, searchTerms, connection) {
6661
+ const relation = quoteAgentRelation(table.relation, connection);
6662
+ const identifier = quoteAgentIdentifier(column, connection);
6663
+ const castValue = `LOWER(CAST(${identifier} AS ${agentTextCastType(connection.driver)}))`;
6664
+ const predicates = searchTerms
6665
+ .slice(0, 5)
6666
+ .map((term) => `${castValue} LIKE ${sqlStringLiteral(`%${escapeSqlLike(term.toLowerCase())}%`)} ESCAPE '\\\\'`)
6667
+ .join(' OR ');
6668
+ return [
6669
+ `SELECT DISTINCT CAST(${identifier} AS ${agentTextCastType(connection.driver)}) AS value`,
6670
+ `FROM ${relation}`,
6671
+ `WHERE ${identifier} IS NOT NULL AND (${predicates})`,
6672
+ 'LIMIT 5',
6673
+ ].join('\n');
6674
+ }
6675
+ function agentTextCastType(driver) {
6676
+ switch (driver) {
6677
+ case 'bigquery':
6678
+ return 'STRING';
6679
+ case 'clickhouse':
6680
+ return 'String';
6681
+ case 'fabric':
6682
+ case 'mssql':
6683
+ return 'NVARCHAR(MAX)';
6684
+ case 'mysql':
6685
+ return 'CHAR';
6686
+ case 'sqlite':
6687
+ return 'TEXT';
6688
+ default:
6689
+ return 'VARCHAR';
6690
+ }
6691
+ }
6692
+ function quoteAgentRelation(relation, connection) {
6693
+ return relation.split('.').map((part) => quoteAgentIdentifier(part, connection)).join('.');
6694
+ }
6695
+ function quoteAgentIdentifier(identifier, connection) {
6696
+ return getDialect(connection.driver).quoteIdentifier(identifier);
6697
+ }
6698
+ function sqlStringLiteral(value) {
6699
+ return `'${value.replace(/'/g, "''")}'`;
6700
+ }
6701
+ function escapeSqlLike(value) {
6702
+ return value.replace(/[\\%_]/g, (match) => `\\${match}`);
6703
+ }
6704
+ function valueProbeRowValues(row) {
6705
+ if (!row || typeof row !== 'object')
6706
+ return [];
6707
+ const record = row;
6708
+ return Object.values(record)
6709
+ .filter((value) => (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'))
6710
+ .map(String)
6711
+ .map((value) => value.trim())
6712
+ .filter(Boolean);
6713
+ }
6714
+ function uniqueStrings(values) {
6715
+ const seen = new Set();
6716
+ const output = [];
6717
+ for (const value of values) {
6718
+ const normalized = value.toLowerCase();
6719
+ if (seen.has(normalized))
6720
+ continue;
6721
+ seen.add(normalized);
6722
+ output.push(value);
6723
+ }
6724
+ return output;
6725
+ }
6169
6726
  function agentSchemaTokens(value) {
6170
6727
  const tokens = new Set();
6171
6728
  for (const raw of value.toLowerCase().match(/[a-z0-9_]+/g) ?? []) {
@@ -6178,10 +6735,60 @@ function agentSchemaTokens(value) {
6178
6735
  }
6179
6736
  return tokens;
6180
6737
  }
6738
+ function hasAgentSchemaToken(value, expected) {
6739
+ const tokens = agentSchemaTokens(value);
6740
+ return expected.some((token) => tokens.has(token));
6741
+ }
6742
+ export function extractAgentValueSearchTerms(question) {
6743
+ const terms = [];
6744
+ for (const match of question.matchAll(/["']([^"']{3,120})["']/g)) {
6745
+ terms.push(match[1]);
6746
+ }
6747
+ for (const match of question.matchAll(/\b[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}\b/g)) {
6748
+ terms.push(match[0]);
6749
+ }
6750
+ for (const match of question.matchAll(/\b[A-Z][a-z0-9]+(?:\s+[A-Z][a-z0-9]+){1,3}\b/g)) {
6751
+ terms.push(match[0]);
6752
+ }
6753
+ for (const match of question.matchAll(/\b(?:for|named|called|only|where|customer|user|account|product)\s+([A-Za-z0-9@._-]+(?:\s+[A-Za-z0-9@._-]+){0,3})/gi)) {
6754
+ terms.push(match[1]);
6755
+ }
6756
+ return uniqueStrings(terms
6757
+ .map(cleanAgentValueSearchTerm)
6758
+ .filter((term) => term.length >= 3 && !AGENT_VALUE_SEARCH_STOP_PHRASES.has(term.toLowerCase()))).slice(0, 6);
6759
+ }
6760
+ function cleanAgentValueSearchTerm(term) {
6761
+ return term
6762
+ .replace(/[?.,;:]+$/g, '')
6763
+ .replace(/\s+/g, ' ')
6764
+ .trim()
6765
+ .replace(/^(?:account|customer|member|named|called|product|sku|subscriber|user)\s+/i, '')
6766
+ .replace(/\s+\b(?:last|next|this)\b.*$/i, '')
6767
+ .replace(/\s+\b(?:last|this)\s+(?:day|week|month|quarter|year)\b.*$/i, '')
6768
+ .replace(/\s+\b(?:daily|weekly|monthly|quarterly|yearly)\b.*$/i, '')
6769
+ .trim();
6770
+ }
6181
6771
  const AGENT_SCHEMA_STOPWORDS = new Set([
6182
6772
  'all', 'and', 'are', 'can', 'data', 'for', 'from', 'have', 'how', 'many', 'me',
6183
6773
  'show', 'the', 'this', 'who', 'with', 'value',
6184
6774
  ]);
6775
+ const AGENT_VALUE_SEARCH_STOP_PHRASES = new Set([
6776
+ 'account',
6777
+ 'customer',
6778
+ 'last week',
6779
+ 'this week',
6780
+ 'last month',
6781
+ 'this month',
6782
+ 'last quarter',
6783
+ 'this quarter',
6784
+ 'last year',
6785
+ 'this year',
6786
+ 'member',
6787
+ 'product',
6788
+ 'sku',
6789
+ 'subscriber',
6790
+ 'user',
6791
+ ]);
6185
6792
  function normalizeAgentSchemaToken(token) {
6186
6793
  if (token === 'orders')
6187
6794
  return 'order';