@duckcodeailabs/dql-cli 1.10.0 → 1.10.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.
@@ -10,7 +10,7 @@ import { Readable } from "node:stream";
10
10
  import { buildMixedSourceWarehouseFallbackSql, findMentionedNotebookDataset, planMixedSourceNotebookSql, planMixedSourceSql, } from "./mixed-source-sql.js";
11
11
  import { resolveNpmInvocation } from './npm-runtime.js';
12
12
  import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, hasStandaloneSemanticRef, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
13
- import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, relationshipValidationProofFingerprint, discoverDbtDomains, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, } from '@duckcodeailabs/dql-core';
13
+ import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, normalizeAnalyticalFailureV1, relationshipValidationProofFingerprint, discoverDbtDomains, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, semanticDimensionReference, } from '@duckcodeailabs/dql-core';
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';
@@ -19,7 +19,7 @@ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from '
19
19
  import { resolveRetrievalHealthStatus } from './retrieval-health.js';
20
20
  import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
21
21
  import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
22
- import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, 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, loadAgentSemanticLayer, } from '@duckcodeailabs/dql-agent';
22
+ import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, 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, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, } from '@duckcodeailabs/dql-agent';
23
23
  import { gatherProposeEnrichment } from './propose-enrich.js';
24
24
  import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
25
25
  import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
@@ -33,7 +33,7 @@ import { buildSemanticObjectDetail, buildSemanticTree, computeSyncDiff, loadSema
33
33
  import { clearBlockStudioImportSessions, candidateToDqlSource, createBlockStudioImportSession, deleteBlockStudioImportSession, listBlockStudioImportSessions, loadBlockStudioImportSession, readBlockStudioImportCandidate, parameterizeSqlForDqlImport, updateBlockStudioImportCandidate, writeBlockStudioImportSession, writeBlockStudioImportCandidate, } from './block-studio-import.js';
34
34
  import { MetricFlowUnavailableError, hasDbtSemanticManifest, } from "./metricflow.js";
35
35
  import { ManagedMetricFlowInstaller, isMetricFlowWarehouseAdapter, metricFlowAdapterForDriver, } from './metricflow-installer.js';
36
- import { compileSemanticRuntimeQuery, describeRuntimeCompatibility, explainMissingSemanticRuntime, getSemanticRuntimeStatus, isSemanticRuntimeError, semanticMetricExecutionCapability as runtimeMetricExecutionCapability, SemanticRuntimeRequiredError, testSemanticRuntimeDraft, } from './semantic-runtime.js';
36
+ import { compileSemanticRuntimeQuery, describeRuntimeCompatibility, explainMissingSemanticRuntime, getSemanticRuntimeStatus, semanticRuntimeErrorCode, semanticMetricExecutionCapability as runtimeMetricExecutionCapability, SemanticRuntimeRequiredError, testSemanticRuntimeDraft, } from './semantic-runtime.js';
37
37
  import { getSemanticRuntimeSettings, saveTestedSemanticRuntimeSettings, saveSemanticRuntimePreference, } from './semantic-runtime-settings.js';
38
38
  import { NotebookDatasetWorkspace, } from "./notebook-datasets.js";
39
39
  import { prepareBlockInvocation } from './block-invocation.js';
@@ -279,6 +279,53 @@ export function agentAnswerHasExecutionFailure(governedAnswer) {
279
279
  return typeof governedAnswer.executionError === 'string'
280
280
  && governedAnswer.executionError.trim().length > 0;
281
281
  }
282
+ /** Rebuild the immutable failed-run input from the artifact retained by API-007. */
283
+ export function analyticalFailedRunFromAgentRun(run) {
284
+ for (const artifact of run.artifacts) {
285
+ const payload = agentRunRecord(artifact.payload);
286
+ if (!payload)
287
+ continue;
288
+ const failure = normalizeAnalyticalFailureV1(payload.analyticalFailure);
289
+ const plan = agentRunRecord(payload.resolvedAnalyticalPlan);
290
+ const planFingerprint = agentRunString(plan?.fingerprint) ?? failure?.planFingerprint;
291
+ if (!failure || !planFingerprint)
292
+ continue;
293
+ const dqlArtifact = normalizeDqlArtifactReference(payload.dqlArtifact);
294
+ const dqlSource = dqlArtifact?.source;
295
+ const compiledSql = agentRunString(payload.sql)
296
+ ?? agentRunString(payload.proposedSql)
297
+ ?? dqlArtifact?.compiledSql;
298
+ const graph = agentRunRecord(payload.analyticalExecutionGraph);
299
+ const rawRoute = agentRunString(graph?.route) ?? agentRunString(plan?.recommendedRoute);
300
+ const route = rawRoute === 'certified'
301
+ ? 'certified'
302
+ : rawRoute === 'semantic'
303
+ ? 'semantic'
304
+ : rawRoute === 'governed_sql'
305
+ ? 'governed_sql'
306
+ : 'generated_sql';
307
+ const rawTrust = dqlArtifact?.trustState ?? agentRunString(payload.reviewStatus) ?? artifact.trustState;
308
+ const trustState = rawTrust === 'certified'
309
+ ? 'certified'
310
+ : rawTrust === 'governed'
311
+ ? 'governed'
312
+ : 'review_required';
313
+ return {
314
+ version: 1,
315
+ runId: failure.runId,
316
+ snapshotId: failure.snapshotId,
317
+ route,
318
+ trustState,
319
+ planFingerprint,
320
+ ...(dqlSource ? { dqlSource } : {}),
321
+ ...(failure.dqlFingerprint ? { dqlFingerprint: failure.dqlFingerprint } : {}),
322
+ ...(compiledSql ? { compiledSql } : {}),
323
+ ...(failure.sqlFingerprint ? { sqlFingerprint: failure.sqlFingerprint } : {}),
324
+ failure,
325
+ };
326
+ }
327
+ return undefined;
328
+ }
282
329
  function businessNarrativeGaps(warnings) {
283
330
  const businessRelevant = (warnings ?? []).filter((warning) => !(/missing requested output column/i.test(warning)
284
331
  || /\bquery plan\b/i.test(warning)
@@ -1220,6 +1267,57 @@ export async function startLocalServer(opts) {
1220
1267
  },
1221
1268
  ...(semanticDriver ? { semanticDriver } : {}),
1222
1269
  ...(semanticTableMapping ? { semanticTableMapping } : {}),
1270
+ ...(semanticLayer && semanticConnection ? {
1271
+ resolveAnalyticalFreshness: async (freshnessRequest) => {
1272
+ const binding = freshnessRequest.authorizedAdapterRequest;
1273
+ if (!binding || binding.route !== 'semantic') {
1274
+ throw Object.assign(new Error('The analytical freshness request is not bound to an authorized semantic route.'), {
1275
+ code: 'FRESHNESS_BINDING_REQUIRED',
1276
+ });
1277
+ }
1278
+ if (freshnessRequest.snapshotId !== routeDecision?.resolvedAnalyticalPlan?.snapshotId) {
1279
+ throw Object.assign(new Error('The analytical freshness request does not match the routed metadata snapshot.'), {
1280
+ code: 'SNAPSHOT_DRIFT',
1281
+ });
1282
+ }
1283
+ projectSnapshot();
1284
+ projectSnapshots.assertCurrent(runProjectSnapshot.snapshotId);
1285
+ const compiled = await compileSemanticRuntimeQuery({
1286
+ metrics: [binding.metric],
1287
+ dimensions: [],
1288
+ timeDimension: {
1289
+ name: binding.timeDimension,
1290
+ granularity: binding.granularity,
1291
+ },
1292
+ orderBy: [{ name: binding.outputField, direction: 'desc' }],
1293
+ limit: 1,
1294
+ }, {
1295
+ projectRoot,
1296
+ projectConfig,
1297
+ detectedProvider: semanticDetectedProvider,
1298
+ semanticLayer: semanticLayer,
1299
+ driver: semanticDriver,
1300
+ tableMapping: semanticTableMapping,
1301
+ });
1302
+ if (!compiled) {
1303
+ throw Object.assign(new Error('The authorized semantic freshness query could not be compiled.'), {
1304
+ code: 'COMPILATION_FAILED',
1305
+ });
1306
+ }
1307
+ const prepared = prepareLocalExecution(compiled.sql, semanticConnection, projectRoot, projectConfig);
1308
+ const result = await executor.executeQuery(prepared.sql, [], runtimeVariables({}), prepared.connection);
1309
+ projectSnapshot();
1310
+ projectSnapshots.assertCurrent(runProjectSnapshot.snapshotId);
1311
+ return {
1312
+ version: 1,
1313
+ snapshotId: freshnessRequest.snapshotId,
1314
+ metricId: freshnessRequest.metricId,
1315
+ timeDimensionId: freshnessRequest.timeDimensionId,
1316
+ observedThrough: analyticalFreshnessObservedThrough(result, freshnessRequest),
1317
+ observedAt: new Date().toISOString(),
1318
+ };
1319
+ },
1320
+ } : {}),
1223
1321
  ...(semanticLayer ? {
1224
1322
  semanticQueryCompiler: async (selection) => {
1225
1323
  const compiled = await compileSemanticRuntimeQuery({
@@ -1262,6 +1360,7 @@ export async function startLocalServer(opts) {
1262
1360
  ...(routeDecision?.resolvedAnalyticalPlan
1263
1361
  ? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
1264
1362
  : {}),
1363
+ analyticalReferenceInstant: new Date().toISOString(),
1265
1364
  executeCertifiedBlock: executeCertifiedBlockForAgent,
1266
1365
  executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact),
1267
1366
  executeDqlArtifact: (artifact) => executeArtifactReferenceForAgent(artifact, request.question),
@@ -1622,6 +1721,8 @@ export async function startLocalServer(opts) {
1622
1721
  && (governedAnswer.refusalCode === 'grounding_gap' || governedAnswer.refusalCode === 'modeling_gap')
1623
1722
  && !isExploratory;
1624
1723
  const isProviderError = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'provider_error';
1724
+ const isAnalyticalFailure = Boolean(governedAnswer.analyticalFailure);
1725
+ const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure;
1625
1726
  // AGT-004: a rejected attribution/export/proof policy is a deliberate
1626
1727
  // governance boundary, not an ambiguous user question and not a repairable
1627
1728
  // retrieval miss. Keep the precise policy detail visible, but never burn two
@@ -1681,14 +1782,14 @@ export async function startLocalServer(opts) {
1681
1782
  synthesizedAnswer = undefined;
1682
1783
  }
1683
1784
  }
1684
- const status = isProviderError || isExecutionFailure
1785
+ const status = isTerminalFailure
1685
1786
  ? 'blocked'
1686
1787
  : needsClarification
1687
1788
  ? 'needs_clarification'
1688
1789
  : isCertified || isSemantic
1689
1790
  ? 'completed'
1690
1791
  : 'needs_review';
1691
- const trustState = isProviderError || isExecutionFailure
1792
+ const trustState = isTerminalFailure
1692
1793
  ? 'blocked'
1693
1794
  : needsClarification
1694
1795
  ? 'not_applicable'
@@ -1697,7 +1798,7 @@ export async function startLocalServer(opts) {
1697
1798
  : isSemantic
1698
1799
  ? 'governed'
1699
1800
  : 'review_required';
1700
- const stopReason = isProviderError || isExecutionFailure
1801
+ const stopReason = isTerminalFailure
1701
1802
  ? 'blocked'
1702
1803
  : needsClarification
1703
1804
  ? 'needs_clarification'
@@ -1706,10 +1807,23 @@ export async function startLocalServer(opts) {
1706
1807
  : isSemantic
1707
1808
  ? 'governed_semantic_answer'
1708
1809
  : 'human_review_required';
1810
+ const terminalFailureActions = governedAnswer.analyticalFailure
1811
+ ? governedAnswer.analyticalFailure.code === 'PERMISSION_DENIED'
1812
+ ? [{ id: 'repair-access', label: 'Change connection or request access', route: 'blocked' }]
1813
+ : governedAnswer.analyticalFailure.code === 'TIMEOUT'
1814
+ ? [{ id: 'retry-same-plan', label: 'Retry the same bounded plan', route: 'generated_answer' }]
1815
+ : governedAnswer.analyticalFailure.code === 'SNAPSHOT_DRIFT'
1816
+ || governedAnswer.analyticalFailure.code === 'COLUMN_NOT_FOUND'
1817
+ || governedAnswer.analyticalFailure.code === 'RELATION_NOT_FOUND'
1818
+ ? [{ id: 'refresh-snapshot', label: 'Refresh the governed snapshot before retrying', route: 'blocked' }]
1819
+ : [{ id: 'review-analytical-failure', label: 'Review the plan, DQL, SQL, and safe repair actions', route: 'blocked' }]
1820
+ : isProviderError
1821
+ ? [{ id: 'retry-after-provider', label: 'Retry after fixing the AI provider', route: 'generated_answer' }]
1822
+ : [{ id: 'retry-after-connection', label: 'Retry after fixing the database connection', route: 'generated_answer' }];
1709
1823
  const nextActions = needsClarification
1710
1824
  ? [{ id: 'clarify', label: 'Clarify question', route: 'generated_answer' }]
1711
- : isExecutionFailure
1712
- ? [{ id: 'retry-after-connection', label: 'Retry after fixing the database connection', route: 'generated_answer' }]
1825
+ : isTerminalFailure
1826
+ ? terminalFailureActions
1713
1827
  : isGroundingGap
1714
1828
  ? [{ id: 'research-gap', label: 'Research missing metadata coverage', route: 'research', artifactKind: 'research_run' }]
1715
1829
  : [
@@ -1726,15 +1840,15 @@ export async function startLocalServer(opts) {
1726
1840
  resolvedRoute,
1727
1841
  answerRefusalCode: governedAnswer.kind === 'no_answer' ? governedAnswer.refusalCode : undefined,
1728
1842
  answerTier: governedAnswer.route?.tier,
1729
- summary: isExecutionFailure
1843
+ summary: isTerminalFailure
1730
1844
  ? 'The governed query could not be executed.'
1731
1845
  : governedAnswer.route?.label ?? (isCertified ? 'Answered from certified DQL context.' : isExploratory ? 'Exploratory DBT-grounded analysis requires review.' : 'Answered with review-required generated analysis.'),
1732
1846
  answer: synthesizedAnswer ?? governedAnswer.answer ?? governedAnswer.text,
1733
1847
  status,
1734
1848
  trustState,
1735
1849
  stopReason,
1736
- artifacts: isExecutionFailure
1737
- ? []
1850
+ artifacts: isTerminalFailure
1851
+ ? [agentRunArtifact('answer', 'Failed governed analytical run', governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, 'blocked')]
1738
1852
  : governedAnswer.kind === 'no_answer'
1739
1853
  // A refusal still keeps the DQL draft the answer loop produced (when any),
1740
1854
  // so the "Review DQL draft" next-action isn't a dead link and the user can
@@ -2415,6 +2529,8 @@ export async function startLocalServer(opts) {
2415
2529
  snapshotId: pack.knowledgeLens.snapshotId,
2416
2530
  sourceFingerprint: pack.freshness.fingerprint ?? undefined,
2417
2531
  knowledgeLens: pack.knowledgeLens,
2532
+ analyticalPolicies: pack.skills.flatMap((skill) => skill.analyticalPolicy ? [skill.analyticalPolicy] : []),
2533
+ contextObjects: pack.objects,
2418
2534
  durationMs: Date.now() - startedAt,
2419
2535
  truncated: pack.retrievalDiagnostics.topRejected.length > 0,
2420
2536
  });
@@ -2566,7 +2682,6 @@ export async function startLocalServer(opts) {
2566
2682
  const resolved = resolveNotebookBlockReferenceCell(cell, projectRoot);
2567
2683
  const activeConnection = requireActiveConnection();
2568
2684
  const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer);
2569
- const plan = buildExecutionPlan(resolved.cell, { semanticLayer, driver: activeConnection.driver, tableMapping });
2570
2685
  const semanticCompose = resolved.cell.type === 'dql'
2571
2686
  && semanticLayer
2572
2687
  && /\btype\s*=\s*"semantic"/i.test(resolved.cell.source)
@@ -2581,6 +2696,12 @@ export async function startLocalServer(opts) {
2581
2696
  if (semanticCompose && !semanticCompose.sql) {
2582
2697
  throw new Error(semanticCompose.diagnostics.map((diagnostic) => diagnostic.message).join(' '));
2583
2698
  }
2699
+ const plan = buildExecutionPlan(resolved.cell, {
2700
+ semanticLayer,
2701
+ driver: activeConnection.driver,
2702
+ tableMapping,
2703
+ semanticSql: semanticCompose?.sql ?? undefined,
2704
+ });
2584
2705
  const executableSql = semanticCompose?.sql ?? plan?.sql;
2585
2706
  if (!executableSql) {
2586
2707
  snapshotCells.push({ cellId, status: 'idle', executionCount: 0, executedAt });
@@ -2646,7 +2767,6 @@ export async function startLocalServer(opts) {
2646
2767
  }
2647
2768
  const activeConnection = requireActiveConnection();
2648
2769
  const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer);
2649
- 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 });
2650
2770
  const semanticCompose = semanticLayer
2651
2771
  ? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
2652
2772
  driver: activeConnection.driver,
@@ -2657,6 +2777,13 @@ export async function startLocalServer(opts) {
2657
2777
  parameters: invocation.values,
2658
2778
  })
2659
2779
  : null;
2780
+ const plan = buildExecutionPlan({ id: `agent-${metadata.name ?? 'dql-artifact'}`, type: 'dql', source, title: metadata.name ?? 'DQL artifact' }, {
2781
+ semanticLayer,
2782
+ driver: activeConnection.driver,
2783
+ tableMapping,
2784
+ parameters: invocation.values,
2785
+ semanticSql: semanticCompose?.sql ?? undefined,
2786
+ });
2660
2787
  if (!plan && !semanticCompose?.sql) {
2661
2788
  const semanticError = semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message;
2662
2789
  throw new Error(semanticError ?? `DQL artifact "${metadata.name ?? 'draft'}" produced no executable SQL.`);
@@ -3724,7 +3851,12 @@ export async function startLocalServer(opts) {
3724
3851
  ?? 'No executable SQL found in block source.';
3725
3852
  throw new Error(message);
3726
3853
  }
3727
- const plan = buildExecutionPlan({ id: 'block-studio', type: 'dql', source, title: 'Block Studio' }, { semanticLayer, driver: activeConnection.driver, tableMapping });
3854
+ const plan = buildExecutionPlan({ id: 'block-studio', type: 'dql', source, title: 'Block Studio' }, {
3855
+ semanticLayer,
3856
+ driver: activeConnection.driver,
3857
+ tableMapping,
3858
+ semanticSql: semanticCompose?.sql ?? undefined,
3859
+ });
3728
3860
  const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan?.sql ?? executableSql, activeConnection, projectRoot, projectConfig);
3729
3861
  const result = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), prepared.connection);
3730
3862
  return {
@@ -3738,7 +3870,24 @@ export async function startLocalServer(opts) {
3738
3870
  const activeConnection = requireActiveConnection(targetConnection);
3739
3871
  const start = Date.now();
3740
3872
  const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer);
3741
- const plan = buildExecutionPlan({ id: 'block-studio-tests', type: 'dql', source, title: 'Block Studio' }, { semanticLayer, driver: activeConnection.driver, tableMapping });
3873
+ // Run tests against the SAME SQL the preview runs: for a semantic block with a
3874
+ // pre-compiled query, that's the query (not a recompiled metric), so the test's
3875
+ // output columns match the block's declared outputs.
3876
+ const semanticCompose = semanticLayer
3877
+ ? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
3878
+ driver: activeConnection.driver,
3879
+ tableMapping,
3880
+ projectRoot,
3881
+ projectConfig,
3882
+ detectedProvider: semanticDetectedProvider,
3883
+ })
3884
+ : null;
3885
+ const plan = buildExecutionPlan({ id: 'block-studio-tests', type: 'dql', source, title: 'Block Studio' }, {
3886
+ semanticLayer,
3887
+ driver: activeConnection.driver,
3888
+ tableMapping,
3889
+ semanticSql: semanticCompose?.sql ?? undefined,
3890
+ });
3742
3891
  const tests = plan?.tests ?? [];
3743
3892
  if (!plan || !plan.sql) {
3744
3893
  return {
@@ -3749,7 +3898,8 @@ export async function startLocalServer(opts) {
3749
3898
  assertions: [{
3750
3899
  name: 'build execution plan',
3751
3900
  passed: false,
3752
- error: 'Could not build an execution plan for this block.',
3901
+ error: semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message
3902
+ ?? 'Could not build an execution plan for this block.',
3753
3903
  }],
3754
3904
  runAt: new Date(),
3755
3905
  };
@@ -3757,18 +3907,6 @@ export async function startLocalServer(opts) {
3757
3907
  if (tests.length === 0) {
3758
3908
  return { passed: 0, failed: 0, skipped: 0, duration: Date.now() - start, assertions: [], runAt: new Date() };
3759
3909
  }
3760
- // Run tests against the SAME SQL the preview runs: for a semantic block with a
3761
- // pre-compiled query, that's the query (not a recompiled metric), so the test's
3762
- // output columns match the block's declared outputs.
3763
- const semanticCompose = semanticLayer
3764
- ? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
3765
- driver: activeConnection.driver,
3766
- tableMapping,
3767
- projectRoot,
3768
- projectConfig,
3769
- detectedProvider: semanticDetectedProvider,
3770
- })
3771
- : null;
3772
3910
  const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan.sql, activeConnection, projectRoot, projectConfig);
3773
3911
  const rawResult = await executor.executeQuery(prepared.sql, plan.sqlParams ?? [], runtimeVariables(plan.variables ?? {}), prepared.connection);
3774
3912
  const rows = Array.isArray(rawResult?.rows) ? rawResult.rows : [];
@@ -4808,6 +4946,50 @@ export async function startLocalServer(opts) {
4808
4946
  res.end(serializeJSON({ runs, total: agentRunStore.list().length, limit }));
4809
4947
  return;
4810
4948
  }
4949
+ if (req.method === 'POST' && /^\/api\/agent-runs\/[^/]+\/analytical-repair$/.test(path)) {
4950
+ const match = path.match(/^\/api\/agent-runs\/([^/]+)\/analytical-repair$/);
4951
+ const id = decodeURIComponent(match?.[1] ?? '');
4952
+ const run = await agentRunStore.get(id);
4953
+ if (!run) {
4954
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
4955
+ res.end(serializeJSON({ error: 'Agent run not found.' }));
4956
+ return;
4957
+ }
4958
+ const source = analyticalFailedRunFromAgentRun(run);
4959
+ if (!source) {
4960
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
4961
+ res.end(serializeJSON({ error: 'This run has no retained analytical failure artifact.' }));
4962
+ return;
4963
+ }
4964
+ const body = await readJSON(req).catch(() => null);
4965
+ const record = agentRunRecord(body);
4966
+ const repair = (agentRunRecord(record?.repair) ?? record);
4967
+ if (!repair) {
4968
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
4969
+ res.end(serializeJSON({ error: 'A v1 analytical repair request is required.' }));
4970
+ return;
4971
+ }
4972
+ // API-007: snapshot refresh is resolved by the runtime, not trusted from
4973
+ // browser input. projectSnapshot() recompiles when governed sources have
4974
+ // changed and gives the immutable identity used by the derivation.
4975
+ const repairWithRuntimeContext = repair.action === 'refresh_snapshot'
4976
+ ? { ...repair, refreshedSnapshotId: projectSnapshot().snapshotId }
4977
+ : repair;
4978
+ const result = deriveAnalyticalRepair(source, repairWithRuntimeContext);
4979
+ if (result.status === 'blocked') {
4980
+ const status = result.code === 'SOURCE_FINGERPRINT_MISMATCH'
4981
+ ? 409
4982
+ : result.code === 'PERMISSION_FAILURE_TERMINAL' || result.code === 'ACTION_NOT_ALLOWED'
4983
+ ? 403
4984
+ : 400;
4985
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
4986
+ res.end(serializeJSON(result));
4987
+ return;
4988
+ }
4989
+ res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
4990
+ res.end(serializeJSON(result));
4991
+ return;
4992
+ }
4811
4993
  if (req.method === 'POST' && /^\/api\/agent-runs\/[^/]+\/cancel$/.test(path)) {
4812
4994
  const match = path.match(/^\/api\/agent-runs\/([^/]+)\/cancel$/);
4813
4995
  const id = decodeURIComponent(match?.[1] ?? '');
@@ -6318,7 +6500,13 @@ export async function startLocalServer(opts) {
6318
6500
  parameters: invocation.values,
6319
6501
  })
6320
6502
  : null;
6321
- const plan = buildExecutionPlan({ id: item.i, type: 'dql', source, title: item.title ?? block.name }, { semanticLayer, driver: targetConnection.driver, tableMapping });
6503
+ const plan = buildExecutionPlan({ id: item.i, type: 'dql', source, title: item.title ?? block.name }, {
6504
+ semanticLayer,
6505
+ driver: targetConnection.driver,
6506
+ tableMapping,
6507
+ parameters: invocation.values,
6508
+ semanticSql: semanticCompose?.sql ?? undefined,
6509
+ });
6322
6510
  if (!plan && !semanticCompose?.sql) {
6323
6511
  tiles.push({
6324
6512
  tileId: item.i,
@@ -9234,7 +9422,7 @@ export async function startLocalServer(opts) {
9234
9422
  owner: m.owner ?? null,
9235
9423
  source: m.source ?? null,
9236
9424
  }));
9237
- const dimensions = semanticLayer.listDimensions().map((d) => ({
9425
+ const dimensions = semanticLayer.listDimensions(undefined, { includeVariants: true }).map((d) => ({
9238
9426
  name: d.name,
9239
9427
  label: d.label,
9240
9428
  description: d.description,
@@ -9245,11 +9433,15 @@ export async function startLocalServer(opts) {
9245
9433
  tags: d.tags ?? [],
9246
9434
  owner: d.owner ?? null,
9247
9435
  cube: d.cube ?? null,
9436
+ reference: semanticDimensionReference(d),
9437
+ qualifiedName: d.qualifiedName ?? null,
9438
+ entityLink: d.entityLink ?? null,
9439
+ canonicalId: `semantic:${d.domain || 'uncategorized'}:dimension:${semanticDimensionReference(d)}`,
9248
9440
  isTimeDimension: d.isTimeDimension ?? false,
9249
9441
  typeParams: d.typeParams ?? null,
9250
9442
  source: d.source ?? null,
9251
9443
  }));
9252
- const timeDimensions = semanticLayer.listTimeDimensions().map((d) => ({
9444
+ const timeDimensions = semanticLayer.listTimeDimensions(undefined, { includeVariants: true }).map((d) => ({
9253
9445
  name: d.name,
9254
9446
  label: d.label,
9255
9447
  description: d.description,
@@ -9258,6 +9450,10 @@ export async function startLocalServer(opts) {
9258
9450
  type: d.type,
9259
9451
  table: d.table,
9260
9452
  cube: d.cube ?? null,
9453
+ reference: semanticDimensionReference(d),
9454
+ qualifiedName: d.qualifiedName ?? null,
9455
+ entityLink: d.entityLink ?? null,
9456
+ canonicalId: `semantic:${d.domain || 'uncategorized'}:dimension:${semanticDimensionReference(d)}`,
9261
9457
  granularities: d.granularities ?? [],
9262
9458
  primaryTime: d.primaryTime ?? false,
9263
9459
  tags: d.tags ?? [],
@@ -9604,10 +9800,15 @@ export async function startLocalServer(opts) {
9604
9800
  sql: d.sql,
9605
9801
  type: d.type,
9606
9802
  table: d.table,
9803
+ cube: d.cube ?? null,
9804
+ reference: semanticDimensionReference(d),
9805
+ qualifiedName: d.qualifiedName ?? null,
9806
+ entityLink: d.entityLink ?? null,
9807
+ canonicalId: `semantic:${d.domain || 'uncategorized'}:dimension:${semanticDimensionReference(d)}`,
9607
9808
  tags: d.tags ?? [],
9608
9809
  owner: d.owner ?? null,
9609
9810
  })),
9610
- timeDimensions: semanticLayer.listTimeDimensions().filter((d) => results.dimensions.some((dim) => dim.name === d.name)).map((d) => ({
9811
+ timeDimensions: semanticLayer.listTimeDimensions(undefined, { includeVariants: true }).filter((d) => results.dimensions.some((dim) => semanticDimensionReference(dim) === semanticDimensionReference(d))).map((d) => ({
9611
9812
  name: d.name,
9612
9813
  label: d.label,
9613
9814
  description: d.description,
@@ -9615,6 +9816,11 @@ export async function startLocalServer(opts) {
9615
9816
  sql: d.sql,
9616
9817
  type: d.type,
9617
9818
  table: d.table,
9819
+ cube: d.cube ?? null,
9820
+ reference: semanticDimensionReference(d),
9821
+ qualifiedName: d.qualifiedName ?? null,
9822
+ entityLink: d.entityLink ?? null,
9823
+ canonicalId: `semantic:${d.domain || 'uncategorized'}:dimension:${semanticDimensionReference(d)}`,
9618
9824
  tags: d.tags ?? [],
9619
9825
  owner: d.owner ?? null,
9620
9826
  })),
@@ -9677,6 +9883,10 @@ export async function startLocalServer(opts) {
9677
9883
  table: d.table,
9678
9884
  tags: d.tags ?? [],
9679
9885
  owner: d.owner ?? null,
9886
+ cube: d.cube ?? null,
9887
+ reference: semanticDimensionReference(d),
9888
+ entityLink: d.entityLink ?? null,
9889
+ canonicalId: `semantic:${d.domain || 'uncategorized'}:dimension:${semanticDimensionReference(d)}`,
9680
9890
  // Additive: qualified name, real grains, and time flag (existing fields
9681
9891
  // above are byte-identical for compatibility).
9682
9892
  qualifiedName: d.qualifiedName ?? null,
@@ -9768,16 +9978,16 @@ export async function startLocalServer(opts) {
9768
9978
  });
9769
9979
  }
9770
9980
  if (semanticLayer && (kind === "all" || kind === "dimension")) {
9771
- for (const dimension of semanticLayer.listDimensions())
9981
+ for (const dimension of semanticLayer.listDimensions(undefined, { includeVariants: true }))
9772
9982
  candidates.push({
9773
9983
  type: "dimension",
9774
- name: dimension.name,
9984
+ name: semanticDimensionReference(dimension),
9775
9985
  label: dimension.label,
9776
9986
  description: dimension.description ?? "",
9777
9987
  domain: dimension.domain,
9778
9988
  tags: dimension.tags ?? [],
9779
- detail: "@dim",
9780
- apply: dimension.name,
9989
+ detail: `${dimension.cube ?? dimension.table} · @dim`,
9990
+ apply: semanticDimensionReference(dimension),
9781
9991
  governance: "semantic",
9782
9992
  });
9783
9993
  }
@@ -10243,13 +10453,13 @@ export async function startLocalServer(opts) {
10243
10453
  res.end(serializeJSON({ error: error.message, code: 'unauthorized' }));
10244
10454
  return;
10245
10455
  }
10246
- const runtimeError = isSemanticRuntimeError(error);
10247
- const status = runtimeError ? 400 : 500;
10456
+ const runtimeCode = semanticRuntimeErrorCode(error);
10457
+ const status = runtimeCode ? 400 : 500;
10248
10458
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
10249
10459
  res.end(serializeJSON({
10250
10460
  error: error instanceof Error ? error.message : String(error),
10251
- code: runtimeError ? 'SEMANTIC_RUNTIME_REQUIRED' : undefined,
10252
- hint: runtimeError
10461
+ code: runtimeCode,
10462
+ hint: runtimeCode === 'SEMANTIC_RUNTIME_REQUIRED'
10253
10463
  ? 'Configure dbt Cloud Semantic Layer in Project & dbt settings, or install a compatible local MetricFlow runtime.'
10254
10464
  : undefined,
10255
10465
  }));
@@ -10314,13 +10524,13 @@ export async function startLocalServer(opts) {
10314
10524
  }));
10315
10525
  }
10316
10526
  catch (error) {
10317
- const runtimeError = isSemanticRuntimeError(error);
10318
- const status = runtimeError ? 400 : 500;
10527
+ const runtimeCode = semanticRuntimeErrorCode(error);
10528
+ const status = runtimeCode ? 400 : 500;
10319
10529
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
10320
10530
  res.end(serializeJSON({
10321
10531
  error: error instanceof Error ? error.message : String(error),
10322
- code: runtimeError ? 'SEMANTIC_RUNTIME_REQUIRED' : undefined,
10323
- hint: runtimeError
10532
+ code: runtimeCode,
10533
+ hint: runtimeCode === 'SEMANTIC_RUNTIME_REQUIRED'
10324
10534
  ? 'Configure dbt Cloud Semantic Layer in Project & dbt settings, or install a compatible local MetricFlow runtime.'
10325
10535
  : undefined,
10326
10536
  }));
@@ -10389,13 +10599,13 @@ export async function startLocalServer(opts) {
10389
10599
  res.end(serializeJSON({ error: 'Block already exists' }));
10390
10600
  return;
10391
10601
  }
10392
- const runtimeError = isSemanticRuntimeError(error);
10393
- const status = runtimeError ? 400 : 500;
10602
+ const runtimeCode = semanticRuntimeErrorCode(error);
10603
+ const status = runtimeCode ? 400 : 500;
10394
10604
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
10395
10605
  res.end(serializeJSON({
10396
10606
  error: error instanceof Error ? error.message : String(error),
10397
- code: runtimeError ? 'SEMANTIC_RUNTIME_REQUIRED' : undefined,
10398
- hint: runtimeError
10607
+ code: runtimeCode,
10608
+ hint: runtimeCode === 'SEMANTIC_RUNTIME_REQUIRED'
10399
10609
  ? 'Configure dbt Cloud Semantic Layer in Project & dbt settings, or install a compatible local MetricFlow runtime.'
10400
10610
  : undefined,
10401
10611
  }));
@@ -10846,12 +11056,6 @@ export async function startLocalServer(opts) {
10846
11056
  const tableMapping = needsSemanticTableMapping(executableCell)
10847
11057
  ? await resolveSemanticTableMapping(executor, cellConnection, semanticLayer)
10848
11058
  : undefined;
10849
- const plan = buildExecutionPlan(executableCell, {
10850
- semanticLayer,
10851
- driver: cellConnection.driver,
10852
- tableMapping,
10853
- parameters: invocation?.values,
10854
- });
10855
11059
  const semanticCompose = executableCell.type === 'dql'
10856
11060
  && semanticLayer
10857
11061
  && /\btype\s*=\s*"semantic"/i.test(executableCell.source)
@@ -10867,6 +11071,13 @@ export async function startLocalServer(opts) {
10867
11071
  if (semanticCompose && !semanticCompose.sql) {
10868
11072
  throw new Error(semanticCompose.diagnostics.map((diagnostic) => diagnostic.message).join(' '));
10869
11073
  }
11074
+ const plan = buildExecutionPlan(executableCell, {
11075
+ semanticLayer,
11076
+ driver: cellConnection.driver,
11077
+ tableMapping,
11078
+ parameters: invocation?.values,
11079
+ semanticSql: semanticCompose?.sql ?? undefined,
11080
+ });
10870
11081
  const executableSql = semanticCompose?.sql ?? plan?.sql;
10871
11082
  if (!executableSql) {
10872
11083
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -11462,6 +11673,48 @@ export function serializeJSON(value) {
11462
11673
  return current;
11463
11674
  });
11464
11675
  }
11676
+ /**
11677
+ * Extract the single time value returned by an authorized semantic freshness
11678
+ * lookup. Adapter aliases may differ by warehouse, so typed date metadata is
11679
+ * the only fallback; arbitrary string columns are never treated as freshness.
11680
+ * Acceptance: AGT-019, SEC-004.
11681
+ */
11682
+ export function analyticalFreshnessObservedThrough(result, request) {
11683
+ const binding = request.authorizedAdapterRequest;
11684
+ if (!binding || binding.route !== 'semantic') {
11685
+ throw Object.assign(new Error('The freshness lookup has no authorized semantic adapter binding.'), {
11686
+ code: 'FRESHNESS_BINDING_REQUIRED',
11687
+ });
11688
+ }
11689
+ const row = result.rows[0];
11690
+ if (!row) {
11691
+ throw Object.assign(new Error('The authorized freshness lookup returned no rows.'), {
11692
+ code: 'FRESHNESS_OBSERVATION_MISSING',
11693
+ });
11694
+ }
11695
+ const normalize = (value) => value.replace(/[^a-zA-Z0-9]+/g, '_').toLowerCase();
11696
+ const target = normalize(binding.outputField);
11697
+ const exact = result.columns.find((column) => normalize(column.name) === target);
11698
+ const typedDates = result.columns.filter((column) => column.type === 'date' || column.type === 'datetime');
11699
+ const column = exact ?? (typedDates.length === 1 ? typedDates[0] : undefined);
11700
+ if (!column) {
11701
+ throw Object.assign(new Error('The authorized freshness lookup did not return one identifiable time field.'), {
11702
+ code: 'FRESHNESS_OBSERVATION_INVALID',
11703
+ });
11704
+ }
11705
+ const value = row[column.name];
11706
+ const instant = value instanceof Date
11707
+ ? value
11708
+ : typeof value === 'string' && value.trim()
11709
+ ? new Date(value)
11710
+ : undefined;
11711
+ if (!instant || Number.isNaN(instant.getTime())) {
11712
+ throw Object.assign(new Error('The authorized freshness value is not a valid instant.'), {
11713
+ code: 'FRESHNESS_OBSERVATION_INVALID',
11714
+ });
11715
+ }
11716
+ return instant.toISOString();
11717
+ }
11465
11718
  /** Serialize a Skill to the shared API contract shape (spec 16). */
11466
11719
  function serializeSkill(skill) {
11467
11720
  return {
@@ -14457,7 +14710,10 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
14457
14710
  : [];
14458
14711
  const semanticRefs = {
14459
14712
  metrics,
14460
- dimensions: config.dimensions,
14713
+ dimensions: Array.from(new Set([
14714
+ ...config.dimensions,
14715
+ ...(config.timeDimension ? [config.timeDimension] : []),
14716
+ ])),
14461
14717
  segments: [],
14462
14718
  };
14463
14719
  const diagnostics = [];
@@ -14479,7 +14735,11 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
14479
14735
  message: `Semantic block selects time_dimension = "${config.timeDimension}" but is missing granularity.`,
14480
14736
  });
14481
14737
  }
14482
- const refValidation = semanticLayer.validateReferences([...metrics, ...config.dimensions]);
14738
+ const refValidation = semanticLayer.validateReferences([
14739
+ ...metrics,
14740
+ ...config.dimensions,
14741
+ ...(config.timeDimension ? [config.timeDimension] : []),
14742
+ ]);
14483
14743
  for (const unknown of refValidation.unknown) {
14484
14744
  diagnostics.push({
14485
14745
  severity: 'error',
@@ -14568,13 +14828,18 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
14568
14828
  */
14569
14829
  async function composeSemanticBlockSqlForRuntime(source, semanticLayer, options) {
14570
14830
  const native = composeSemanticBlockSql(source, semanticLayer, options);
14571
- if (native.sql)
14572
- return native;
14573
14831
  const config = parseSemanticBlockConfig(source);
14574
14832
  if (config.blockType !== 'semantic')
14575
14833
  return native;
14834
+ // An authored/precompiled query is already the governed artifact payload.
14835
+ // Keep that contract intact rather than sending it through a second compiler.
14836
+ if (extractBlockStudioSql(source))
14837
+ return native;
14576
14838
  const metrics = config.metrics.length > 0 ? config.metrics : config.metric ? [config.metric] : [];
14577
- if (metrics.length === 0 || native.diagnostics.some((diagnostic) => diagnostic.code === 'semantic_ref'))
14839
+ if (metrics.length === 0
14840
+ || native.diagnostics.some((diagnostic) => diagnostic.code === 'semantic_ref'
14841
+ || diagnostic.code === 'semantic_metric_missing'
14842
+ || diagnostic.code === 'semantic_granularity_missing'))
14578
14843
  return native;
14579
14844
  const runtimeBindings = semanticRuntimeParameterBindings(source, options.parameters ?? {});
14580
14845
  try {
@@ -14593,10 +14858,18 @@ async function composeSemanticBlockSqlForRuntime(source, semanticLayer, options)
14593
14858
  driver: options.driver,
14594
14859
  tableMapping: options.tableMapping,
14595
14860
  });
14596
- if (!compiled)
14597
- return native;
14861
+ if (!compiled) {
14862
+ return {
14863
+ sql: null,
14864
+ diagnostics: native.diagnostics,
14865
+ semanticRefs: native.semanticRefs,
14866
+ };
14867
+ }
14598
14868
  return {
14599
14869
  sql: compiled.sql,
14870
+ // Native composition may have failed for a derived/ratio metric before
14871
+ // the selected full runtime compiled it successfully. That diagnostic is
14872
+ // no longer true for this run.
14600
14873
  diagnostics: native.diagnostics.filter((diagnostic) => diagnostic.code !== 'semantic_compose_failed'),
14601
14874
  semanticRefs: native.semanticRefs,
14602
14875
  };
@@ -14608,7 +14881,9 @@ async function composeSemanticBlockSqlForRuntime(source, semanticLayer, options)
14608
14881
  ...native.diagnostics.filter((diagnostic) => diagnostic.code !== 'semantic_compose_failed'),
14609
14882
  {
14610
14883
  severity: 'error',
14611
- code: isSemanticRuntimeError(error) ? 'semantic_runtime_required' : 'semantic_compose_failed',
14884
+ code: semanticRuntimeErrorCode(error) === 'SEMANTIC_RUNTIME_REQUIRED'
14885
+ ? 'semantic_runtime_required'
14886
+ : 'semantic_compose_failed',
14612
14887
  message: error instanceof Error ? error.message : String(error),
14613
14888
  },
14614
14889
  ],