@duckcodeailabs/dql-cli 1.8.2 → 1.8.4

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 (48) hide show
  1. package/dist/assets/dql-notebook/assets/{index-DGbXmDU6.js → index-CPG4A872.js} +340 -340
  2. package/dist/assets/dql-notebook/index.html +1 -1
  3. package/dist/block-invocation.d.ts +2 -2
  4. package/dist/block-invocation.d.ts.map +1 -1
  5. package/dist/commands/doctor.d.ts.map +1 -1
  6. package/dist/commands/doctor.js +62 -2
  7. package/dist/commands/doctor.js.map +1 -1
  8. package/dist/dbt-cloud-semantic.d.ts +42 -0
  9. package/dist/dbt-cloud-semantic.d.ts.map +1 -0
  10. package/dist/dbt-cloud-semantic.js +185 -0
  11. package/dist/dbt-cloud-semantic.js.map +1 -0
  12. package/dist/llm/analytics-tools.d.ts.map +1 -1
  13. package/dist/llm/analytics-tools.js +13 -1
  14. package/dist/llm/analytics-tools.js.map +1 -1
  15. package/dist/llm/cancellation.d.ts +13 -0
  16. package/dist/llm/cancellation.d.ts.map +1 -0
  17. package/dist/llm/cancellation.js +19 -0
  18. package/dist/llm/cancellation.js.map +1 -0
  19. package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
  20. package/dist/llm/providers/dql-agent-provider.js +6 -0
  21. package/dist/llm/providers/dql-agent-provider.js.map +1 -1
  22. package/dist/llm/types.d.ts +3 -1
  23. package/dist/llm/types.d.ts.map +1 -1
  24. package/dist/local-runtime.d.ts +40 -3
  25. package/dist/local-runtime.d.ts.map +1 -1
  26. package/dist/local-runtime.js +547 -217
  27. package/dist/local-runtime.js.map +1 -1
  28. package/dist/package.json +10 -10
  29. package/dist/providers/subscription-cli.d.ts.map +1 -1
  30. package/dist/providers/subscription-cli.js +16 -0
  31. package/dist/providers/subscription-cli.js.map +1 -1
  32. package/dist/retrieval-health.d.ts +29 -0
  33. package/dist/retrieval-health.d.ts.map +1 -0
  34. package/dist/retrieval-health.js +88 -0
  35. package/dist/retrieval-health.js.map +1 -0
  36. package/dist/semantic-runtime-settings.d.ts +55 -0
  37. package/dist/semantic-runtime-settings.d.ts.map +1 -0
  38. package/dist/semantic-runtime-settings.js +191 -0
  39. package/dist/semantic-runtime-settings.js.map +1 -0
  40. package/dist/semantic-runtime.d.ts +80 -0
  41. package/dist/semantic-runtime.d.ts.map +1 -0
  42. package/dist/semantic-runtime.js +238 -0
  43. package/dist/semantic-runtime.js.map +1 -0
  44. package/dist/version-status.d.ts +28 -0
  45. package/dist/version-status.d.ts.map +1 -0
  46. package/dist/version-status.js +118 -0
  47. package/dist/version-status.js.map +1 -0
  48. package/package.json +10 -10
@@ -14,9 +14,12 @@ import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetri
14
14
  import { load as loadYaml } from 'js-yaml';
15
15
  import { listBlockTemplates } from './block-templates.js';
16
16
  import { getRunner as getLLMRunner } from './llm/index.js';
17
+ import { rethrowIfCancelled } from './llm/cancellation.js';
18
+ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from './version-status.js';
19
+ import { resolveRetrievalHealthStatus } from './retrieval-health.js';
17
20
  import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
18
21
  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, recordAgentRuntimeVersion, resolveDomainContextEnvelope, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, } from '@duckcodeailabs/dql-agent';
22
+ 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, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, defaultAgentRunSqlitePath, 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
23
  import { gatherProposeEnrichment } from './propose-enrich.js';
21
24
  import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
22
25
  import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
@@ -28,7 +31,9 @@ import { LocalAppStorage, LocalNotebookResearchStorage, defaultLocalAppsDbPath,
28
31
  import { Certifier, ENTERPRISE_RULES, evaluateInvariants, hasInvariantViolation, } from '@duckcodeailabs/dql-governance';
29
32
  import { buildSemanticObjectDetail, buildSemanticTree, computeSyncDiff, loadSemanticImportManifest, performSemanticImport, previewSemanticImport, syncSemanticImport, } from './semantic-import.js';
30
33
  import { clearBlockStudioImportSessions, candidateToDqlSource, createBlockStudioImportSession, deleteBlockStudioImportSession, listBlockStudioImportSessions, loadBlockStudioImportSession, readBlockStudioImportCandidate, parameterizeSqlForDqlImport, updateBlockStudioImportCandidate, writeBlockStudioImportSession, writeBlockStudioImportCandidate, } from './block-studio-import.js';
31
- import { MetricFlowUnavailableError, compileMetricFlowQuery, hasDbtSemanticManifest, hasMetricFlowCli, } from "./metricflow.js";
34
+ import { MetricFlowUnavailableError, hasDbtSemanticManifest, } from "./metricflow.js";
35
+ import { compileSemanticRuntimeQuery, getSemanticRuntimeStatus, isSemanticRuntimeError, listRuntimeCompatibleDimensions, semanticMetricExecutionCapability as runtimeMetricExecutionCapability, SemanticRuntimeRequiredError, testSemanticRuntimeDraft, } from './semantic-runtime.js';
36
+ import { getSemanticRuntimeSettings, saveTestedSemanticRuntimeSettings, } from './semantic-runtime-settings.js';
32
37
  import { NotebookDatasetWorkspace, } from "./notebook-datasets.js";
33
38
  import { prepareBlockInvocation } from './block-invocation.js';
34
39
  const NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT = 500;
@@ -164,17 +169,26 @@ export function parseAgentRunRequestBody(body) {
164
169
  }
165
170
  const AGENT_LOOKUP_DEADLINE_MS = 45_000;
166
171
  const AGENT_RESEARCH_DEADLINE_MS = 120_000;
172
+ /** Env-tunable run deadline (e.g. slow subscription-CLI providers), clamped to sane bounds. */
173
+ function resolveAgentDeadlineMs(envKey, fallback, env = process.env) {
174
+ const configured = Number(env[envKey]);
175
+ if (!Number.isFinite(configured) || configured <= 0)
176
+ return fallback;
177
+ return Math.max(15_000, Math.min(600_000, Math.floor(configured)));
178
+ }
167
179
  /**
168
180
  * PERF-002: one wall-clock budget follows the request through routing, provider
169
181
  * calls, repair, and execution. Ordinary Ask never inherits Research's budget
170
182
  * merely because it spans two tables; explicit/deep investigation does.
171
183
  */
172
- export function agentRunDeadlineMs(request) {
184
+ export function agentRunDeadlineMs(request, env = process.env) {
185
+ const lookupDeadline = resolveAgentDeadlineMs('DQL_AGENT_LOOKUP_DEADLINE_MS', AGENT_LOOKUP_DEADLINE_MS, env);
186
+ const researchDeadline = resolveAgentDeadlineMs('DQL_AGENT_RESEARCH_DEADLINE_MS', AGENT_RESEARCH_DEADLINE_MS, env);
173
187
  if (request.requestedMode === 'research' || request.analysisDepth === 'deep') {
174
- return AGENT_RESEARCH_DEADLINE_MS;
188
+ return researchDeadline;
175
189
  }
176
190
  const plan = buildAnalysisQuestionPlan(request.question);
177
- return plan.needsResearchWorkspace ? AGENT_RESEARCH_DEADLINE_MS : AGENT_LOOKUP_DEADLINE_MS;
191
+ return plan.needsResearchWorkspace ? researchDeadline : lookupDeadline;
178
192
  }
179
193
  export function shouldSynthesizeAgentRunAnswer(governedAnswer) {
180
194
  if (governedAnswer.kind === 'no_answer')
@@ -215,7 +229,11 @@ function businessNarrativeGaps(warnings) {
215
229
  // source of prior turns (survives refresh); the client-built context remains
216
230
  // the fallback for embedders that never send a threadId.
217
231
  async function conversationContextFromThread(store, threadId, clientContext, question) {
218
- const turns = store.recentTurns(threadId, 8).map((turn) => {
232
+ // Token-budget backstop: six verbatim turns with per-field caps. Older turns
233
+ // remain reachable through the rolling summary + semantic recall below —
234
+ // carrying more raw prose mostly slows every provider call on follow-ups.
235
+ const clampTurnText = (value) => value && value.length > 1_200 ? `${value.slice(0, 1_200)}…` : value;
236
+ const turns = store.recentTurns(threadId, 6).map((turn) => {
219
237
  const contract = turn.contract ?? {};
220
238
  const topN = contract.topN;
221
239
  const topNValue = typeof topN === 'number'
@@ -225,8 +243,8 @@ async function conversationContextFromThread(store, threadId, clientContext, que
225
243
  : undefined;
226
244
  return compactConversationRecord({
227
245
  id: turn.id,
228
- question: turn.question,
229
- answerSummary: turn.answerSummary,
246
+ question: clampTurnText(turn.question),
247
+ answerSummary: clampTurnText(turn.answerSummary),
230
248
  sourceCertifiedBlock: turn.sourceCertifiedBlock,
231
249
  route: turn.route,
232
250
  trustLabel: turn.trustLabel,
@@ -425,6 +443,8 @@ export async function startLocalServer(opts) {
425
443
  const loopback = bindHost === '127.0.0.1' || bindHost === 'localhost' || bindHost === '::1';
426
444
  const authToken = opts.authToken ?? process.env.DQL_SERVER_TOKEN;
427
445
  const runtimeVersion = readDqlRuntimeVersion();
446
+ // Warm the latest-version cache in the background (2s cap, 24h cache; offline → unknown).
447
+ void fetchLatestPublishedDqlVersion();
428
448
  const allowedOrigins = new Set((opts.allowedOrigins ?? (process.env.DQL_ALLOWED_ORIGINS ?? '').split(','))
429
449
  .map((value) => value.trim().replace(/\/$/, ''))
430
450
  .filter(Boolean));
@@ -1069,12 +1089,17 @@ export async function startLocalServer(opts) {
1069
1089
  // dialect-correct SQL (e.g. DATE_TRUNC / identifier quoting). Absent when no
1070
1090
  // connection is configured — the compiler then uses its default dialect.
1071
1091
  let semanticDriver;
1092
+ let semanticConnection;
1072
1093
  try {
1073
- semanticDriver = requireActiveConnection().driver;
1094
+ semanticConnection = requireActiveConnection();
1095
+ semanticDriver = semanticConnection.driver;
1074
1096
  }
1075
1097
  catch {
1076
1098
  semanticDriver = undefined;
1077
1099
  }
1100
+ const semanticTableMapping = semanticLayer && semanticConnection
1101
+ ? await resolveSemanticTableMapping(executor, semanticConnection, semanticLayer)
1102
+ : undefined;
1078
1103
  const requestedDomain = agentRunWorkspaceValue(request, 'domain');
1079
1104
  const requestedPurpose = agentRunWorkspaceValue(request, 'purpose');
1080
1105
  const requestedModelAreaId = agentRunWorkspaceValue(request, 'modelAreaId');
@@ -1093,7 +1118,12 @@ export async function startLocalServer(opts) {
1093
1118
  await runner.run({
1094
1119
  provider: resolvedProvider,
1095
1120
  messages: [
1096
- ...(request.history ?? []).map((message) => ({ role: message.role, content: message.text })),
1121
+ // No-thread fallback path: bound the raw client history so follow-ups
1122
+ // cannot inflate every provider call (8 turns × 4k chars).
1123
+ ...(request.history ?? []).slice(-8).map((message) => ({
1124
+ role: message.role,
1125
+ content: message.text.length > 4_000 ? `${message.text.slice(0, 4_000)}…` : message.text,
1126
+ })),
1097
1127
  ...(isRepair
1098
1128
  ? [{ role: 'assistant', content: `The prior attempt needs repair without changing the original question or requested output grain: ${repair?.repairHint}` }]
1099
1129
  : []),
@@ -1116,6 +1146,26 @@ export async function startLocalServer(opts) {
1116
1146
  projectSnapshots.assertCurrent(snapshotId);
1117
1147
  },
1118
1148
  ...(semanticDriver ? { semanticDriver } : {}),
1149
+ ...(semanticTableMapping ? { semanticTableMapping } : {}),
1150
+ ...(semanticLayer ? {
1151
+ semanticQueryCompiler: async (selection) => {
1152
+ const compiled = await compileSemanticRuntimeQuery({
1153
+ ...selection,
1154
+ dimensions: selection.dimensions ?? [],
1155
+ }, {
1156
+ projectRoot,
1157
+ projectConfig,
1158
+ detectedProvider: semanticDetectedProvider,
1159
+ semanticLayer: semanticLayer,
1160
+ driver: semanticDriver,
1161
+ tableMapping: semanticTableMapping,
1162
+ });
1163
+ if (!compiled) {
1164
+ throw new SemanticRuntimeRequiredError('The selected semantic members could not be composed. Configure dbt Cloud Semantic Layer or a compatible local MetricFlow runtime for derived metrics.');
1165
+ }
1166
+ return { sql: compiled.sql, engine: compiled.engine };
1167
+ },
1168
+ } : {}),
1119
1169
  ...(routeDecision?.meaningResolution?.selectedConceptIds.length
1120
1170
  ? { preferredEvidenceIds: routeDecision.meaningResolution.selectedConceptIds }
1121
1171
  : {}),
@@ -1429,6 +1479,9 @@ export async function startLocalServer(opts) {
1429
1479
  applySmartVisualization(governedAnswer, request.question);
1430
1480
  }
1431
1481
  catch (error) {
1482
+ // Deadline/cancellation propagates to the engine, which renders the
1483
+ // graceful "bounded execution deadline" outcome — never a provider error.
1484
+ rethrowIfCancelled(error, request.signal);
1432
1485
  const message = formatAgentRunInfrastructureError(error, 'AI answer provider');
1433
1486
  return {
1434
1487
  summary: message,
@@ -1456,7 +1509,7 @@ export async function startLocalServer(opts) {
1456
1509
  const isExploratory = Boolean(governedAnswer.exploratoryCandidate);
1457
1510
  const isExecutionFailure = agentAnswerHasExecutionFailure(governedAnswer);
1458
1511
  const isGroundingGap = governedAnswer.kind === 'no_answer'
1459
- && governedAnswer.refusalCode === 'grounding_gap'
1512
+ && (governedAnswer.refusalCode === 'grounding_gap' || governedAnswer.refusalCode === 'modeling_gap')
1460
1513
  && !isExploratory;
1461
1514
  const isProviderError = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'provider_error';
1462
1515
  // AGT-004: a rejected attribution/export/proof policy is a deliberate
@@ -1786,6 +1839,7 @@ export async function startLocalServer(opts) {
1786
1839
  }
1787
1840
  }
1788
1841
  catch (error) {
1842
+ rethrowIfCancelled(error, request.signal);
1789
1843
  researchWorkspaceError = formatNotebookResearchStorageError(error);
1790
1844
  }
1791
1845
  }
@@ -2355,7 +2409,13 @@ export async function startLocalServer(opts) {
2355
2409
  getEvidence: buildAgentRunEvidence,
2356
2410
  getCatalogContext: buildRankedAgentRunCatalogContext,
2357
2411
  });
2358
- const agentRunStore = new FileAgentRunStore({ path: defaultAgentRunStorePath(projectRoot) });
2412
+ // P0: one row per run with retention + old-run compaction. The legacy JSON
2413
+ // store rewrote the entire file (123 MB observed) twice per answered question;
2414
+ // existing history is imported once and the JSON renamed to *.migrated.
2415
+ const agentRunStore = new SqliteAgentRunStore({
2416
+ path: defaultAgentRunSqlitePath(projectRoot),
2417
+ legacyJsonPath: defaultAgentRunStorePath(projectRoot),
2418
+ });
2359
2419
  // A run may outlive its streaming browser connection, so cancellation is
2360
2420
  // server-owned and keyed by run id rather than relying on fetch abort alone.
2361
2421
  const activeAgentRunControllers = new Map();
@@ -2425,13 +2485,28 @@ export async function startLocalServer(opts) {
2425
2485
  const activeConnection = requireActiveConnection();
2426
2486
  const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer);
2427
2487
  const plan = buildExecutionPlan(resolved.cell, { semanticLayer, driver: activeConnection.driver, tableMapping });
2428
- if (!plan) {
2488
+ const semanticCompose = resolved.cell.type === 'dql'
2489
+ && semanticLayer
2490
+ && /\btype\s*=\s*"semantic"/i.test(resolved.cell.source)
2491
+ ? await composeSemanticBlockSqlForRuntime(resolved.cell.source, semanticLayer, {
2492
+ driver: activeConnection.driver,
2493
+ tableMapping,
2494
+ detectedProvider: semanticDetectedProvider,
2495
+ projectRoot,
2496
+ projectConfig,
2497
+ })
2498
+ : null;
2499
+ if (semanticCompose && !semanticCompose.sql) {
2500
+ throw new Error(semanticCompose.diagnostics.map((diagnostic) => diagnostic.message).join(' '));
2501
+ }
2502
+ const executableSql = semanticCompose?.sql ?? plan?.sql;
2503
+ if (!executableSql) {
2429
2504
  snapshotCells.push({ cellId, status: 'idle', executionCount: 0, executedAt });
2430
2505
  continue;
2431
2506
  }
2432
- const prepared = prepareLocalExecution(plan.sql, activeConnection, projectRoot, projectConfig);
2507
+ const prepared = prepareLocalExecution(executableSql, activeConnection, projectRoot, projectConfig);
2433
2508
  assertAppAccess({ app, domain: resolved.domain ?? app.domain, level: 'execute' });
2434
- const rawResult = await executor.executeQuery(prepared.sql, plan.sqlParams, runtimeVariables(plan.variables), prepared.connection);
2509
+ const rawResult = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}), prepared.connection);
2435
2510
  const result = normalizeQueryResult(rawResult);
2436
2511
  snapshotCells.push({
2437
2512
  cellId,
@@ -2478,6 +2553,7 @@ export async function startLocalServer(opts) {
2478
2553
  const invocation = prepareBlockInvocation({
2479
2554
  source,
2480
2555
  parameters: invocationInput?.parameters,
2556
+ parameterSources: invocationInput?.parameterSources,
2481
2557
  question: invocationInput?.question,
2482
2558
  surface: 'ask_ai',
2483
2559
  });
@@ -2490,7 +2566,7 @@ export async function startLocalServer(opts) {
2490
2566
  const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer);
2491
2567
  const plan = buildExecutionPlan({ id: `agent-${metadata.name ?? 'dql-artifact'}`, type: 'dql', source, title: metadata.name ?? 'DQL artifact' }, { semanticLayer, driver: activeConnection.driver, tableMapping, parameters: invocation.values });
2492
2568
  const semanticCompose = semanticLayer
2493
- ? composeSemanticBlockSql(source, semanticLayer, {
2569
+ ? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
2494
2570
  driver: activeConnection.driver,
2495
2571
  tableMapping,
2496
2572
  projectRoot,
@@ -2574,10 +2650,11 @@ export async function startLocalServer(opts) {
2574
2650
  * candidate remains review-required and is not executed automatically.
2575
2651
  */
2576
2652
  const executeExploratoryCandidate = async (candidate, schemaContext = [], question = '', requestedTopN) => {
2577
- const preflight = repairExploratorySqlBeforeExecution(candidate.sql, schemaContext, question);
2653
+ const activeConnection = requireActiveConnection();
2654
+ const preflight = repairExploratorySqlBeforeExecution(candidate.sql, schemaContext, question, activeConnection.driver);
2578
2655
  const boundedSql = applyRequestedTopNToExploratorySql(preflight.sql, requestedTopN);
2579
2656
  const repairs = boundedSql === preflight.sql
2580
- ? preflight.repairs
2657
+ ? [...preflight.repairs]
2581
2658
  : [...preflight.repairs, `Applied the requested overall top-${requestedTopN} bound before exploratory execution.`];
2582
2659
  if (preflight.blockedReason) {
2583
2660
  return {
@@ -2587,23 +2664,65 @@ export async function startLocalServer(opts) {
2587
2664
  error: preflight.blockedReason,
2588
2665
  };
2589
2666
  }
2590
- const analysis = analyzeSqlReferences(boundedSql);
2667
+ const analysis = analyzeSqlReferences(boundedSql, activeConnection.driver);
2591
2668
  if (!analysis.parsed) {
2592
2669
  return { proofs: [], sql: boundedSql, repairs, error: 'DQL could not parse the exploratory SQL to validate its join predicates.' };
2593
2670
  }
2671
+ const probeableJoins = probeableExploratoryJoins(analysis.joins, analysis.ctes);
2594
2672
  // A normal customer → order → order-item → product question needs three
2595
2673
  // joins. Keep the lane bounded, but do not reject this common dbt-star path
2596
- // solely because the old two-join demo limit was too small.
2597
- if (analysis.joins.length > 4) {
2674
+ // solely because the old two-join demo limit was too small. CTE-internal
2675
+ // joins are counted physically (they appear in analysis.joins with their
2676
+ // physical endpoints); joins ON a CTE are restructuring, not new paths.
2677
+ if (probeableJoins.length > 4) {
2598
2678
  return { proofs: [], sql: boundedSql, repairs, error: 'This exploratory query has more than four joins and requires an analyst to review the relationship path.' };
2599
2679
  }
2600
- if (analysis.joins.some((join) => !join.leftRelation || !join.rightRelation)) {
2680
+ if (probeableJoins.some((join) => !join.leftRelation || !join.rightRelation)) {
2601
2681
  return { proofs: [], sql: boundedSql, repairs, error: 'This exploratory query has a join endpoint DQL could not resolve for bounded validation.' };
2602
2682
  }
2603
- const activeConnection = requireActiveConnection();
2683
+ // Structural binding: when the candidate carries a declared draft join path,
2684
+ // every SQL join must ride one of its edges on the declared key pair. A join
2685
+ // outside the declared path (or on different keys) is not executed — it is a
2686
+ // review-required modeling question, not a runtime guess.
2687
+ const declaredEdges = candidate.exploratoryPath?.edges ?? [];
2688
+ const joinEdges = new Map();
2689
+ if (declaredEdges.length > 0) {
2690
+ for (const [index, join] of probeableJoins.entries()) {
2691
+ const edge = declaredEdges.find((value) => declaredExploratoryRelationMatches(join.leftRelation, value.fromRelation) && declaredExploratoryRelationMatches(join.rightRelation, value.toRelation)
2692
+ || declaredExploratoryRelationMatches(join.leftRelation, value.toRelation) && declaredExploratoryRelationMatches(join.rightRelation, value.fromRelation));
2693
+ if (!edge) {
2694
+ return {
2695
+ proofs: [],
2696
+ sql: boundedSql,
2697
+ repairs,
2698
+ error: `The join between ${join.leftRelation} and ${join.rightRelation} is not part of the declared relationship path, so DQL did not execute it. Declare the relationship in the DQL model to enable it.`,
2699
+ };
2700
+ }
2701
+ const keyMatches = edge.keys.some((key) => exploratoryColumnsMatch(join.leftColumn, key.from) && exploratoryColumnsMatch(join.rightColumn, key.to)
2702
+ || exploratoryColumnsMatch(join.leftColumn, key.to) && exploratoryColumnsMatch(join.rightColumn, key.from));
2703
+ if (!keyMatches) {
2704
+ return {
2705
+ proofs: [],
2706
+ sql: boundedSql,
2707
+ repairs,
2708
+ error: `The join ${join.leftColumn} = ${join.rightColumn} uses different keys than relationship "${edge.relationshipId}" declares (${edge.keys.map((key) => `${key.from}=${key.to}`).join(', ')}). Review the declared keys before executing.`,
2709
+ };
2710
+ }
2711
+ joinEdges.set(index, edge);
2712
+ }
2713
+ const exercised = new Set([...joinEdges.values()].map((edge) => edge.relationshipId));
2714
+ for (const edge of declaredEdges) {
2715
+ if (!exercised.has(edge.relationshipId)) {
2716
+ repairs.push(`Declared relationship "${edge.relationshipId}" was not exercised by this query.`);
2717
+ }
2718
+ }
2719
+ }
2720
+ if (probeableJoins.length < analysis.joins.length) {
2721
+ repairs.push(`Skipped join probes for ${analysis.joins.length - probeableJoins.length} join(s) on derived (CTE) relations; the final query execution validates them.`);
2722
+ }
2604
2723
  const proofs = [];
2605
2724
  try {
2606
- for (const join of analysis.joins) {
2725
+ for (const [index, join] of probeableJoins.entries()) {
2607
2726
  const proofSql = buildExploratoryJoinProbeSql({
2608
2727
  leftRelation: join.leftRelation,
2609
2728
  leftColumn: join.leftColumn,
@@ -2613,6 +2732,14 @@ export async function startLocalServer(opts) {
2613
2732
  const prepared = prepareLocalExecution(proofSql, activeConnection, projectRoot, projectConfig);
2614
2733
  const raw = await executor.executeQuery(prepared.sql, [], runtimeVariables({}), prepared.connection);
2615
2734
  proofs.push({ summary: summarizeExploratoryJoinProbe(raw.rows, join.leftRelation, join.leftColumn, join.rightRelation, join.rightColumn) });
2735
+ // Gating (not observational): a structural contradiction between the
2736
+ // UNFILTERED key overlap and the declared relationship stops automatic
2737
+ // execution. A legitimately empty *filtered* business result is still an
2738
+ // answer — these gates only inspect the raw key samples.
2739
+ const contradiction = exploratoryProbeContradiction(raw.rows, join, joinEdges.get(index));
2740
+ if (contradiction) {
2741
+ return { proofs, sql: boundedSql, repairs, error: contradiction };
2742
+ }
2616
2743
  }
2617
2744
  const result = await executeGeneratedSqlForAgent(boundedSql);
2618
2745
  return { result, proofs, sql: boundedSql, repairs };
@@ -3461,7 +3588,7 @@ export async function startLocalServer(opts) {
3461
3588
  throw new Error(`Provide required parameter${invocation.unresolvedParameters.length === 1 ? '' : 's'}: ${invocation.unresolvedParameters.join(', ')}.`);
3462
3589
  }
3463
3590
  const semanticCompose = semanticLayer
3464
- ? composeSemanticBlockSql(source, semanticLayer, {
3591
+ ? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
3465
3592
  driver: activeConnection.driver,
3466
3593
  tableMapping,
3467
3594
  projectRoot,
@@ -3515,7 +3642,7 @@ export async function startLocalServer(opts) {
3515
3642
  // pre-compiled query, that's the query (not a recompiled metric), so the test's
3516
3643
  // output columns match the block's declared outputs.
3517
3644
  const semanticCompose = semanticLayer
3518
- ? composeSemanticBlockSql(source, semanticLayer, {
3645
+ ? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
3519
3646
  driver: activeConnection.driver,
3520
3647
  tableMapping,
3521
3648
  projectRoot,
@@ -3733,8 +3860,19 @@ export async function startLocalServer(opts) {
3733
3860
  }
3734
3861
  }
3735
3862
  if (req.method === 'GET' && path === '/api/health') {
3863
+ // REL-002: expose runtime/build identity so a server started before a
3864
+ // rebuild or running a different version than the project pin is visibly
3865
+ // stale. Uses only the cached latest-version lookup — never blocks.
3866
+ const versionStatus = resolveDqlRuntimeVersionStatus({ projectRoot, runningVersion: runtimeVersion });
3867
+ void fetchLatestPublishedDqlVersion();
3868
+ const healthValueGrounding = resolveAgentRuntimeValueGrounding(projectConfig);
3869
+ const retrievalHealth = resolveRetrievalHealthStatus({
3870
+ projectRoot,
3871
+ valueGroundingMode: healthValueGrounding.mode,
3872
+ searchSafeColumnCount: healthValueGrounding.searchSafeColumns.size,
3873
+ });
3736
3874
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
3737
- res.end(serializeJSON({ status: 'ok', version: runtimeVersion }));
3875
+ res.end(serializeJSON({ status: 'ok', version: runtimeVersion, versionStatus, retrievalHealth }));
3738
3876
  return;
3739
3877
  }
3740
3878
  if (req.method === 'GET' && path === '/api/onboarding/launch') {
@@ -4543,6 +4681,11 @@ export async function startLocalServer(opts) {
4543
4681
  const conversationStore = parsed.request.threadId ? getConversationStore() : null;
4544
4682
  if (conversationStore && parsed.request.threadId && conversationStore.getThread(parsed.request.threadId)) {
4545
4683
  parsed.request.conversationContext = await conversationContextFromThread(conversationStore, parsed.request.threadId, parsed.request.conversationContext, parsed.request.question);
4684
+ // The persisted thread is authoritative for prior turns. Raw client
4685
+ // history would duplicate the same conversation into the prompt a
4686
+ // second time — dropping it keeps follow-up prompts bounded.
4687
+ if (parsed.request.history?.length)
4688
+ parsed.request.history = [];
4546
4689
  }
4547
4690
  const wantsStream = url.searchParams.get('stream') === '1' || url.searchParams.get('stream') === 'true';
4548
4691
  const runId = parsed.request.runId;
@@ -5917,7 +6060,7 @@ export async function startLocalServer(opts) {
5917
6060
  operator: filter.operator,
5918
6061
  values: Array.isArray(filter.value) ? filter.value.map(String) : [String(filter.value)],
5919
6062
  }));
5920
- const composed = composeRuntimeSemanticQuery({
6063
+ const composed = await composeRuntimeSemanticQuery({
5921
6064
  metrics: item.semantic.metrics,
5922
6065
  dimensions: item.semantic.dimensions ?? [],
5923
6066
  filters: [...staticFilters, ...activeFilters],
@@ -6008,7 +6151,7 @@ export async function startLocalServer(opts) {
6008
6151
  continue;
6009
6152
  }
6010
6153
  const semanticCompose = semanticLayer
6011
- ? composeSemanticBlockSql(source, semanticLayer, {
6154
+ ? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
6012
6155
  driver: targetConnection.driver,
6013
6156
  tableMapping,
6014
6157
  projectRoot,
@@ -7387,7 +7530,7 @@ export async function startLocalServer(opts) {
7387
7530
  const activeConnection = requireActiveConnection();
7388
7531
  const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer);
7389
7532
  const semanticCompose = semanticLayer
7390
- ? composeSemanticBlockSql(source, semanticLayer, {
7533
+ ? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
7391
7534
  driver: activeConnection.driver,
7392
7535
  tableMapping,
7393
7536
  projectRoot,
@@ -7893,8 +8036,23 @@ export async function startLocalServer(opts) {
7893
8036
  draftSave: readiness.candidate.draftSave ?? { status: 'pending' },
7894
8037
  };
7895
8038
  writeBlockStudioImportCandidate(projectRoot, importId, next);
7896
- await refreshLocalMetadataCatalog(projectRoot);
7897
- const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
8039
+ let compiledManifest;
8040
+ let lineageRefresh;
8041
+ try {
8042
+ compiledManifest = compileBlockStudioManifest(projectRoot, projectConfig);
8043
+ lineageRefresh = { status: 'ready', compiledAt: new Date().toISOString() };
8044
+ }
8045
+ catch (error) {
8046
+ lineageRefresh = {
8047
+ status: 'failed',
8048
+ message: error instanceof Error ? error.message : String(error),
8049
+ };
8050
+ }
8051
+ await refreshLocalMetadataCatalog(projectRoot, compiledManifest, semanticLayer);
8052
+ const payload = {
8053
+ ...openBlockStudioDocument(projectRoot, savedPath, semanticLayer),
8054
+ lineageRefresh,
8055
+ };
7898
8056
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
7899
8057
  res.end(serializeJSON({ candidate: next, block: payload, certification }));
7900
8058
  return;
@@ -8007,10 +8165,23 @@ export async function startLocalServer(opts) {
8007
8165
  }
8008
8166
  const nextSession = { ...session, candidates: nextCandidates, updatedAt: new Date().toISOString() };
8009
8167
  writeBlockStudioImportSession(projectRoot, nextSession);
8010
- if (saved.length > 0)
8011
- await refreshLocalMetadataCatalog(projectRoot);
8168
+ let lineageRefresh;
8169
+ if (saved.length > 0) {
8170
+ let compiledManifest;
8171
+ try {
8172
+ compiledManifest = compileBlockStudioManifest(projectRoot, projectConfig);
8173
+ lineageRefresh = { status: 'ready', compiledAt: new Date().toISOString() };
8174
+ }
8175
+ catch (error) {
8176
+ lineageRefresh = {
8177
+ status: 'failed',
8178
+ message: error instanceof Error ? error.message : String(error),
8179
+ };
8180
+ }
8181
+ await refreshLocalMetadataCatalog(projectRoot, compiledManifest, semanticLayer);
8182
+ }
8012
8183
  res.writeHead(errors.length > 0 ? 207 : 200, { 'Content-Type': 'application/json; charset=utf-8' });
8013
- res.end(serializeJSON({ ok: errors.length === 0, session: nextSession, saved, errors }));
8184
+ res.end(serializeJSON({ ok: errors.length === 0, session: nextSession, saved, errors, lineageRefresh }));
8014
8185
  }
8015
8186
  catch (error) {
8016
8187
  res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -8154,9 +8325,14 @@ export async function startLocalServer(opts) {
8154
8325
  const connections = getProjectConnectionsForApi(cfg);
8155
8326
  const defaultKey = resolveDefaultConnectionKey(cfg, connections) ?? Object.keys(connections)[0] ?? 'default';
8156
8327
  const userPrefs = readUserPrefs(userPrefsPath);
8328
+ // UI-009 / PERF-001: Block Studio already loads the canonical semantic
8329
+ // layer. Let it omit this second, potentially multi-megabyte rendering
8330
+ // of the same 7,500+ object catalog while keeping the route compatible
8331
+ // for older clients.
8332
+ const includeSemantic = url.searchParams.get('includeSemantic') !== 'false';
8157
8333
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
8158
8334
  res.end(serializeJSON({
8159
- semanticTree: semanticLayer ? buildSemanticTree(semanticLayer, semanticImportManifest) : null,
8335
+ semanticTree: includeSemantic && semanticLayer ? buildSemanticTree(semanticLayer, semanticImportManifest) : null,
8160
8336
  databaseTree: await buildDatabaseSchemaTree(projectRoot, executor, connection),
8161
8337
  connection: {
8162
8338
  default: defaultKey,
@@ -8317,8 +8493,26 @@ export async function startLocalServer(opts) {
8317
8493
  stableSuffix: metadata.candidateId,
8318
8494
  })
8319
8495
  : saveBlockStudioArtifacts(projectRoot, saveOptions);
8320
- await refreshLocalMetadataCatalog(projectRoot);
8321
- const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
8496
+ let compiledManifest;
8497
+ let lineageRefresh;
8498
+ try {
8499
+ compiledManifest = compileBlockStudioManifest(projectRoot, projectConfig);
8500
+ lineageRefresh = { status: 'ready', compiledAt: new Date().toISOString() };
8501
+ }
8502
+ catch (error) {
8503
+ // The block is already safely stored. Preserve it and report the
8504
+ // rebuild failure separately instead of turning a successful save
8505
+ // into a misleading 500 response.
8506
+ lineageRefresh = {
8507
+ status: 'failed',
8508
+ message: error instanceof Error ? error.message : String(error),
8509
+ };
8510
+ }
8511
+ await refreshLocalMetadataCatalog(projectRoot, compiledManifest, semanticLayer);
8512
+ const payload = {
8513
+ ...openBlockStudioDocument(projectRoot, savedPath, semanticLayer),
8514
+ lineageRefresh,
8515
+ };
8322
8516
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
8323
8517
  res.end(serializeJSON(payload));
8324
8518
  }
@@ -8717,6 +8911,47 @@ export async function startLocalServer(opts) {
8717
8911
  }
8718
8912
  return;
8719
8913
  }
8914
+ // ── Semantic runtime adapters (API-004 / UI-009 / E2E-008) ───────────────
8915
+ if (req.method === 'GET' && path === '/api/semantic-runtime') {
8916
+ try {
8917
+ const runtime = await getSemanticRuntimeStatus(projectRoot, { probeConfiguredCloud: true });
8918
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
8919
+ res.end(serializeJSON({ ...getSemanticRuntimeSettings(projectRoot), runtime }));
8920
+ }
8921
+ catch (error) {
8922
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
8923
+ res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
8924
+ }
8925
+ return;
8926
+ }
8927
+ if (req.method === 'POST' && path === '/api/semantic-runtime/dbt-cloud/test') {
8928
+ try {
8929
+ const body = await readJSON(req);
8930
+ const result = await testSemanticRuntimeDraft(projectRoot, body);
8931
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
8932
+ res.end(serializeJSON(result));
8933
+ }
8934
+ catch (error) {
8935
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
8936
+ res.end(serializeJSON({ ok: false, error: error instanceof Error ? error.message : String(error) }));
8937
+ }
8938
+ return;
8939
+ }
8940
+ if (req.method === 'POST' && path === '/api/semantic-runtime/dbt-cloud/apply') {
8941
+ try {
8942
+ const body = await readJSON(req);
8943
+ const result = await testSemanticRuntimeDraft(projectRoot, body);
8944
+ const settings = saveTestedSemanticRuntimeSettings(projectRoot, body, result);
8945
+ const runtime = await getSemanticRuntimeStatus(projectRoot);
8946
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
8947
+ res.end(serializeJSON({ ok: true, ...settings, runtime }));
8948
+ }
8949
+ catch (error) {
8950
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
8951
+ res.end(serializeJSON({ ok: false, error: error instanceof Error ? error.message : String(error) }));
8952
+ }
8953
+ return;
8954
+ }
8720
8955
  // ── Semantic layer discovery API ─────────────────────────────────────────
8721
8956
  if (req.method === 'GET' && path === '/api/semantic-layer') {
8722
8957
  const userPrefs = readUserPrefs(userPrefsPath);
@@ -8746,8 +8981,7 @@ export async function startLocalServer(opts) {
8746
8981
  const dbtManifestReady = provider === 'dbt'
8747
8982
  ? hasDbtSemanticManifest(projectRoot, semanticConfig?.projectPath)
8748
8983
  : false;
8749
- const metricFlowReady = provider === 'dbt' ? hasMetricFlowCli() : false;
8750
- const dbtExecutionReady = dbtManifestReady && metricFlowReady;
8984
+ const semanticRuntime = await getSemanticRuntimeStatus(projectRoot, { probeConfiguredCloud: true });
8751
8985
  const metrics = semanticLayer.listMetrics().map((m) => ({
8752
8986
  name: m.name,
8753
8987
  label: m.label,
@@ -8762,7 +8996,7 @@ export async function startLocalServer(opts) {
8762
8996
  typeParams: m.typeParams ?? null,
8763
8997
  filter: m.filter ?? null,
8764
8998
  source: m.source ?? null,
8765
- execution: semanticMetricExecutionCapability(m.name, semanticLayer, provider, metricFlowReady, connection?.driver),
8999
+ execution: runtimeMetricExecutionCapability(m.name, semanticLayer, provider, semanticRuntime),
8766
9000
  }));
8767
9001
  const measures = semanticLayer.listMeasures().map((m) => ({
8768
9002
  name: m.name,
@@ -8860,24 +9094,21 @@ export async function startLocalServer(opts) {
8860
9094
  owner: q.owner ?? null,
8861
9095
  source: q.source ?? null,
8862
9096
  }));
8863
- const dbtExecutionSetup = dbtExecutionReady
8864
- ? null
8865
- : !dbtManifestReady && !metricFlowReady
8866
- ? 'Run `dbt parse` or `dbt build` so target/semantic_manifest.json exists, and install MetricFlow so `mf` is on PATH.'
8867
- : !dbtManifestReady
8868
- ? 'Run `dbt parse` or `dbt build` so target/semantic_manifest.json exists.'
8869
- : 'Install MetricFlow so `mf` is on PATH, or set DQL_METRICFLOW_BIN to the MetricFlow executable.';
9097
+ const dbtExecutionSetup = !dbtManifestReady
9098
+ ? 'Run `dbt parse` or `dbt build` so target/semantic_manifest.json exists.'
9099
+ : semanticRuntime.setup;
8870
9100
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
8871
9101
  res.end(serializeJSON({
8872
9102
  available: true,
8873
9103
  provider,
8874
9104
  execution: provider === 'dbt'
8875
9105
  ? {
8876
- engine: 'metricflow',
8877
- ready: dbtExecutionReady,
9106
+ engine: semanticRuntime.active,
9107
+ ready: semanticRuntime.active !== 'native' || metrics.every((metric) => metric.execution.status === 'ready'),
8878
9108
  setup: dbtExecutionSetup,
9109
+ adapters: semanticRuntime.adapters,
8879
9110
  }
8880
- : { engine: 'native', ready: true, setup: null },
9111
+ : { engine: 'native', ready: true, setup: null, adapters: semanticRuntime.adapters },
8881
9112
  errors: semanticLayerErrors,
8882
9113
  metrics,
8883
9114
  measures,
@@ -9214,7 +9445,7 @@ export async function startLocalServer(opts) {
9214
9445
  .split(',')
9215
9446
  .map((value) => value.trim())
9216
9447
  .filter(Boolean);
9217
- const dimensions = semanticLayer.listCompatibleDimensions(metrics).map((d) => ({
9448
+ const dimensions = (await listRuntimeCompatibleDimensions(projectRoot, semanticLayer, metrics)).map((d) => ({
9218
9449
  name: d.name,
9219
9450
  label: d.label,
9220
9451
  description: d.description,
@@ -9727,37 +9958,8 @@ export async function startLocalServer(opts) {
9727
9958
  // Resolve which connection to use — request can override default
9728
9959
  const targetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
9729
9960
  const driver = targetConnection.driver;
9730
- // Build table mapping: resolve semantic model names to actual DB table names
9731
- let tableMapping;
9732
- try {
9733
- const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name FROM information_schema.tables WHERE UPPER(table_schema) NOT IN ('INFORMATION_SCHEMA', 'PG_CATALOG')`, [], {}, targetConnection);
9734
- const dbTableNames = new Set();
9735
- const schemaQualified = new Map();
9736
- for (const row of tablesResult.rows) {
9737
- const schema = String(row['table_schema'] ?? '');
9738
- const name = String(row['table_name'] ?? '');
9739
- dbTableNames.add(name);
9740
- schemaQualified.set(name, schema ? `${schema}.${name}` : name);
9741
- }
9742
- // For each table in the semantic layer, map to qualified name if it exists
9743
- const allSemanticTables = new Set();
9744
- for (const m of semanticLayer.listMetrics())
9745
- allSemanticTables.add(m.table);
9746
- for (const d of semanticLayer.listDimensions())
9747
- allSemanticTables.add(d.table);
9748
- tableMapping = {};
9749
- for (const semTable of allSemanticTables) {
9750
- if (dbTableNames.has(semTable) && schemaQualified.has(semTable)) {
9751
- tableMapping[semTable] = schemaQualified.get(semTable);
9752
- }
9753
- }
9754
- if (Object.keys(tableMapping).length === 0)
9755
- tableMapping = undefined;
9756
- }
9757
- catch {
9758
- // Non-fatal: proceed without table mapping
9759
- }
9760
- const composed = composeRuntimeSemanticQuery({
9961
+ const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer);
9962
+ const composed = await composeRuntimeSemanticQuery({
9761
9963
  metrics,
9762
9964
  dimensions,
9763
9965
  filters,
@@ -9775,10 +9977,10 @@ export async function startLocalServer(opts) {
9775
9977
  });
9776
9978
  if (!composed) {
9777
9979
  const provider = semanticConfig?.provider ?? semanticDetectedProvider ?? 'dql';
9778
- const metricFlowReady = provider === 'dbt' && hasMetricFlowCli();
9980
+ const semanticRuntime = await getSemanticRuntimeStatus(projectRoot, { probeConfiguredCloud: true });
9779
9981
  const blocked = metrics.map((metricName) => ({
9780
9982
  metric: metricName,
9781
- ...semanticMetricExecutionCapability(metricName, semanticLayer, provider, metricFlowReady, driver),
9983
+ ...runtimeMetricExecutionCapability(metricName, semanticLayer, provider, semanticRuntime),
9782
9984
  })).filter((capability) => capability.status !== 'ready');
9783
9985
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
9784
9986
  res.end(serializeJSON({
@@ -9808,13 +10010,14 @@ export async function startLocalServer(opts) {
9808
10010
  res.end(serializeJSON({ error: error.message, code: 'unauthorized' }));
9809
10011
  return;
9810
10012
  }
9811
- const status = error instanceof MetricFlowUnavailableError ? 400 : 500;
10013
+ const runtimeError = isSemanticRuntimeError(error);
10014
+ const status = runtimeError ? 400 : 500;
9812
10015
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
9813
10016
  res.end(serializeJSON({
9814
10017
  error: error instanceof Error ? error.message : String(error),
9815
- code: error instanceof MetricFlowUnavailableError ? 'metricflow_unavailable' : undefined,
9816
- hint: error instanceof MetricFlowUnavailableError
9817
- ? 'Install dbt Semantic Layer dependencies, run dbt parse/build to create target/semantic_manifest.json, then retry.'
10018
+ code: runtimeError ? 'SEMANTIC_RUNTIME_REQUIRED' : undefined,
10019
+ hint: runtimeError
10020
+ ? 'Configure dbt Cloud Semantic Layer in Project & dbt settings, or install a compatible local MetricFlow runtime.'
9818
10021
  : undefined,
9819
10022
  }));
9820
10023
  }
@@ -9831,31 +10034,8 @@ export async function startLocalServer(opts) {
9831
10034
  const { metrics = [], dimensions = [], filters = [], limit, timeDimension, orderBy, savedQuery, engine } = body;
9832
10035
  const targetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
9833
10036
  const driver = targetConnection.driver;
9834
- let tableMapping;
9835
- try {
9836
- const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name FROM information_schema.tables WHERE UPPER(table_schema) NOT IN ('INFORMATION_SCHEMA', 'PG_CATALOG')`, [], {}, targetConnection);
9837
- const schemaQualified = new Map();
9838
- for (const row of tablesResult.rows) {
9839
- const schema = String(row['table_schema'] ?? '');
9840
- const name = String(row['table_name'] ?? '');
9841
- schemaQualified.set(name, schema ? `${schema}.${name}` : name);
9842
- }
9843
- tableMapping = {};
9844
- for (const metric of semanticLayer.listMetrics()) {
9845
- if (schemaQualified.has(metric.table))
9846
- tableMapping[metric.table] = schemaQualified.get(metric.table);
9847
- }
9848
- for (const dimension of semanticLayer.listDimensions()) {
9849
- if (schemaQualified.has(dimension.table))
9850
- tableMapping[dimension.table] = schemaQualified.get(dimension.table);
9851
- }
9852
- if (Object.keys(tableMapping).length === 0)
9853
- tableMapping = undefined;
9854
- }
9855
- catch {
9856
- tableMapping = undefined;
9857
- }
9858
- const composed = composeRuntimeSemanticQuery({
10037
+ const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer);
10038
+ const composed = await composeRuntimeSemanticQuery({
9859
10039
  metrics,
9860
10040
  dimensions,
9861
10041
  filters,
@@ -9873,10 +10053,10 @@ export async function startLocalServer(opts) {
9873
10053
  });
9874
10054
  if (!composed) {
9875
10055
  const provider = semanticConfig?.provider ?? semanticDetectedProvider ?? 'dql';
9876
- const metricFlowReady = provider === 'dbt' && hasMetricFlowCli();
10056
+ const semanticRuntime = await getSemanticRuntimeStatus(projectRoot, { probeConfiguredCloud: true });
9877
10057
  const blocked = metrics.map((metricName) => ({
9878
10058
  metric: metricName,
9879
- ...semanticMetricExecutionCapability(metricName, semanticLayer, provider, metricFlowReady, driver),
10059
+ ...runtimeMetricExecutionCapability(metricName, semanticLayer, provider, semanticRuntime),
9880
10060
  })).filter((capability) => capability.status !== 'ready');
9881
10061
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
9882
10062
  res.end(serializeJSON({
@@ -9900,13 +10080,14 @@ export async function startLocalServer(opts) {
9900
10080
  }));
9901
10081
  }
9902
10082
  catch (error) {
9903
- const status = error instanceof MetricFlowUnavailableError ? 400 : 500;
10083
+ const runtimeError = isSemanticRuntimeError(error);
10084
+ const status = runtimeError ? 400 : 500;
9904
10085
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
9905
10086
  res.end(serializeJSON({
9906
10087
  error: error instanceof Error ? error.message : String(error),
9907
- code: error instanceof MetricFlowUnavailableError ? 'metricflow_unavailable' : undefined,
9908
- hint: error instanceof MetricFlowUnavailableError
9909
- ? 'Install dbt Semantic Layer dependencies, run dbt parse/build to create target/semantic_manifest.json, then retry.'
10088
+ code: runtimeError ? 'SEMANTIC_RUNTIME_REQUIRED' : undefined,
10089
+ hint: runtimeError
10090
+ ? 'Configure dbt Cloud Semantic Layer in Project & dbt settings, or install a compatible local MetricFlow runtime.'
9910
10091
  : undefined,
9911
10092
  }));
9912
10093
  }
@@ -9931,7 +10112,8 @@ export async function startLocalServer(opts) {
9931
10112
  return;
9932
10113
  }
9933
10114
  const targetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
9934
- const composed = composeRuntimeSemanticQuery({
10115
+ const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer);
10116
+ const composed = await composeRuntimeSemanticQuery({
9935
10117
  metrics,
9936
10118
  dimensions,
9937
10119
  filters,
@@ -9942,6 +10124,7 @@ export async function startLocalServer(opts) {
9942
10124
  projectConfig,
9943
10125
  detectedProvider: semanticDetectedProvider,
9944
10126
  driver: targetConnection.driver,
10127
+ tableMapping,
9945
10128
  });
9946
10129
  if (!composed) {
9947
10130
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -9972,13 +10155,14 @@ export async function startLocalServer(opts) {
9972
10155
  res.end(serializeJSON({ error: 'Block already exists' }));
9973
10156
  return;
9974
10157
  }
9975
- const status = error instanceof MetricFlowUnavailableError ? 400 : 500;
10158
+ const runtimeError = isSemanticRuntimeError(error);
10159
+ const status = runtimeError ? 400 : 500;
9976
10160
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
9977
10161
  res.end(serializeJSON({
9978
10162
  error: error instanceof Error ? error.message : String(error),
9979
- code: error instanceof MetricFlowUnavailableError ? 'metricflow_unavailable' : undefined,
9980
- hint: error instanceof MetricFlowUnavailableError
9981
- ? 'Install dbt Semantic Layer dependencies, run dbt parse/build to create target/semantic_manifest.json, then retry.'
10163
+ code: runtimeError ? 'SEMANTIC_RUNTIME_REQUIRED' : undefined,
10164
+ hint: runtimeError
10165
+ ? 'Configure dbt Cloud Semantic Layer in Project & dbt settings, or install a compatible local MetricFlow runtime.'
9982
10166
  : undefined,
9983
10167
  }));
9984
10168
  }
@@ -10300,7 +10484,12 @@ export async function startLocalServer(opts) {
10300
10484
  parameters,
10301
10485
  question: typeof body.question === 'string' ? body.question : undefined,
10302
10486
  });
10303
- const contract = prepareBlockInvocation({ source, parameters, surface: 'ask_ai' });
10487
+ const contract = prepareBlockInvocation({
10488
+ source,
10489
+ parameters,
10490
+ question: typeof body.question === 'string' ? body.question : undefined,
10491
+ surface: 'ask_ai',
10492
+ });
10304
10493
  const certified = /\bstatus\s*=\s*"certified"/i.test(source);
10305
10494
  const semantic = /\btype\s*=\s*"semantic"/i.test(source);
10306
10495
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -10396,6 +10585,7 @@ export async function startLocalServer(opts) {
10396
10585
  parameters: body.parameters && typeof body.parameters === 'object' && !Array.isArray(body.parameters)
10397
10586
  ? body.parameters
10398
10587
  : {},
10588
+ question: typeof body.question === 'string' ? body.question : undefined,
10399
10589
  surface: 'notebook',
10400
10590
  })
10401
10591
  : null;
@@ -10414,15 +10604,31 @@ export async function startLocalServer(opts) {
10414
10604
  tableMapping,
10415
10605
  parameters: invocation?.values,
10416
10606
  });
10417
- if (!plan) {
10607
+ const semanticCompose = executableCell.type === 'dql'
10608
+ && semanticLayer
10609
+ && /\btype\s*=\s*"semantic"/i.test(executableCell.source)
10610
+ ? await composeSemanticBlockSqlForRuntime(executableCell.source, semanticLayer, {
10611
+ driver: cellConnection.driver,
10612
+ tableMapping,
10613
+ parameters: invocation?.values,
10614
+ detectedProvider: semanticDetectedProvider,
10615
+ projectRoot,
10616
+ projectConfig,
10617
+ })
10618
+ : null;
10619
+ if (semanticCompose && !semanticCompose.sql) {
10620
+ throw new Error(semanticCompose.diagnostics.map((diagnostic) => diagnostic.message).join(' '));
10621
+ }
10622
+ const executableSql = semanticCompose?.sql ?? plan?.sql;
10623
+ if (!executableSql) {
10418
10624
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
10419
10625
  res.end(serializeJSON({ cellType: cell.type, result: null }));
10420
10626
  return;
10421
10627
  }
10422
- const prepared = prepareLocalExecution(plan.sql, cellConnection, projectRoot, projectConfig);
10628
+ const prepared = prepareLocalExecution(executableSql, cellConnection, projectRoot, projectConfig);
10423
10629
  const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
10424
10630
  assertAppAccess({ app, domain: resolved.domain ?? app?.domain, level: 'execute' });
10425
- const rawResult = await executor.executeQuery(prepared.sql, plan.sqlParams, runtimeVariables({ ...plan.variables, ...(invocation?.values ?? {}) }), prepared.connection);
10631
+ const rawResult = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...(invocation?.values ?? {}) }), prepared.connection);
10426
10632
  const normalized = normalizeQueryResult(rawResult);
10427
10633
  // Enforce the block's declared invariants against the result set. This
10428
10634
  // is additive: blocks without invariants produce `null` and the
@@ -10436,29 +10642,29 @@ export async function startLocalServer(opts) {
10436
10642
  recordNotebookQueryRun(projectRoot, {
10437
10643
  notebookPath: execContext.notebookPath,
10438
10644
  cellId: execContext.cellId ?? cell.id,
10439
- cellName: execContext.cellName ?? plan.title ?? resolved.blockName,
10645
+ cellName: execContext.cellName ?? plan?.title ?? resolved.blockName,
10440
10646
  researchRunId: execContext.researchRunId,
10441
10647
  source: execContext.source ?? (cell.type === 'dql' ? 'notebook_dql_cell' : 'notebook_cell'),
10442
10648
  status: 'success',
10443
10649
  rowCount: normalized.rowCount ?? normalized.rows.length,
10444
10650
  durationMs: Date.now() - start,
10445
- sql: plan.sql,
10651
+ sql: executableSql,
10446
10652
  objectKey: resolved.blockPath,
10447
10653
  });
10448
10654
  updateNotebookResearchFromCellExecution(projectRoot, execContext, {
10449
10655
  status: 'success',
10450
10656
  resultPreview: normalized,
10451
- sql: plan.sql,
10657
+ sql: executableSql,
10452
10658
  });
10453
10659
  }
10454
10660
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
10455
10661
  res.end(serializeJSON({
10456
10662
  cellType: cell.type,
10457
- title: plan.title,
10663
+ title: plan?.title,
10458
10664
  blockName: resolved.blockName,
10459
10665
  blockPath: resolved.blockPath,
10460
- chartConfig: plan.chartConfig,
10461
- tests: plan.tests,
10666
+ chartConfig: plan?.chartConfig,
10667
+ tests: plan?.tests,
10462
10668
  result: normalized,
10463
10669
  ...(invocation ? {
10464
10670
  invocation: {
@@ -11391,9 +11597,34 @@ function latestOpenContextBootstrapSession(projectRoot) {
11391
11597
  }
11392
11598
  return null;
11393
11599
  }
11394
- async function refreshLocalMetadataCatalog(projectRoot) {
11600
+ /**
11601
+ * API-004 / E2E-006: save-time compile for Block Studio. The manifest is
11602
+ * replaced atomically so lineage readers and agent retrieval never observe a
11603
+ * half-written snapshot, and an existing manifest survives compilation errors.
11604
+ */
11605
+ export function compileBlockStudioManifest(projectRoot, projectConfig = loadProjectConfig(projectRoot)) {
11606
+ const dbtManifestPath = resolveDbtManifestPath(projectRoot, projectConfig) ?? undefined;
11607
+ const manifest = buildManifest({ projectRoot, dqlVersion: 'notebook', dbtManifestPath });
11608
+ const manifestPath = join(projectRoot, 'dql-manifest.json');
11609
+ const tempPath = `${manifestPath}.tmp-${process.pid}-${Date.now()}`;
11610
+ try {
11611
+ writeFileSync(tempPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
11612
+ renameSync(tempPath, manifestPath);
11613
+ }
11614
+ catch (error) {
11615
+ rmSync(tempPath, { force: true });
11616
+ throw error;
11617
+ }
11618
+ _lineageCache.delete(projectRoot);
11619
+ return manifest;
11620
+ }
11621
+ async function refreshLocalMetadataCatalog(projectRoot, manifest, semanticLayer) {
11395
11622
  try {
11396
- await ensureMetadataCatalogFresh(projectRoot, { force: true });
11623
+ await ensureMetadataCatalogFresh(projectRoot, {
11624
+ force: true,
11625
+ ...(manifest ? { manifest } : {}),
11626
+ ...(semanticLayer ? { semanticLayer } : {}),
11627
+ });
11397
11628
  }
11398
11629
  catch {
11399
11630
  // The catalog is a rebuildable local cache. Save/certify flows should not
@@ -12846,13 +13077,13 @@ export function buildAgentPreviewSql(sql) {
12846
13077
  * only when the owning entity is retained in GROUP BY. It never invents a
12847
13078
  * relation, join key, or allocation rule.
12848
13079
  */
12849
- export function repairExploratorySqlBeforeExecution(sql, schemaContext, question = '') {
13080
+ export function repairExploratorySqlBeforeExecution(sql, schemaContext, question = '', dialect = 'duckdb') {
12850
13081
  const repairs = [];
12851
- let repairedSql = qualifyExploratoryRelationsFromSchema(sql, schemaContext, repairs);
12852
- repairedSql = repairExploratoryRelationQualifiers(repairedSql, repairs);
12853
- repairedSql = repairExploratoryLifetimeMeasureSelection(repairedSql, schemaContext, question, repairs);
13082
+ let repairedSql = qualifyExploratoryRelationsFromSchema(sql, schemaContext, repairs, dialect);
13083
+ repairedSql = repairExploratoryRelationQualifiers(repairedSql, repairs, dialect);
13084
+ repairedSql = repairExploratoryLifetimeMeasureSelection(repairedSql, schemaContext, question, repairs, dialect);
12854
13085
  repairedSql = repairExploratoryMisleadingPercentAliases(repairedSql, question, repairs);
12855
- const grainRepair = repairExploratoryNonAdditiveAggregates(repairedSql, schemaContext, repairs);
13086
+ const grainRepair = repairExploratoryNonAdditiveAggregates(repairedSql, schemaContext, repairs, dialect);
12856
13087
  repairedSql = grainRepair.sql;
12857
13088
  return {
12858
13089
  sql: repairedSql,
@@ -12903,8 +13134,8 @@ export function applyRequestedTopNToExploratorySql(sql, requestedTopN) {
12903
13134
  }
12904
13135
  return `${withoutTerminator}\nLIMIT ${requestedTopN}`;
12905
13136
  }
12906
- function qualifyExploratoryRelationsFromSchema(sql, schemaContext, repairs) {
12907
- const analysis = analyzeSqlReferences(sql);
13137
+ function qualifyExploratoryRelationsFromSchema(sql, schemaContext, repairs, dialect = 'duckdb') {
13138
+ const analysis = analyzeSqlReferences(sql, dialect);
12908
13139
  if (!analysis.parsed)
12909
13140
  return sql;
12910
13141
  let repaired = sql;
@@ -12932,8 +13163,8 @@ function qualifyExploratoryRelationsFromSchema(sql, schemaContext, repairs) {
12932
13163
  }
12933
13164
  return repaired;
12934
13165
  }
12935
- function repairExploratoryRelationQualifiers(sql, repairs) {
12936
- const analysis = analyzeSqlReferences(sql);
13166
+ function repairExploratoryRelationQualifiers(sql, repairs, dialect = 'duckdb') {
13167
+ const analysis = analyzeSqlReferences(sql, dialect);
12937
13168
  if (!analysis.parsed)
12938
13169
  return sql;
12939
13170
  const declared = Object.entries(analysis.aliasToRelation).map(([qualifier, relation]) => ({
@@ -12956,10 +13187,10 @@ function repairExploratoryRelationQualifiers(sql, repairs) {
12956
13187
  }
12957
13188
  return repaired;
12958
13189
  }
12959
- function repairExploratoryLifetimeMeasureSelection(sql, schemaContext, question, repairs) {
13190
+ function repairExploratoryLifetimeMeasureSelection(sql, schemaContext, question, repairs, dialect = 'duckdb') {
12960
13191
  if (!/\b(lifetime|life\s*span|lifespan|customer\s+life)\b/i.test(question))
12961
13192
  return sql;
12962
- const analysis = analyzeSqlReferences(sql);
13193
+ const analysis = analyzeSqlReferences(sql, dialect);
12963
13194
  if (!analysis.parsed || analysis.joins.length === 0)
12964
13195
  return sql;
12965
13196
  const groupClause = sql.match(/\bgroup\s+by\s+([\s\S]*?)(?:\border\s+by\b|\blimit\b|\bqualify\b|\bhaving\b|$)/i)?.[1] ?? '';
@@ -12985,8 +13216,8 @@ function repairExploratoryLifetimeMeasureSelection(sql, schemaContext, question,
12985
13216
  }
12986
13217
  return repaired;
12987
13218
  }
12988
- function repairExploratoryNonAdditiveAggregates(sql, schemaContext, repairs) {
12989
- const analysis = analyzeSqlReferences(sql);
13219
+ function repairExploratoryNonAdditiveAggregates(sql, schemaContext, repairs, dialect = 'duckdb') {
13220
+ const analysis = analyzeSqlReferences(sql, dialect);
12990
13221
  if (!analysis.parsed || analysis.joins.length === 0)
12991
13222
  return { sql };
12992
13223
  const risky = analysis.aggregates.filter((aggregate) => aggregate.func.toLowerCase() === 'sum'
@@ -13108,6 +13339,16 @@ export function buildExploratoryJoinProbeSql(input) {
13108
13339
  SELECT right_key, COUNT(*) AS match_count
13109
13340
  FROM matches
13110
13341
  GROUP BY right_key
13342
+ ),
13343
+ left_key_counts AS (
13344
+ SELECT join_key, COUNT(*) AS key_count
13345
+ FROM left_sample
13346
+ GROUP BY join_key
13347
+ ),
13348
+ right_key_counts AS (
13349
+ SELECT join_key, COUNT(*) AS key_count
13350
+ FROM right_sample
13351
+ GROUP BY join_key
13111
13352
  )
13112
13353
  SELECT
13113
13354
  (SELECT COUNT(*) FROM left_sample) AS left_sample_rows,
@@ -13116,7 +13357,87 @@ SELECT
13116
13357
  (SELECT COUNT(*) FROM left_sample AS l WHERE NOT EXISTS (SELECT 1 FROM right_sample AS r WHERE r.join_key = l.join_key)) AS unmatched_left_sample_rows,
13117
13358
  (SELECT COUNT(*) FROM right_sample AS r WHERE NOT EXISTS (SELECT 1 FROM left_sample AS l WHERE l.join_key = r.join_key)) AS unmatched_right_sample_rows,
13118
13359
  (SELECT COALESCE(MAX(match_count), 0) FROM left_counts) AS max_matches_per_left_key,
13119
- (SELECT COALESCE(MAX(match_count), 0) FROM right_counts) AS max_matches_per_right_key`;
13360
+ (SELECT COALESCE(MAX(match_count), 0) FROM right_counts) AS max_matches_per_right_key,
13361
+ (SELECT COALESCE(MAX(key_count), 0) FROM left_key_counts) AS max_left_rows_per_key,
13362
+ (SELECT COALESCE(MAX(key_count), 0) FROM right_key_counts) AS max_right_rows_per_key`;
13363
+ }
13364
+ /**
13365
+ * CTE names are query-internal derived relations, not warehouse tables. A join
13366
+ * whose endpoint is a CTE (e.g. `WITH joy_items AS (…) … JOIN joy_items`) must
13367
+ * be excluded from declared-path enforcement and from join probes — probing
13368
+ * `FROM "joy_items"` throws a DuckDB catalog error for a table that was never
13369
+ * supposed to exist. The physical joins INSIDE the CTE are still present in
13370
+ * the parsed join list and get validated/probed on their own.
13371
+ */
13372
+ export function probeableExploratoryJoins(joins, ctes) {
13373
+ const cteNames = new Set(ctes.map((name) => name.split('.').at(-1)?.toLowerCase() ?? name.toLowerCase()));
13374
+ if (cteNames.size === 0)
13375
+ return joins;
13376
+ return joins.filter((join) => ![join.leftRelation, join.rightRelation].some((relation) => {
13377
+ const bare = relation?.replace(/["`\[\]]/g, '').split('.').at(-1)?.toLowerCase();
13378
+ return Boolean(bare && cteNames.has(bare));
13379
+ }));
13380
+ }
13381
+ function declaredExploratoryRelationMatches(sqlRelation, declaredRelation) {
13382
+ if (!declaredRelation)
13383
+ return false;
13384
+ const normalize = (value) => value.replace(/["`\[\]]/g, '').trim();
13385
+ return exploratoryRelationsMatch(normalize(sqlRelation), normalize(declaredRelation));
13386
+ }
13387
+ function exploratoryColumnsMatch(sqlColumn, declaredColumn) {
13388
+ const normalize = (value) => value.replace(/["`\[\]]/g, '').split('.').at(-1)?.trim().toLowerCase() ?? value.toLowerCase();
13389
+ return normalize(sqlColumn) === normalize(declaredColumn);
13390
+ }
13391
+ /**
13392
+ * Gate a join probe against the declared relationship. Returns an error string
13393
+ * when the UNFILTERED key samples structurally contradict the declaration:
13394
+ * - zero key overlap while both sides have rows (wrong/mistyped key), or
13395
+ * - duplicate keys on the declared "one" side of a *_to_one / one_to_* edge.
13396
+ * Sampling caveat: gates only fire on definitive contradictions, never on low
13397
+ * match rates, so a sparse but real relationship still executes.
13398
+ */
13399
+ export function exploratoryProbeContradiction(rows, join, edge) {
13400
+ const row = Array.isArray(rows) && rows[0] && typeof rows[0] === 'object'
13401
+ ? rows[0]
13402
+ : {};
13403
+ const number = (key) => {
13404
+ const value = Number(row[key]);
13405
+ return Number.isFinite(value) ? value : 0;
13406
+ };
13407
+ const leftRows = number('left_sample_rows');
13408
+ const rightRows = number('right_sample_rows');
13409
+ const joined = number('joined_rows');
13410
+ if (leftRows > 0 && rightRows > 0 && joined === 0) {
13411
+ return `The join key ${join.leftColumn} = ${join.rightColumn} produced no matching rows between ${join.leftRelation} and ${join.rightRelation} (unfiltered sample). The declared key pair is likely wrong — review the relationship before executing this analysis.`;
13412
+ }
13413
+ if (!edge)
13414
+ return undefined;
13415
+ // Orient the declared cardinality onto the parsed join: which physical side is
13416
+ // the declared "one" side? Duplicated keys there contradict the declaration.
13417
+ const leftIsFrom = Boolean(join.leftRelation && declaredExploratoryRelationMatches(join.leftRelation, edge.fromRelation));
13418
+ const rightIsFrom = Boolean(join.rightRelation && declaredExploratoryRelationMatches(join.rightRelation, edge.fromRelation));
13419
+ if (leftIsFrom === rightIsFrom)
13420
+ return undefined; // orientation unresolved — do not guess
13421
+ const oneSide = edge.cardinality === 'many_to_one' ? 'to' : edge.cardinality === 'one_to_many' ? 'from' : edge.cardinality === 'one_to_one' ? 'both' : undefined;
13422
+ if (!oneSide)
13423
+ return undefined;
13424
+ // Per-side key duplication measured WITHIN each sample (max rows sharing one
13425
+ // key value). Duplicates inside a sample are definitive — a subset of a table
13426
+ // cannot show duplicates the full table does not have. The pairwise
13427
+ // max_matches_* columns are NOT used here: they inflate whenever the
13428
+ // legitimate "many" side repeats a key value.
13429
+ const leftDuplicated = number('max_left_rows_per_key') > 1;
13430
+ const rightDuplicated = number('max_right_rows_per_key') > 1;
13431
+ const fromSideIsLeft = leftIsFrom;
13432
+ const duplicatedSides = [
13433
+ ...(leftDuplicated ? [fromSideIsLeft ? 'from' : 'to'] : []),
13434
+ ...(rightDuplicated ? [fromSideIsLeft ? 'to' : 'from'] : []),
13435
+ ];
13436
+ const violated = oneSide === 'both' ? duplicatedSides.length > 0 : duplicatedSides.includes(oneSide);
13437
+ if (violated) {
13438
+ return `Relationship "${edge.relationshipId}" declares ${edge.cardinality}, but the sampled join keys show duplicates on the declared unique side. Executing would duplicate rows — validate and correct the relationship first.`;
13439
+ }
13440
+ return undefined;
13120
13441
  }
13121
13442
  function summarizeExploratoryJoinProbe(rows, leftRelation, leftColumn, rightRelation, rightColumn) {
13122
13443
  const row = Array.isArray(rows) && rows[0] && typeof rows[0] === 'object'
@@ -13716,28 +14037,6 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
13716
14037
  validation: validateBlockStudioSource(source, semanticLayer),
13717
14038
  };
13718
14039
  }
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
- }
13741
14040
  function parseBlockStudioArrayField(source, key) {
13742
14041
  const match = source.match(new RegExp(`\\b${key}\\s*=\\s*\\[([\\s\\S]*?)\\]`, 'i'));
13743
14042
  if (!match)
@@ -13790,8 +14089,7 @@ export async function resolveSemanticTableMapping(executor, connection, semantic
13790
14089
  const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name
13791
14090
  FROM information_schema.tables
13792
14091
  WHERE UPPER(table_schema) NOT IN ('INFORMATION_SCHEMA', 'PG_CATALOG')
13793
- ORDER BY table_schema, table_name
13794
- LIMIT 2000`, [], {}, connection);
14092
+ ORDER BY table_schema, table_name`, [], {}, connection);
13795
14093
  return buildSemanticTableMapping(semanticLayer, tablesResult.rows);
13796
14094
  }
13797
14095
  catch {
@@ -13833,41 +14131,17 @@ function isDbtSemanticRuntime(projectConfig, detectedProvider, semanticLayer) {
13833
14131
  return true;
13834
14132
  return Boolean(semanticLayer?.listMetrics().some((metric) => metric.source?.provider === 'dbt'));
13835
14133
  }
13836
- function composeRuntimeSemanticQuery(request, semanticLayer, context) {
13837
- const useMetricFlow = request.engine === 'metricflow' || (request.engine !== 'native' &&
13838
- isDbtSemanticRuntime(context.projectConfig, context.detectedProvider, semanticLayer));
13839
- if (useMetricFlow) {
13840
- const effectiveSemanticConfig = resolveProjectSemanticConfig(context.projectConfig, context.projectRoot);
13841
- const dbtProjectPath = effectiveSemanticConfig?.provider === 'dbt'
13842
- ? effectiveSemanticConfig.projectPath
13843
- : context.projectConfig.dbt?.projectDir;
13844
- try {
13845
- const compiled = compileMetricFlowQuery({
13846
- projectRoot: context.projectRoot,
13847
- dbtProjectPath,
13848
- metrics: request.metrics,
13849
- dimensions: request.dimensions,
13850
- filters: request.filters,
13851
- timeDimension: request.timeDimension,
13852
- orderBy: request.orderBy,
13853
- limit: request.limit,
13854
- savedQuery: request.savedQuery,
13855
- });
13856
- return {
13857
- sql: compiled.sql,
13858
- joins: [],
13859
- tables: [],
13860
- engine: 'metricflow',
13861
- };
13862
- }
13863
- catch (error) {
13864
- // An explicit MetricFlow request must retain its strict engine contract.
13865
- // For the default dbt path, a missing local CLI may fall back only if the
13866
- // native composer can safely materialize the requested simple metrics.
13867
- if (request.engine === 'metricflow' || !(error instanceof MetricFlowUnavailableError))
13868
- throw error;
13869
- }
13870
- }
14134
+ async function composeRuntimeSemanticQuery(request, semanticLayer, context) {
14135
+ return compileSemanticRuntimeQuery(request, {
14136
+ projectRoot: context.projectRoot,
14137
+ projectConfig: context.projectConfig,
14138
+ detectedProvider: context.detectedProvider,
14139
+ semanticLayer,
14140
+ driver: context.driver,
14141
+ tableMapping: context.tableMapping,
14142
+ });
14143
+ }
14144
+ function composeRuntimeSemanticQueryNative(request, semanticLayer, context) {
13871
14145
  const composed = semanticLayer.composeQuery({
13872
14146
  metrics: request.metrics,
13873
14147
  dimensions: request.dimensions,
@@ -13936,7 +14210,7 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
13936
14210
  let composed;
13937
14211
  try {
13938
14212
  composed = options?.projectRoot && options.projectConfig
13939
- ? composeRuntimeSemanticQuery({
14213
+ ? composeRuntimeSemanticQueryNative({
13940
14214
  metrics,
13941
14215
  dimensions: config.dimensions,
13942
14216
  filters: mergeSemanticRuntimeFilters(config.filters, runtimeBindings.filters),
@@ -13945,9 +14219,6 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
13945
14219
  : undefined,
13946
14220
  limit: runtimeBindings.limit ?? config.limit,
13947
14221
  }, semanticLayer, {
13948
- projectRoot: options.projectRoot,
13949
- projectConfig: options.projectConfig,
13950
- detectedProvider: options.detectedProvider ?? null,
13951
14222
  driver: options.driver,
13952
14223
  tableMapping: options.tableMapping,
13953
14224
  })
@@ -13973,10 +14244,14 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
13973
14244
  }
13974
14245
  if (!composed) {
13975
14246
  const provider = options?.projectConfig && isDbtSemanticRuntime(options.projectConfig, options.detectedProvider, semanticLayer) ? 'dbt' : (options?.detectedProvider ?? 'dql');
13976
- const metricFlowReady = provider === 'dbt' && hasMetricFlowCli();
13977
14247
  const reasons = metrics.map((metricName) => {
13978
- const capability = semanticMetricExecutionCapability(metricName, semanticLayer, provider, metricFlowReady, options?.driver);
13979
- return capability.status === 'ready' ? null : `${metricName}: ${capability.reason}`;
14248
+ if (semanticLayer.canComposeMetric(metricName))
14249
+ return null;
14250
+ const metric = semanticLayer.getMetric(metricName);
14251
+ const kind = metric?.metricType || metric?.aggregation || metric?.type || 'metric';
14252
+ return provider === 'dbt'
14253
+ ? `${metricName}: ${kind} metric requires a configured dbt Cloud or local MetricFlow runtime.`
14254
+ : `${metricName}: the metric does not have enough composable measure and relation metadata.`;
13980
14255
  }).filter((reason) => Boolean(reason));
13981
14256
  diagnostics.push({
13982
14257
  severity: 'error',
@@ -13993,6 +14268,61 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
13993
14268
  semanticRefs,
13994
14269
  };
13995
14270
  }
14271
+ /**
14272
+ * Runtime-aware semantic block compiler. Static validation remains synchronous
14273
+ * and native-only, while execution surfaces can use the bundled dbt Cloud or
14274
+ * local MetricFlow adapters without changing the persisted semantic identities.
14275
+ */
14276
+ async function composeSemanticBlockSqlForRuntime(source, semanticLayer, options) {
14277
+ const native = composeSemanticBlockSql(source, semanticLayer, options);
14278
+ if (native.sql)
14279
+ return native;
14280
+ const config = parseSemanticBlockConfig(source);
14281
+ if (config.blockType !== 'semantic')
14282
+ return native;
14283
+ const metrics = config.metrics.length > 0 ? config.metrics : config.metric ? [config.metric] : [];
14284
+ if (metrics.length === 0 || native.diagnostics.some((diagnostic) => diagnostic.code === 'semantic_ref'))
14285
+ return native;
14286
+ const runtimeBindings = semanticRuntimeParameterBindings(source, options.parameters ?? {});
14287
+ try {
14288
+ const compiled = await composeRuntimeSemanticQuery({
14289
+ metrics,
14290
+ dimensions: config.dimensions,
14291
+ filters: mergeSemanticRuntimeFilters(config.filters, runtimeBindings.filters),
14292
+ timeDimension: config.timeDimension && config.granularity
14293
+ ? { name: config.timeDimension, granularity: config.granularity }
14294
+ : undefined,
14295
+ limit: runtimeBindings.limit ?? config.limit,
14296
+ }, semanticLayer, {
14297
+ projectRoot: options.projectRoot,
14298
+ projectConfig: options.projectConfig,
14299
+ detectedProvider: options.detectedProvider ?? null,
14300
+ driver: options.driver,
14301
+ tableMapping: options.tableMapping,
14302
+ });
14303
+ if (!compiled)
14304
+ return native;
14305
+ return {
14306
+ sql: compiled.sql,
14307
+ diagnostics: native.diagnostics.filter((diagnostic) => diagnostic.code !== 'semantic_compose_failed'),
14308
+ semanticRefs: native.semanticRefs,
14309
+ };
14310
+ }
14311
+ catch (error) {
14312
+ return {
14313
+ sql: null,
14314
+ diagnostics: [
14315
+ ...native.diagnostics.filter((diagnostic) => diagnostic.code !== 'semantic_compose_failed'),
14316
+ {
14317
+ severity: 'error',
14318
+ code: isSemanticRuntimeError(error) ? 'semantic_runtime_required' : 'semantic_compose_failed',
14319
+ message: error instanceof Error ? error.message : String(error),
14320
+ },
14321
+ ],
14322
+ semanticRefs: native.semanticRefs,
14323
+ };
14324
+ }
14325
+ }
13996
14326
  function semanticRuntimeParameterBindings(source, values) {
13997
14327
  const program = new Parser(source, '<semantic-parameters>').parse();
13998
14328
  const block = program.statements.find((statement) => statement.kind === NodeKind.BlockDecl);