@duckcodeailabs/dql-cli 1.8.0 → 1.8.2

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.
@@ -16,7 +16,7 @@ import { listBlockTemplates } from './block-templates.js';
16
16
  import { getRunner as getLLMRunner } from './llm/index.js';
17
17
  import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
18
18
  import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
19
- import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, buildLocalContextPack, toAgentRetrievalEvidence, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, propose, proposePlan, recordCorrectionTrace, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine, FileAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, reindexProject, invalidateAgentProjectState, resolveDomainContextEnvelope, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, } from '@duckcodeailabs/dql-agent';
19
+ import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, buildLocalContextPack, toAgentRetrievalEvidence, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, propose, proposePlan, recordCorrectionTrace, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine, FileAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, } from '@duckcodeailabs/dql-agent';
20
20
  import { gatherProposeEnrichment } from './propose-enrich.js';
21
21
  import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
22
22
  import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
@@ -36,18 +36,26 @@ const NOTEBOOK_FAVICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0
36
36
  export function resolveProjectSemanticConfig(projectConfig, projectRoot) {
37
37
  const configured = projectConfig.semanticLayer;
38
38
  const dbtProjectDir = projectConfig.dbt?.projectDir;
39
+ const dbtManifestPath = projectConfig.dbt?.manifestPath;
40
+ if (configured?.provider === 'dbt') {
41
+ return {
42
+ ...configured,
43
+ projectPath: configured.projectPath ?? dbtProjectDir,
44
+ manifestPath: configured.manifestPath ?? dbtManifestPath,
45
+ };
46
+ }
39
47
  if (dbtProjectDir
40
48
  && (!configured || configured.provider === 'dql')
41
- && hasDbtSemanticArtifacts(projectRoot, dbtProjectDir)) {
42
- return { provider: 'dbt', projectPath: dbtProjectDir };
49
+ && hasDbtSemanticArtifacts(projectRoot, dbtProjectDir, dbtManifestPath)) {
50
+ return { provider: 'dbt', projectPath: dbtProjectDir, manifestPath: dbtManifestPath };
43
51
  }
44
52
  return configured;
45
53
  }
46
- function hasDbtSemanticArtifacts(projectRoot, dbtProjectDir) {
54
+ function hasDbtSemanticArtifacts(projectRoot, dbtProjectDir, configuredManifestPath) {
47
55
  const dbtRoot = resolve(projectRoot, dbtProjectDir);
48
- if (existsSync(join(dbtRoot, 'target', 'semantic_manifest.json')))
56
+ const manifestPath = resolve(dbtRoot, configuredManifestPath ?? 'target/manifest.json');
57
+ if (existsSync(join(dirname(manifestPath), 'semantic_manifest.json')))
49
58
  return true;
50
- const manifestPath = join(dbtRoot, 'target', 'manifest.json');
51
59
  if (!existsSync(manifestPath))
52
60
  return false;
53
61
  try {
@@ -137,6 +145,7 @@ export function parseAgentRunRequestBody(body) {
137
145
  return {
138
146
  request: {
139
147
  question,
148
+ selectedEvidenceId: agentRunString(record.selectedEvidenceId),
140
149
  requestedMode,
141
150
  audience,
142
151
  intent: agentRunString(record.intent),
@@ -189,6 +198,10 @@ export function shouldSynthesizeAgentRunAnswer(governedAnswer) {
189
198
  return false;
190
199
  return true;
191
200
  }
201
+ export function agentAnswerHasExecutionFailure(governedAnswer) {
202
+ return typeof governedAnswer.executionError === 'string'
203
+ && governedAnswer.executionError.trim().length > 0;
204
+ }
192
205
  function businessNarrativeGaps(warnings) {
193
206
  const businessRelevant = (warnings ?? []).filter((warning) => !(/missing requested output column/i.test(warning)
194
207
  || /\bquery plan\b/i.test(warning)
@@ -272,16 +285,20 @@ function recordConversationTurn(store, threadId, run) {
272
285
  // Conversation persistence is additive; a failed write must not fail the run.
273
286
  }
274
287
  }
275
- function conversationTurnInputFromRun(run) {
288
+ export function conversationTurnInputFromRun(run) {
276
289
  const artifact = run.artifacts.find((candidate) => candidate.kind === 'answer')
277
290
  ?? run.artifacts.find((candidate) => candidate.kind === 'research_run')
278
291
  ?? run.artifacts[0];
279
292
  const payload = agentRunRecord(artifact?.payload);
280
293
  const result = agentRunRecord(payload?.result);
281
294
  const columns = conversationResultColumns(result?.columns);
282
- const rows = Array.isArray(result?.rows)
283
- ? result.rows.filter((row) => Boolean(row && typeof row === 'object' && !Array.isArray(row))).slice(0, 8)
295
+ // The visual preview stays tiny, but member resolution needs a wider bounded
296
+ // value window. Deriving dimensions from only the eight preview rows caused a
297
+ // valid row 9/10 member to disappear before the next turn (AGT-012/E2E-010).
298
+ const memberRows = Array.isArray(result?.rows)
299
+ ? result.rows.filter((row) => Boolean(row && typeof row === 'object' && !Array.isArray(row))).slice(0, 24)
284
300
  : [];
301
+ const rows = memberRows.slice(0, 8);
285
302
  const rowsSample = rows.map((row) => columns.map((column) => row[column]));
286
303
  const contextPack = agentRunRecord(payload?.contextPack);
287
304
  const questionPlan = agentRunRecord(contextPack?.questionPlan);
@@ -305,7 +322,7 @@ function conversationTurnInputFromRun(run) {
305
322
  ? {
306
323
  columns,
307
324
  rowsSample,
308
- dimensionValues: conversationDimensionValues(columns, rows),
325
+ dimensionValues: conversationDimensionValues(columns, memberRows),
309
326
  measureColumns,
310
327
  rowCount: typeof rowCountRaw === 'number' ? rowCountRaw : rows.length || undefined,
311
328
  }
@@ -421,6 +438,7 @@ export async function startLocalServer(opts) {
421
438
  if (gitRoot)
422
439
  ensureLocalRuntimeGitignore(projectRoot);
423
440
  let projectConfig = loadProjectConfig(projectRoot);
441
+ recordAgentRuntimeVersion(projectRoot, runtimeVersion);
424
442
  const configuredConnection = rawConnection
425
443
  ? normalizeProjectConnection(rawConnection, projectRoot)
426
444
  : projectConfig.defaultConnection
@@ -499,6 +517,8 @@ export async function startLocalServer(opts) {
499
517
  const prepared = await ensureAgentProjectReady(projectRoot, {
500
518
  kgPath: defaultKgPath(projectRoot),
501
519
  manifest: input.manifest,
520
+ forceKgIndex: input.forceRebuild,
521
+ forceMetadataCatalog: input.forceRebuild,
502
522
  });
503
523
  const current = onboardingJobs.get(id);
504
524
  if (!current || current.status === 'cancelled')
@@ -660,18 +680,25 @@ export async function startLocalServer(opts) {
660
680
  const semanticLayerDir = join(projectRoot, 'semantic-layer');
661
681
  let semanticImportManifest = loadSemanticImportManifest(projectRoot);
662
682
  const userPrefsPath = join(projectRoot, '.dql-user-prefs.json');
663
- const semanticConfig = resolveProjectSemanticConfig(projectConfig, projectRoot);
683
+ let semanticConfig = resolveProjectSemanticConfig(projectConfig, projectRoot);
664
684
  let semanticLastSyncTime = null;
665
- {
685
+ const reloadSemanticLayer = async () => {
686
+ semanticConfig = resolveProjectSemanticConfig(projectConfig, projectRoot);
666
687
  const semanticConnection = connection;
667
688
  const executeQuery = semanticConfig?.provider === 'snowflake' && semanticConnection
668
689
  ? async (sql) => { const r = await executor.executeQuery(sql, [], {}, semanticConnection); return { rows: r.rows }; }
669
690
  : undefined;
670
691
  const result = await resolveSemanticLayerAsync(semanticConfig, projectRoot, executeQuery);
671
- semanticLayer = result.layer;
672
692
  semanticLayerErrors = result.errors;
673
693
  semanticDetectedProvider = result.detectedProvider;
674
- semanticLastSyncTime = result.layer ? new Date().toISOString() : null;
694
+ if (result.layer) {
695
+ semanticLayer = result.layer;
696
+ semanticLastSyncTime = new Date().toISOString();
697
+ }
698
+ else if (result.errors.length === 0) {
699
+ semanticLayer = undefined;
700
+ semanticLastSyncTime = null;
701
+ }
675
702
  semanticImportManifest = loadSemanticImportManifest(projectRoot);
676
703
  // Legacy fallback if provider system returned nothing and no errors
677
704
  if (!semanticLayer && semanticLayerErrors.length === 0 && existsSync(semanticLayerDir)) {
@@ -681,8 +708,28 @@ export async function startLocalServer(opts) {
681
708
  }
682
709
  catch { /* continue without */ }
683
710
  }
684
- }
711
+ return result;
712
+ };
713
+ await reloadSemanticLayer();
685
714
  await refreshLocalMetadataCatalog(projectRoot);
715
+ const startupDbtManifestPath = resolveDbtManifestPath(projectRoot, projectConfig);
716
+ if (startupDbtManifestPath && !isAgentProjectIndexReady(projectRoot)) {
717
+ try {
718
+ const startupSnapshot = projectSnapshot();
719
+ startDbtPreparationJob({
720
+ kind: 'dbt_prepare',
721
+ snapshotId: startupSnapshot.snapshotId,
722
+ manifest: startupSnapshot.manifest,
723
+ validationDurationMs: 0,
724
+ compileDurationMs: 0,
725
+ forceRebuild: true,
726
+ });
727
+ }
728
+ catch {
729
+ // Startup remains available with the last valid cache; Setup exposes the
730
+ // compile error and the next explicit refresh retries preparation.
731
+ }
732
+ }
686
733
  const recordDatasetMetadataSnapshot = (datasets = datasetWorkspace.list()) => {
687
734
  if (datasets.length === 0)
688
735
  return;
@@ -1407,6 +1454,7 @@ export async function startLocalServer(opts) {
1407
1454
  || governedAnswer.sourceCertifiedBlock
1408
1455
  || governedAnswer.dqlArtifact?.kind === 'certified_block'));
1409
1456
  const isExploratory = Boolean(governedAnswer.exploratoryCandidate);
1457
+ const isExecutionFailure = agentAnswerHasExecutionFailure(governedAnswer);
1410
1458
  const isGroundingGap = governedAnswer.kind === 'no_answer'
1411
1459
  && governedAnswer.refusalCode === 'grounding_gap'
1412
1460
  && !isExploratory;
@@ -1427,7 +1475,7 @@ export async function startLocalServer(opts) {
1427
1475
  const needsClarification = governedAnswer.kind === 'no_answer'
1428
1476
  && !isGroundingGap && !isProviderError && !isModelDeclined && !isPolicyBlocked;
1429
1477
  const sql = governedAnswer.proposedSql ?? governedAnswer.sql;
1430
- const runnableSql = governedAnswer.kind === 'no_answer' || (isExploratory && !governedAnswer.result)
1478
+ const runnableSql = governedAnswer.kind === 'no_answer' || isExecutionFailure || (isExploratory && !governedAnswer.result)
1431
1479
  ? undefined
1432
1480
  : sql;
1433
1481
  // Render executed rows deterministically for ordinary lookups. A second LLM
@@ -1466,14 +1514,14 @@ export async function startLocalServer(opts) {
1466
1514
  synthesizedAnswer = undefined;
1467
1515
  }
1468
1516
  }
1469
- const status = isProviderError
1517
+ const status = isProviderError || isExecutionFailure
1470
1518
  ? 'blocked'
1471
1519
  : needsClarification
1472
1520
  ? 'needs_clarification'
1473
1521
  : isCertified || isSemantic
1474
1522
  ? 'completed'
1475
1523
  : 'needs_review';
1476
- const trustState = isProviderError
1524
+ const trustState = isProviderError || isExecutionFailure
1477
1525
  ? 'blocked'
1478
1526
  : needsClarification
1479
1527
  ? 'not_applicable'
@@ -1482,7 +1530,7 @@ export async function startLocalServer(opts) {
1482
1530
  : isSemantic
1483
1531
  ? 'governed'
1484
1532
  : 'review_required';
1485
- const stopReason = isProviderError
1533
+ const stopReason = isProviderError || isExecutionFailure
1486
1534
  ? 'blocked'
1487
1535
  : needsClarification
1488
1536
  ? 'needs_clarification'
@@ -1493,33 +1541,41 @@ export async function startLocalServer(opts) {
1493
1541
  : 'human_review_required';
1494
1542
  const nextActions = needsClarification
1495
1543
  ? [{ id: 'clarify', label: 'Clarify question', route: 'generated_answer' }]
1496
- : [
1497
- { id: 'create-block', label: governedAnswer.dqlArtifact ? 'Review DQL draft' : 'Create DQL draft', route: 'dql_block_draft', artifactKind: 'dql_block_draft' },
1498
- { id: 'research-gap', label: 'Research deeper', route: 'research' },
1499
- ...(runnableSql ? [{
1500
- id: 'insert-sql',
1501
- label: governedAnswer.dqlArtifact ? 'Insert as DQL cell' : 'Insert SQL preview',
1502
- route: 'sql_cell',
1503
- artifactKind: 'sql_cell',
1504
- }] : []),
1505
- ];
1544
+ : isExecutionFailure
1545
+ ? [{ id: 'retry-after-connection', label: 'Retry after fixing the database connection', route: 'generated_answer' }]
1546
+ : isGroundingGap
1547
+ ? [{ id: 'research-gap', label: 'Research missing metadata coverage', route: 'research', artifactKind: 'research_run' }]
1548
+ : [
1549
+ { id: 'create-block', label: governedAnswer.dqlArtifact ? 'Review DQL draft' : 'Create DQL draft', route: 'dql_block_draft', artifactKind: 'dql_block_draft' },
1550
+ { id: 'research-gap', label: 'Research deeper', route: 'research' },
1551
+ ...(runnableSql ? [{
1552
+ id: 'insert-sql',
1553
+ label: governedAnswer.dqlArtifact ? 'Insert as DQL cell' : 'Insert SQL preview',
1554
+ route: 'sql_cell',
1555
+ artifactKind: 'sql_cell',
1556
+ }] : []),
1557
+ ];
1506
1558
  return {
1507
1559
  resolvedRoute,
1508
1560
  answerRefusalCode: governedAnswer.kind === 'no_answer' ? governedAnswer.refusalCode : undefined,
1509
1561
  answerTier: governedAnswer.route?.tier,
1510
- summary: governedAnswer.route?.label ?? (isCertified ? 'Answered from certified DQL context.' : isExploratory ? 'Exploratory DBT-grounded analysis requires review.' : 'Answered with review-required generated analysis.'),
1562
+ summary: isExecutionFailure
1563
+ ? 'The governed query could not be executed.'
1564
+ : governedAnswer.route?.label ?? (isCertified ? 'Answered from certified DQL context.' : isExploratory ? 'Exploratory DBT-grounded analysis requires review.' : 'Answered with review-required generated analysis.'),
1511
1565
  answer: synthesizedAnswer ?? governedAnswer.answer ?? governedAnswer.text,
1512
1566
  status,
1513
1567
  trustState,
1514
1568
  stopReason,
1515
- artifacts: governedAnswer.kind === 'no_answer'
1516
- // A refusal still keeps the DQL draft the answer loop produced (when any),
1517
- // so the "Review DQL draft" next-action isn't a dead link and the user can
1518
- // see the SQL that was about to run. Provider outages carry no draft.
1519
- ? (governedAnswer.dqlArtifact && !isProviderError
1520
- ? [agentRunArtifact('dql_block_draft', 'DQL draft (review required)', governedAnswer.dqlArtifact, undefined, 'review_required')]
1521
- : [])
1522
- : [agentRunArtifact('answer', isCertified ? 'Certified answer' : isSemantic ? 'Governed semantic answer' : isExploratory ? 'Exploratory DBT-grounded answer' : 'Review-required answer', governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, isCertified ? 'certified' : isSemantic ? 'governed' : 'review_required')],
1569
+ artifacts: isExecutionFailure
1570
+ ? []
1571
+ : governedAnswer.kind === 'no_answer'
1572
+ // A refusal still keeps the DQL draft the answer loop produced (when any),
1573
+ // so the "Review DQL draft" next-action isn't a dead link and the user can
1574
+ // see the SQL that was about to run. Provider outages carry no draft.
1575
+ ? (governedAnswer.dqlArtifact && !isProviderError && !isGroundingGap && !isModelDeclined && !isPolicyBlocked
1576
+ ? [agentRunArtifact('dql_block_draft', 'DQL draft (review required)', governedAnswer.dqlArtifact, undefined, 'review_required')]
1577
+ : [])
1578
+ : [agentRunArtifact('answer', isCertified ? 'Certified answer' : isSemantic ? 'Governed semantic answer' : isExploratory ? 'Exploratory DBT-grounded answer' : 'Review-required answer', governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, isCertified ? 'certified' : isSemantic ? 'governed' : 'review_required')],
1523
1579
  evaluations: [
1524
1580
  agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.', {
1525
1581
  plannedRoute: route,
@@ -1548,6 +1604,7 @@ export async function startLocalServer(opts) {
1548
1604
  : isExploratory
1549
1605
  ? 'The answer used bounded DBT/schema evidence because no certified modeled relationship path covered the request. It is review-required and was not certified.'
1550
1606
  : 'The answer is generated or semantic-layer backed and remains review-required.', governedAnswer.route),
1607
+ ...(isExecutionFailure ? [agentRunEvaluation('query-execution', 'Query execution', false, 'blocking', `The governed query failed before it produced a result: ${governedAnswer.executionError}`)] : []),
1551
1608
  ...(isGroundingGap ? [
1552
1609
  {
1553
1610
  ...agentRunEvaluation('grounding-gap', 'Metadata grounding', false, 'warning', 'The bounded lookup could not prove the required metadata grounding. No automatic retry or Research escalation was started.', {
@@ -2133,6 +2190,7 @@ export async function startLocalServer(opts) {
2133
2190
  : undefined;
2134
2191
  const task = buildLocalContextPack(projectRoot, {
2135
2192
  question: request.question,
2193
+ focusObjectKey: request.selectedEvidenceId,
2136
2194
  followUp,
2137
2195
  priorContextPackId: agentRunString(request.conversationContext?.contextPackId),
2138
2196
  conversationTopicRelation: topicRelation === 'continuation'
@@ -2208,9 +2266,9 @@ export async function startLocalServer(opts) {
2208
2266
  };
2209
2267
  }
2210
2268
  if ((candidate.kind === 'semantic_metric' || candidate.kind === 'semantic_member')
2211
- && semanticEvidence.has(candidate.id)
2212
- && pack.routeDecision.route !== 'clarify'
2213
- && pack.routeDecision.route !== 'conflict') {
2269
+ && (semanticEvidence.has(candidate.id) || request.selectedEvidenceId === candidate.id)
2270
+ && (request.selectedEvidenceId === candidate.id
2271
+ || (pack.routeDecision.route !== 'clarify' && pack.routeDecision.route !== 'conflict'))) {
2214
2272
  const requestedDimensions = pack.questionPlan.requestedShape.dimensions.map((dimension) => dimension.toLowerCase());
2215
2273
  const availableDimensions = (candidate.dimensions ?? []).map((dimension) => dimension.toLowerCase());
2216
2274
  const dimensionsFit = requestedDimensions.length === 0 || requestedDimensions.every((requested) => availableDimensions.some((available) => available === requested || available.endsWith(`.${requested}`)));
@@ -3293,20 +3351,7 @@ export async function startLocalServer(opts) {
3293
3351
  }
3294
3352
  // Hot-reload semantic layer on change and notify frontend
3295
3353
  if (dir === 'semantic-layer') {
3296
- const semanticConnection = connection;
3297
- const executeQuery = semanticConfig?.provider === 'snowflake' && semanticConnection
3298
- ? async (sql) => { const r = await executor.executeQuery(sql, [], {}, semanticConnection); return { rows: r.rows }; }
3299
- : undefined;
3300
- resolveSemanticLayerAsync(semanticConfig, projectRoot, executeQuery).then((refreshed) => {
3301
- if (refreshed.layer) {
3302
- semanticLayer = refreshed.layer;
3303
- semanticLayerErrors = refreshed.errors;
3304
- semanticLastSyncTime = new Date().toISOString();
3305
- semanticImportManifest = loadSemanticImportManifest(projectRoot);
3306
- }
3307
- else if (refreshed.errors.length > 0) {
3308
- semanticLayerErrors = refreshed.errors;
3309
- }
3354
+ reloadSemanticLayer().then(() => {
3310
3355
  // Notify all connected notebook clients to re-fetch the semantic layer
3311
3356
  const reloadPayload = JSON.stringify({ type: 'semantic-reload' });
3312
3357
  for (const client of sseClients) {
@@ -3323,7 +3368,7 @@ export async function startLocalServer(opts) {
3323
3368
  }
3324
3369
  catch { /* dir not watchable */ }
3325
3370
  }
3326
- const configuredDbtManifest = resolveDbtManifestPath(projectRoot);
3371
+ const configuredDbtManifest = resolveDbtManifestPath(projectRoot, projectConfig);
3327
3372
  if (configuredDbtManifest && existsSync(dirname(configuredDbtManifest))) {
3328
3373
  try {
3329
3374
  watch(dirname(configuredDbtManifest), { persistent: false }, (_eventType, filename) => {
@@ -3344,6 +3389,17 @@ export async function startLocalServer(opts) {
3344
3389
  sseClients.delete(client);
3345
3390
  }
3346
3391
  }
3392
+ void reloadSemanticLayer().then(() => {
3393
+ const reloadPayload = JSON.stringify({ type: 'semantic-reload' });
3394
+ for (const client of sseClients) {
3395
+ try {
3396
+ client.write(`event: change\ndata: ${reloadPayload}\n\n`);
3397
+ }
3398
+ catch {
3399
+ sseClients.delete(client);
3400
+ }
3401
+ }
3402
+ }).catch(() => { });
3347
3403
  });
3348
3404
  }
3349
3405
  catch { /* dbt artifact directory not watchable */ }
@@ -3866,6 +3922,10 @@ export async function startLocalServer(opts) {
3866
3922
  let snapshotManifest;
3867
3923
  const compileStartedAt = Date.now();
3868
3924
  try {
3925
+ const semanticReload = await reloadSemanticLayer();
3926
+ if (semanticReload.errors.length > 0) {
3927
+ throw new Error(`dbt semantic artifacts could not be loaded: ${semanticReload.errors.join('; ')}`);
3928
+ }
3869
3929
  const snapshot = projectSnapshot();
3870
3930
  snapshotId = snapshot.snapshotId;
3871
3931
  snapshotManifest = snapshot.manifest;
@@ -3878,6 +3938,7 @@ export async function startLocalServer(opts) {
3878
3938
  connection = previousConnection;
3879
3939
  projectSnapshots.invalidate();
3880
3940
  invalidateAgentProjectState(projectRoot);
3941
+ await reloadSemanticLayer();
3881
3942
  throw Object.assign(new Error(`dbt-first configuration did not compile and was rolled back: ${error instanceof Error ? error.message : String(error)}`), { code: 'SNAPSHOT_BUILD_FAILED' });
3882
3943
  }
3883
3944
  const preparation = startDbtPreparationJob({
@@ -3918,6 +3979,7 @@ export async function startLocalServer(opts) {
3918
3979
  const validationDurationMs = Date.now() - refreshStartedAt;
3919
3980
  const compileStartedAt = Date.now();
3920
3981
  projectSnapshots.invalidate();
3982
+ await reloadSemanticLayer();
3921
3983
  const snapshot = projectSnapshot();
3922
3984
  invalidateAgentProjectState(projectRoot);
3923
3985
  const job = startDbtPreparationJob({
@@ -8143,16 +8205,7 @@ export async function startLocalServer(opts) {
8143
8205
  }
8144
8206
  if (req.method === 'POST' && path === '/api/semantic-layer/reload') {
8145
8207
  try {
8146
- const semanticConnection = connection;
8147
- const executeQuery = semanticConfig?.provider === 'snowflake' && semanticConnection
8148
- ? async (sql) => { const r = await executor.executeQuery(sql, [], {}, semanticConnection); return { rows: r.rows }; }
8149
- : undefined;
8150
- const refreshed = await resolveSemanticLayerAsync(semanticConfig, projectRoot, executeQuery);
8151
- semanticLayer = refreshed.layer;
8152
- semanticLayerErrors = refreshed.errors;
8153
- semanticDetectedProvider = refreshed.detectedProvider;
8154
- semanticLastSyncTime = refreshed.layer ? new Date().toISOString() : null;
8155
- semanticImportManifest = loadSemanticImportManifest(projectRoot);
8208
+ await reloadSemanticLayer();
8156
8209
  const diagnostics = buildSemanticLayerDiagnostics(projectRoot, projectConfig, {
8157
8210
  semanticLayer,
8158
8211
  semanticErrors: semanticLayerErrors,
@@ -8162,7 +8215,7 @@ export async function startLocalServer(opts) {
8162
8215
  });
8163
8216
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
8164
8217
  res.end(serializeJSON({
8165
- ok: Boolean(refreshed.layer),
8218
+ ok: Boolean(semanticLayer),
8166
8219
  ...diagnostics,
8167
8220
  }));
8168
8221
  }
@@ -8689,6 +8742,12 @@ export async function startLocalServer(opts) {
8689
8742
  }));
8690
8743
  return;
8691
8744
  }
8745
+ const provider = semanticConfig?.provider ?? semanticDetectedProvider ?? 'dql';
8746
+ const dbtManifestReady = provider === 'dbt'
8747
+ ? hasDbtSemanticManifest(projectRoot, semanticConfig?.projectPath)
8748
+ : false;
8749
+ const metricFlowReady = provider === 'dbt' ? hasMetricFlowCli() : false;
8750
+ const dbtExecutionReady = dbtManifestReady && metricFlowReady;
8692
8751
  const metrics = semanticLayer.listMetrics().map((m) => ({
8693
8752
  name: m.name,
8694
8753
  label: m.label,
@@ -8703,6 +8762,7 @@ export async function startLocalServer(opts) {
8703
8762
  typeParams: m.typeParams ?? null,
8704
8763
  filter: m.filter ?? null,
8705
8764
  source: m.source ?? null,
8765
+ execution: semanticMetricExecutionCapability(m.name, semanticLayer, provider, metricFlowReady, connection?.driver),
8706
8766
  }));
8707
8767
  const measures = semanticLayer.listMeasures().map((m) => ({
8708
8768
  name: m.name,
@@ -8800,12 +8860,6 @@ export async function startLocalServer(opts) {
8800
8860
  owner: q.owner ?? null,
8801
8861
  source: q.source ?? null,
8802
8862
  }));
8803
- const provider = semanticConfig?.provider ?? semanticDetectedProvider ?? 'dql';
8804
- const dbtManifestReady = provider === 'dbt'
8805
- ? hasDbtSemanticManifest(projectRoot, semanticConfig?.projectPath)
8806
- : false;
8807
- const metricFlowReady = provider === 'dbt' ? hasMetricFlowCli() : false;
8808
- const dbtExecutionReady = dbtManifestReady && metricFlowReady;
8809
8863
  const dbtExecutionSetup = dbtExecutionReady
8810
8864
  ? null
8811
8865
  : !dbtManifestReady && !metricFlowReady
@@ -9720,8 +9774,20 @@ export async function startLocalServer(opts) {
9720
9774
  tableMapping,
9721
9775
  });
9722
9776
  if (!composed) {
9777
+ const provider = semanticConfig?.provider ?? semanticDetectedProvider ?? 'dql';
9778
+ const metricFlowReady = provider === 'dbt' && hasMetricFlowCli();
9779
+ const blocked = metrics.map((metricName) => ({
9780
+ metric: metricName,
9781
+ ...semanticMetricExecutionCapability(metricName, semanticLayer, provider, metricFlowReady, driver),
9782
+ })).filter((capability) => capability.status !== 'ready');
9723
9783
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
9724
- res.end(serializeJSON({ error: `Could not compose query for metrics: [${metrics.join(', ')}]` }));
9784
+ res.end(serializeJSON({
9785
+ error: blocked.length > 0
9786
+ ? blocked.map((capability) => `${capability.metric}: ${capability.reason}`).join(' ')
9787
+ : 'The selected dimensions do not share a governed join path with every selected metric.',
9788
+ code: blocked.length > 0 ? 'SEMANTIC_RUNTIME_REQUIRED' : 'SEMANTIC_FIELDS_INCOMPATIBLE',
9789
+ details: { metrics, dimensions, blocked },
9790
+ }));
9725
9791
  return;
9726
9792
  }
9727
9793
  // Execute the composed SQL against the resolved connection
@@ -9806,8 +9872,20 @@ export async function startLocalServer(opts) {
9806
9872
  tableMapping,
9807
9873
  });
9808
9874
  if (!composed) {
9875
+ const provider = semanticConfig?.provider ?? semanticDetectedProvider ?? 'dql';
9876
+ const metricFlowReady = provider === 'dbt' && hasMetricFlowCli();
9877
+ const blocked = metrics.map((metricName) => ({
9878
+ metric: metricName,
9879
+ ...semanticMetricExecutionCapability(metricName, semanticLayer, provider, metricFlowReady, driver),
9880
+ })).filter((capability) => capability.status !== 'ready');
9809
9881
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
9810
- res.end(serializeJSON({ error: 'Could not compose semantic block preview SQL.' }));
9882
+ res.end(serializeJSON({
9883
+ error: blocked.length > 0
9884
+ ? blocked.map((capability) => `${capability.metric}: ${capability.reason}`).join(' ')
9885
+ : 'The selected dimensions do not share a governed join path with every selected metric.',
9886
+ code: blocked.length > 0 ? 'SEMANTIC_RUNTIME_REQUIRED' : 'SEMANTIC_FIELDS_INCOMPATIBLE',
9887
+ details: { metrics, dimensions, blocked },
9888
+ }));
9811
9889
  return;
9812
9890
  }
9813
9891
  const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
@@ -13638,6 +13716,28 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
13638
13716
  validation: validateBlockStudioSource(source, semanticLayer),
13639
13717
  };
13640
13718
  }
13719
+ function semanticMetricExecutionCapability(metricName, semanticLayer, provider, metricFlowReady, driver) {
13720
+ if (provider === 'dbt' && metricFlowReady) {
13721
+ return { status: 'ready', engine: 'metricflow', reason: null };
13722
+ }
13723
+ const native = semanticLayer.composeQuery({ metrics: [metricName], dimensions: [], driver });
13724
+ if (native)
13725
+ return { status: 'ready', engine: 'native', reason: null };
13726
+ const metric = semanticLayer.getMetric(metricName);
13727
+ if (provider === 'dbt') {
13728
+ const kind = metric?.metricType || metric?.aggregation || metric?.type || 'metric';
13729
+ return {
13730
+ status: 'requires_setup',
13731
+ engine: null,
13732
+ reason: `${kind} metric requires MetricFlow execution. Install/configure the MetricFlow CLI, then refresh the semantic layer.`,
13733
+ };
13734
+ }
13735
+ return {
13736
+ status: 'unsupported',
13737
+ engine: null,
13738
+ reason: 'The metric does not have enough composable measure and relation metadata.',
13739
+ };
13740
+ }
13641
13741
  function parseBlockStudioArrayField(source, key) {
13642
13742
  const match = source.match(new RegExp(`\\b${key}\\s*=\\s*\\[([\\s\\S]*?)\\]`, 'i'));
13643
13743
  if (!match)
@@ -13872,10 +13972,18 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
13872
13972
  return { sql: null, diagnostics, semanticRefs };
13873
13973
  }
13874
13974
  if (!composed) {
13975
+ const provider = options?.projectConfig && isDbtSemanticRuntime(options.projectConfig, options.detectedProvider, semanticLayer) ? 'dbt' : (options?.detectedProvider ?? 'dql');
13976
+ const metricFlowReady = provider === 'dbt' && hasMetricFlowCli();
13977
+ const reasons = metrics.map((metricName) => {
13978
+ const capability = semanticMetricExecutionCapability(metricName, semanticLayer, provider, metricFlowReady, options?.driver);
13979
+ return capability.status === 'ready' ? null : `${metricName}: ${capability.reason}`;
13980
+ }).filter((reason) => Boolean(reason));
13875
13981
  diagnostics.push({
13876
13982
  severity: 'error',
13877
13983
  code: 'semantic_compose_failed',
13878
- message: `Could not compose SQL for semantic block metrics: [${metrics.join(', ')}].`,
13984
+ message: reasons.length > 0
13985
+ ? `Could not compose SQL for semantic block metrics. ${reasons.join(' ')}`
13986
+ : `Could not compose SQL for semantic block metrics: [${metrics.join(', ')}]. Check that the selected dimensions share a governed join path.`,
13879
13987
  });
13880
13988
  return { sql: null, diagnostics, semanticRefs };
13881
13989
  }