@duckcodeailabs/dql-agent 1.14.2 → 1.14.3-rc.1

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 (145) hide show
  1. package/dist/agent-run-engine.d.ts +63 -2
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +864 -29
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-planner.d.ts +6 -0
  6. package/dist/agent-run-planner.d.ts.map +1 -1
  7. package/dist/agent-run-planner.js +2 -0
  8. package/dist/agent-run-planner.js.map +1 -1
  9. package/dist/agent-run-store.d.ts.map +1 -1
  10. package/dist/agent-run-store.js +48 -3
  11. package/dist/agent-run-store.js.map +1 -1
  12. package/dist/agentic/analyst-loop.d.ts +7 -1
  13. package/dist/agentic/analyst-loop.d.ts.map +1 -1
  14. package/dist/agentic/analyst-loop.js +163 -0
  15. package/dist/agentic/analyst-loop.js.map +1 -1
  16. package/dist/agentic/assumptions.d.ts +1 -1
  17. package/dist/agentic/assumptions.d.ts.map +1 -1
  18. package/dist/agentic/assumptions.js.map +1 -1
  19. package/dist/agentic/research-agent.d.ts.map +1 -1
  20. package/dist/agentic/research-agent.js +10 -1
  21. package/dist/agentic/research-agent.js.map +1 -1
  22. package/dist/agentic/sql-authorization.d.ts +8 -0
  23. package/dist/agentic/sql-authorization.d.ts.map +1 -1
  24. package/dist/agentic/sql-authorization.js +3 -0
  25. package/dist/agentic/sql-authorization.js.map +1 -1
  26. package/dist/analytical-compatibility.d.ts.map +1 -1
  27. package/dist/analytical-compatibility.js +8 -1
  28. package/dist/analytical-compatibility.js.map +1 -1
  29. package/dist/analytical-failure-repair.d.ts.map +1 -1
  30. package/dist/analytical-failure-repair.js +6 -2
  31. package/dist/analytical-failure-repair.js.map +1 -1
  32. package/dist/analytical-frame.d.ts +23 -1
  33. package/dist/analytical-frame.d.ts.map +1 -1
  34. package/dist/analytical-frame.js +322 -16
  35. package/dist/analytical-frame.js.map +1 -1
  36. package/dist/analytical-orchestration.d.ts +314 -3
  37. package/dist/analytical-orchestration.d.ts.map +1 -1
  38. package/dist/analytical-orchestration.js +486 -43
  39. package/dist/analytical-orchestration.js.map +1 -1
  40. package/dist/answer-loop.d.ts +64 -8
  41. package/dist/answer-loop.d.ts.map +1 -1
  42. package/dist/answer-loop.js +528 -67
  43. package/dist/answer-loop.js.map +1 -1
  44. package/dist/ask-observability/index.d.ts +11 -0
  45. package/dist/ask-observability/index.d.ts.map +1 -0
  46. package/dist/ask-observability/index.js +7 -0
  47. package/dist/ask-observability/index.js.map +1 -0
  48. package/dist/ask-observability/instrumentation.d.ts +35 -0
  49. package/dist/ask-observability/instrumentation.d.ts.map +1 -0
  50. package/dist/ask-observability/instrumentation.js +472 -0
  51. package/dist/ask-observability/instrumentation.js.map +1 -0
  52. package/dist/ask-observability/observer.d.ts +62 -0
  53. package/dist/ask-observability/observer.d.ts.map +1 -0
  54. package/dist/ask-observability/observer.js +537 -0
  55. package/dist/ask-observability/observer.js.map +1 -0
  56. package/dist/ask-observability/portable.d.ts +98 -0
  57. package/dist/ask-observability/portable.d.ts.map +1 -0
  58. package/dist/ask-observability/portable.js +527 -0
  59. package/dist/ask-observability/portable.js.map +1 -0
  60. package/dist/ask-observability/store.d.ts +130 -0
  61. package/dist/ask-observability/store.d.ts.map +1 -0
  62. package/dist/ask-observability/store.js +1114 -0
  63. package/dist/ask-observability/store.js.map +1 -0
  64. package/dist/ask-observability/types.d.ts +357 -0
  65. package/dist/ask-observability/types.d.ts.map +1 -0
  66. package/dist/ask-observability/types.js +2 -0
  67. package/dist/ask-observability/types.js.map +1 -0
  68. package/dist/ask-observability/utils.d.ts +21 -0
  69. package/dist/ask-observability/utils.d.ts.map +1 -0
  70. package/dist/ask-observability/utils.js +102 -0
  71. package/dist/ask-observability/utils.js.map +1 -0
  72. package/dist/generated-analytical-proposal.d.ts +37 -1
  73. package/dist/generated-analytical-proposal.d.ts.map +1 -1
  74. package/dist/generated-analytical-proposal.js +211 -3
  75. package/dist/generated-analytical-proposal.js.map +1 -1
  76. package/dist/governed-relational-compiler.d.ts +15 -0
  77. package/dist/governed-relational-compiler.d.ts.map +1 -1
  78. package/dist/governed-relational-compiler.js +99 -1
  79. package/dist/governed-relational-compiler.js.map +1 -1
  80. package/dist/index.d.ts +9 -7
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +6 -4
  83. package/dist/index.js.map +1 -1
  84. package/dist/intent-controller.d.ts +10 -0
  85. package/dist/intent-controller.d.ts.map +1 -1
  86. package/dist/intent-controller.js +1 -1
  87. package/dist/intent-controller.js.map +1 -1
  88. package/dist/meaning-resolution.d.ts +78 -6
  89. package/dist/meaning-resolution.d.ts.map +1 -1
  90. package/dist/meaning-resolution.js +331 -38
  91. package/dist/meaning-resolution.js.map +1 -1
  92. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  93. package/dist/metadata/analysis-planner.js +21 -1
  94. package/dist/metadata/analysis-planner.js.map +1 -1
  95. package/dist/metadata/catalog.d.ts.map +1 -1
  96. package/dist/metadata/catalog.js +1 -1
  97. package/dist/metadata/catalog.js.map +1 -1
  98. package/dist/metadata/meaning-evidence.d.ts +9 -2
  99. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  100. package/dist/metadata/meaning-evidence.js +261 -11
  101. package/dist/metadata/meaning-evidence.js.map +1 -1
  102. package/dist/plan-execution-adapter.d.ts +9 -1
  103. package/dist/plan-execution-adapter.d.ts.map +1 -1
  104. package/dist/plan-execution-adapter.js +93 -13
  105. package/dist/plan-execution-adapter.js.map +1 -1
  106. package/dist/provider-egress.d.ts +24 -10
  107. package/dist/provider-egress.d.ts.map +1 -1
  108. package/dist/provider-egress.js +22 -17
  109. package/dist/provider-egress.js.map +1 -1
  110. package/dist/providers/claude.d.ts.map +1 -1
  111. package/dist/providers/claude.js +10 -9
  112. package/dist/providers/claude.js.map +1 -1
  113. package/dist/providers/dispatch.d.ts +27 -1
  114. package/dist/providers/dispatch.d.ts.map +1 -1
  115. package/dist/providers/dispatch.js +63 -1
  116. package/dist/providers/dispatch.js.map +1 -1
  117. package/dist/providers/gemini.js +10 -10
  118. package/dist/providers/gemini.js.map +1 -1
  119. package/dist/providers/index.d.ts +2 -2
  120. package/dist/providers/index.d.ts.map +1 -1
  121. package/dist/providers/index.js +1 -1
  122. package/dist/providers/index.js.map +1 -1
  123. package/dist/providers/ollama.js +15 -15
  124. package/dist/providers/ollama.js.map +1 -1
  125. package/dist/providers/openai.js +22 -22
  126. package/dist/providers/openai.js.map +1 -1
  127. package/dist/providers/types.d.ts +52 -0
  128. package/dist/providers/types.d.ts.map +1 -1
  129. package/dist/providers/types.js.map +1 -1
  130. package/dist/relationship-proof.d.ts +81 -0
  131. package/dist/relationship-proof.d.ts.map +1 -1
  132. package/dist/relationship-proof.js +228 -4
  133. package/dist/relationship-proof.js.map +1 -1
  134. package/dist/resolved-analytical-plan.d.ts +18 -4
  135. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  136. package/dist/resolved-analytical-plan.js +764 -47
  137. package/dist/resolved-analytical-plan.js.map +1 -1
  138. package/dist/router.d.ts +10 -1
  139. package/dist/router.d.ts.map +1 -1
  140. package/dist/router.js +1785 -213
  141. package/dist/router.js.map +1 -1
  142. package/dist/semantic-bridge/compose.d.ts.map +1 -1
  143. package/dist/semantic-bridge/compose.js +23 -4
  144. package/dist/semantic-bridge/compose.js.map +1 -1
  145. package/package.json +4 -4
@@ -35,7 +35,7 @@ import { analyticalErrorDetail, } from './analytical-error.js';
35
35
  import { fanoutWarningsForSql } from './metadata/grain-ledger.js';
36
36
  import { buildAggregationSafetyProof, buildSemanticCompilationAggregationSafetyProof, } from './aggregation-safety-proof.js';
37
37
  import { buildSemanticProofAuthorityV1 } from './semantic-proof-authority.js';
38
- import { GENERATED_ANALYTICAL_TUPLE_DRIFT_MESSAGE, validateGeneratedAnalyticalProposal, } from './generated-analytical-proposal.js';
38
+ import { GENERATED_ANALYTICAL_TUPLE_DRIFT_MESSAGE, frozenRequiredOutputBindingProofsForPlan, validateFrozenRequiredOutputProjection, validateGeneratedAnalyticalProposal, } from './generated-analytical-proposal.js';
39
39
  import { evaluateDbtFirstGeneratedSql } from './metadata/dbt-first-safety.js';
40
40
  import { planAnalyticalPath, humanizeAnalyticalEntityId, analyticalPolicyUserFacingReason, } from './metadata/analytical-policy.js';
41
41
  import { planCertifiedAdaptation } from './metadata/block-adapt.js';
@@ -1161,9 +1161,17 @@ export async function answer(input) {
1161
1161
  const analyticalExecutionGraph = analyticalGraphBuild?.status === 'ready'
1162
1162
  ? analyticalGraphBuild.graph
1163
1163
  : undefined;
1164
+ // A v2 plan can also be synthesized for legacy/direct AnswerLoop callers so
1165
+ // they can opt into the deterministic compiler. That compatibility bridge is
1166
+ // not a router freeze: it must not turn a missing optional graph/adapter into
1167
+ // a terminal answer before the established, safe legacy semantic execution
1168
+ // path is considered. Router-selected semantic plans carry the explicit
1169
+ // `selectedCascadeTier` signal and remain immutable/terminal below.
1170
+ const routerFrozenSemanticPlan = Boolean(frozenSemanticRouteForInput(normalizedInput));
1164
1171
  const analyticalExecutionGraphFailure = normalizedInput.resolvedAnalyticalPlan?.schemaVersion === 2
1165
1172
  && !multiMetricPlan
1166
1173
  && !generatedProposalPlan
1174
+ && routerFrozenSemanticPlan
1167
1175
  ? analyticalGraphBuild?.status === 'blocked'
1168
1176
  ? {
1169
1177
  code: analyticalGraphBuild.code,
@@ -1248,7 +1256,23 @@ export async function answer(input) {
1248
1256
  }
1249
1257
  : compiledInput;
1250
1258
  const loopResult = await runAnswerLoop(executionInput);
1251
- const result = applyHollowAnswerGate(validateCertifiedAnalyticalGraphAnswer(loopResult, executionInput.analyticalExecutionGraph, executionInput.resolvedAnalyticalPlan?.analyticalFrame));
1259
+ let result = applyHollowAnswerGate(validateCertifiedAnalyticalGraphAnswer(loopResult, executionInput.analyticalExecutionGraph, executionInput.resolvedAnalyticalPlan?.analyticalFrame));
1260
+ const frozenSemanticRoute = frozenSemanticRouteForInput(executionInput);
1261
+ // A final defensive boundary keeps future return sites from accidentally
1262
+ // restoring the old semantic -> generated fallthrough. Do not relabel an
1263
+ // actual generated result as semantic: replace it with an inspectable,
1264
+ // same-tier terminal failure instead.
1265
+ const returnedNonSemanticRoute = frozenSemanticRoute && ((result.route !== undefined && result.route.tier !== 'semantic_metric')
1266
+ || (result.route === undefined && result.kind !== 'no_answer' && result.sourceTier !== 'semantic_layer'));
1267
+ if (returnedNonSemanticRoute && frozenSemanticRoute) {
1268
+ result = frozenSemanticPlanFailureAnswer({
1269
+ answerInput: executionInput,
1270
+ considered: result.considered ?? [],
1271
+ providerName: executionInput.provider.name,
1272
+ code: 'EXECUTION_GRAPH_MISMATCH',
1273
+ reason: `The pinned semantic plan ${frozenSemanticRoute.ref ?? 'selected by the router'} returned a non-semantic answer route. DQL did not substitute generated SQL.`,
1274
+ });
1275
+ }
1252
1276
  // Attach the canonical trust label once, at the single exit point, so every
1253
1277
  // return site inside runAnswerLoop stays untouched and backward compatible.
1254
1278
  // Freshness-aware trust: for a certified answer, fold the source block's data
@@ -1256,7 +1280,7 @@ export async function answer(input) {
1256
1280
  // stale data" / "Certified · upstream failed". Non-certified or fresh answers
1257
1281
  // are unaffected.
1258
1282
  const { _semanticMetricMatch, ...publicResult } = result;
1259
- const chosenRoute = result.route ?? deriveAiRoute(result, _semanticMetricMatch);
1283
+ const chosenRoute = frozenSemanticRoute ?? result.route ?? deriveAiRoute(result, _semanticMetricMatch);
1260
1284
  // P0 — record the high-level action this turn warranted, so callers can route
1261
1285
  // (compose_app → app build, investigate → research) and the UI can show the
1262
1286
  // agent's reasoning. Computed once at the single exit from the finished answer.
@@ -1390,6 +1414,69 @@ function deriveAiRoute(result, metricMatch) {
1390
1414
  ? { tier: 'generated_sql', label: 'Prepared review-required DQL artifact with SQL preview.' }
1391
1415
  : { tier: 'generated_sql', label: 'Prepared review-required SQL preview.' };
1392
1416
  }
1417
+ /**
1418
+ * The router owns the cascade decision. Once it freezes a semantic plan, the
1419
+ * answer loop may either execute that exact plan or return a semantic-tier
1420
+ * terminal failure. It must never let an unavailable semantic adapter fall
1421
+ * through to the legacy generated-SQL lane merely because the latter can still
1422
+ * construct a preview.
1423
+ */
1424
+ function frozenSemanticRouteForInput(input) {
1425
+ const plan = input.resolvedAnalyticalPlan;
1426
+ // `freezeLegacySemanticSelection()` upgrades older direct AnswerLoop callers
1427
+ // to a typed plan so they can use the deterministic semantic compiler. That
1428
+ // migration is not the router's immutable cascade freeze. The server-owned
1429
+ // cascade explicitly carries `selectedCascadeTier: 'semantic'`; only that
1430
+ // signal activates the no-downgrade boundary below. Otherwise direct legacy
1431
+ // callers retain their established deterministic semantic behavior.
1432
+ if (input.selectedCascadeTier !== 'semantic'
1433
+ || plan?.mode !== 'authoritative'
1434
+ || plan.capability !== 'semantic_execution')
1435
+ return undefined;
1436
+ const executionId = plan.executionId ?? plan.selectedConceptIds[0];
1437
+ return {
1438
+ tier: 'semantic_metric',
1439
+ label: executionId
1440
+ ? `Frozen semantic plan ${executionId}`
1441
+ : 'Frozen semantic plan',
1442
+ ...(executionId ? { ref: executionId } : {}),
1443
+ };
1444
+ }
1445
+ function frozenSemanticPlanFailureAnswer(input) {
1446
+ const { answerInput, considered, providerName, code, reason } = input;
1447
+ const route = frozenSemanticRouteForInput(answerInput);
1448
+ // Callers use this only for an authoritative semantic plan. Keep the
1449
+ // fallback defensive so an internal misuse still produces a safe no-answer.
1450
+ const analyticalFailure = analyticalFailureForInput(answerInput, {
1451
+ error: { code, message: reason },
1452
+ phase: 'compilation',
1453
+ failedBindings: answerInput.resolvedAnalyticalPlan?.executionId
1454
+ ? [{
1455
+ qualifiedId: answerInput.resolvedAnalyticalPlan.executionId,
1456
+ role: 'metric',
1457
+ reasonCode: code,
1458
+ }]
1459
+ : undefined,
1460
+ });
1461
+ return {
1462
+ kind: 'no_answer',
1463
+ sourceTier: 'no_answer',
1464
+ certification: 'analyst_review_required',
1465
+ reviewStatus: 'none',
1466
+ confidence: 0,
1467
+ text: analyticalFailure.message,
1468
+ answer: analyticalFailure.message,
1469
+ executionError: analyticalFailure.message,
1470
+ refusalCode: 'grounding_gap',
1471
+ refusalDetails: { code, message: analyticalFailure.message },
1472
+ analyticalFailure,
1473
+ ...(route ? { route } : {}),
1474
+ citations: contextPackCitations(answerInput.contextPack, 8),
1475
+ considered,
1476
+ contextPack: answerInput.contextPack,
1477
+ providerUsed: providerName,
1478
+ };
1479
+ }
1393
1480
  /** "customers_customer_name" → "customers" for a question the user has to read. */
1394
1481
  function humanizeDeicticDimension(dimension) {
1395
1482
  const leaf = dimension.split(/[.:]/).pop() ?? dimension;
@@ -1419,6 +1506,7 @@ async function runAnswerLoop(input) {
1419
1506
  // or provider is dispatched. Full retrieval stays on `input.contextPack`
1420
1507
  // for receipts and diagnostics only.
1421
1508
  const forcedExploratoryTier = input.selectedCascadeTier === 'exploratory_sql';
1509
+ const frozenSemanticRoute = frozenSemanticRouteForInput(input);
1422
1510
  const exploratoryClosureContextPack = forcedExploratoryTier
1423
1511
  ? scopeContextPackToExploratoryCandidateClosure(scopedContextPack, input.exploratoryCandidateIds)
1424
1512
  : undefined;
@@ -1510,6 +1598,31 @@ async function runAnswerLoop(input) {
1510
1598
  const authoritativePlanBinding = !forcedExploratoryTier && input.resolvedAnalyticalPlan?.mode === 'authoritative'
1511
1599
  ? input.resolvedPlanExecutionBinding
1512
1600
  : undefined;
1601
+ // `freezeLegacySemanticSelection()` attaches an authoritative-shaped v2
1602
+ // plan for direct AnswerLoop compatibility. It is not the router-owned
1603
+ // semantic cascade decision. Only an explicit selected semantic tier may
1604
+ // make an adapter/graph miss terminal; direct callers retain the established
1605
+ // deterministic metric path, which still applies the ordinary SQL safety
1606
+ // validation before it can execute.
1607
+ const legacyDirectSemanticPlan = input.selectedCascadeTier === undefined
1608
+ && input.resolvedAnalyticalPlan?.mode === 'authoritative'
1609
+ && input.resolvedAnalyticalPlan.recommendedRoute === 'semantic'
1610
+ && !frozenSemanticRoute;
1611
+ // A semantic freeze is an immutable execution authority, not a hint for the
1612
+ // legacy loop. In particular, projects that have indexed semantic metadata
1613
+ // but have not configured the corresponding local semantic adapter used to
1614
+ // continue into generated SQL. Stop here with the selected plan's typed
1615
+ // diagnostic instead; no provider, tool, or SQL work is allowed to replace
1616
+ // it after freeze.
1617
+ if (!forcedExploratoryTier && frozenSemanticRoute && !input.semanticLayer) {
1618
+ return frozenSemanticPlanFailureAnswer({
1619
+ answerInput: input,
1620
+ considered,
1621
+ providerName: provider.name,
1622
+ code: 'SEMANTIC_LAYER_REQUIRED',
1623
+ reason: `The pinned semantic adapter for ${frozenSemanticRoute.ref ?? 'the selected semantic plan'} is unavailable.`,
1624
+ });
1625
+ }
1513
1626
  if (!forcedExploratoryTier && input.analyticalPeriodResolutionFailure) {
1514
1627
  const failure = input.analyticalPeriodResolutionFailure;
1515
1628
  const structuredFailureCode = failure.error && typeof failure.error === 'object'
@@ -1601,7 +1714,11 @@ async function runAnswerLoop(input) {
1601
1714
  input,
1602
1715
  binding: input.semanticGraphExecutionBinding,
1603
1716
  graph: input.analyticalExecutionGraph,
1604
- capability: normalizeMetricCapabilityContract(input.semanticGraphExecutionBinding.metricNode.payload?.analyticalCapability),
1717
+ // The execution registry identifies the adapter metric, but its compact
1718
+ // node payload may omit the relationship-path metadata that was frozen
1719
+ // on the RAP. Re-deriving aggregation proof from that projection makes
1720
+ // an already accepted native MetricFlow grouping fail post-freeze.
1721
+ capability: input.semanticGraphExecutionBinding.capability,
1605
1722
  considered,
1606
1723
  providerName: provider.name,
1607
1724
  });
@@ -1716,7 +1833,7 @@ async function runAnswerLoop(input) {
1716
1833
  providerUsed: provider.name,
1717
1834
  };
1718
1835
  }
1719
- if (authoritativePlanBinding?.status === 'blocked') {
1836
+ if (authoritativePlanBinding?.status === 'blocked' && !legacyDirectSemanticPlan) {
1720
1837
  // An ambiguous singular reference is answerable the moment the user says
1721
1838
  // which one they meant, so offer the candidates rather than dead-ending.
1722
1839
  // The old refusal shipped the binder's internal sentence as the answer
@@ -1792,7 +1909,7 @@ async function runAnswerLoop(input) {
1792
1909
  ? authoritativePlanBinding
1793
1910
  : undefined;
1794
1911
  const preferredSemanticMetric = authoritativeSemanticBinding?.metricNode
1795
- ?? (input.resolvedAnalyticalPlan?.mode === 'authoritative'
1912
+ ?? (input.resolvedAnalyticalPlan?.mode === 'authoritative' && !legacyDirectSemanticPlan
1796
1913
  ? undefined
1797
1914
  : resolvePreferredSemanticMetric([input.preferredExecutionId, ...(input.preferredEvidenceIds ?? [])], semanticMetricNodes, kg));
1798
1915
  const semanticLayerForExec = input.semanticLayer;
@@ -1802,7 +1919,7 @@ async function runAnswerLoop(input) {
1802
1919
  ? null
1803
1920
  : preferredSemanticMetric
1804
1921
  ? { metric: preferredSemanticMetric, score: 1, basis: 'name' }
1805
- : input.resolvedAnalyticalPlan?.mode === 'authoritative'
1922
+ : input.resolvedAnalyticalPlan?.mode === 'authoritative' && !legacyDirectSemanticPlan
1806
1923
  ? null
1807
1924
  : await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
1808
1925
  measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
@@ -1981,6 +2098,7 @@ async function runAnswerLoop(input) {
1981
2098
  if (artifactHit && businessContextTerminal) {
1982
2099
  let result;
1983
2100
  let executionError;
2101
+ let executionFailureDetail;
1984
2102
  if (artifactHit.node.kind === 'block' && input.executeCertifiedBlock) {
1985
2103
  try {
1986
2104
  result = await input.executeCertifiedBlock(artifactHit.node, {
@@ -1993,6 +2111,7 @@ async function runAnswerLoop(input) {
1993
2111
  result = trimResultToRequestedTopN(result, questionPlan);
1994
2112
  }
1995
2113
  catch (err) {
2114
+ executionFailureDetail = analyticalErrorDetail(err);
1996
2115
  executionError = err instanceof Error ? err.message : String(err);
1997
2116
  }
1998
2117
  }
@@ -2098,6 +2217,9 @@ async function runAnswerLoop(input) {
2098
2217
  ? undefined
2099
2218
  : questionPlan.requestedShape.topN?.n);
2100
2219
  const authoritativeCertifiedFailure = Boolean(authoritativeCertifiedBinding && executionError);
2220
+ const observabilityExecutionFailure = authoritativeCertifiedFailure
2221
+ ? observabilityFailureForFrozenExecution(executionFailureDetail)
2222
+ : undefined;
2101
2223
  // A selected certified block which returns an incomplete tuple is a
2102
2224
  // same-tier terminal failure. It is not a reason to reinterpret meaning,
2103
2225
  // label the partial rows generated, or retry another route after freeze.
@@ -2143,6 +2265,7 @@ async function runAnswerLoop(input) {
2143
2265
  block: artifactHit.node.kind === 'block' ? artifactHit.node : undefined,
2144
2266
  result,
2145
2267
  executionError,
2268
+ ...(observabilityExecutionFailure ? { observabilityExecutionFailure } : {}),
2146
2269
  ...(authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
2147
2270
  ? { refusalCode: 'grounding_gap' }
2148
2271
  : {}),
@@ -2637,8 +2760,21 @@ async function runAnswerLoop(input) {
2637
2760
  contextPack: input.contextPack,
2638
2761
  considered,
2639
2762
  providerUsed: provider.name,
2763
+ ...(frozenSemanticRoute ? { route: frozenSemanticRoute } : {}),
2640
2764
  };
2641
2765
  }
2766
+ // The semantic branch above has exhausted the exact plan's compiler and
2767
+ // adapter choices. Do not let `metricFirst` or the ordinary generated
2768
+ // proposal branch reinterpret the question after a semantic freeze.
2769
+ if (frozenSemanticRoute && !semanticBridgeAnswer) {
2770
+ return frozenSemanticPlanFailureAnswer({
2771
+ answerInput: input,
2772
+ considered,
2773
+ providerName: provider.name,
2774
+ code: 'COMPILATION_FAILED',
2775
+ reason: `The pinned semantic adapter could not compose ${frozenSemanticRoute.ref ?? 'the selected semantic plan'} without changing the frozen plan.`,
2776
+ });
2777
+ }
2642
2778
  const metricFirst = semanticMetricMatch
2643
2779
  ? buildGovernedMetricFirstSql({
2644
2780
  metric: semanticMetricMatch.metric,
@@ -2970,14 +3106,36 @@ async function runAnswerLoop(input) {
2970
3106
  || (input.contextPack?.allowedSqlContext?.relations.length ?? 0) > 0
2971
3107
  || (input.contextPack?.allowedSqlContext?.sourceBlockSql.length ?? 0) > 0
2972
3108
  || contextBlocks.length > 0;
3109
+ // A selected bounded-exploration RAP is stronger than the advisory legacy
3110
+ // analytical-path diagnosis. The latter may still call raw output terms a
3111
+ // semantic "missing dimension" even after the router has proven one safe
3112
+ // physical relation and frozen the exploratory tuple. Do not let that
3113
+ // advisory pre-freeze diagnosis suppress the one explicitly authorized
3114
+ // same-plan model-decline repair; the host re-validates the exact proposal
3115
+ // against the frozen closure before it can execute.
3116
+ const frozenExploratoryRepair = frozenExploratoryModelRepairAuthority(input);
2973
3117
  if (!parsed.sql && !governedMetricAnswer && wantsGeneratedData && hasGeneratableContext
2974
- && (analyticalPlan?.safe !== false || analyticalPlanAllowsDbtExploration(analyticalPlan))) {
3118
+ && (frozenExploratoryRepair
3119
+ || analyticalPlan?.safe !== false
3120
+ || analyticalPlanAllowsDbtExploration(analyticalPlan))) {
2975
3121
  try {
2976
3122
  proposed = await generateProposalWithOptionalTools({
2977
3123
  provider,
2978
- messages: [...messages, { role: 'system', content: FORCE_JOIN_INSTRUCTION }],
2979
- // One tool round per ordinary Ask. The second generation is a bounded
2980
- // composition correction over already-retrieved evidence.
3124
+ messages: [...messages, {
3125
+ role: 'system',
3126
+ content: [
3127
+ FORCE_JOIN_INSTRUCTION,
3128
+ ...(frozenExploratoryRepair
3129
+ ? [renderFrozenExploratoryRepairContract(input.resolvedAnalyticalPlan, questionPlan)]
3130
+ : []),
3131
+ ].join('\n\n'),
3132
+ }],
3133
+ // A router-frozen exploratory plan may consume exactly one additional
3134
+ // provider transport to correct a model decline. The host carries the
3135
+ // immutable plan and output tuple into this retry; it is neither a
3136
+ // replan nor a route change. Legacy direct callers retain their
3137
+ // historical composition correction, but do not receive the typed
3138
+ // repair authority below.
2981
3139
  tools: [],
2982
3140
  questionPlan,
2983
3141
  intent,
@@ -2986,6 +3144,13 @@ async function runAnswerLoop(input) {
2986
3144
  analysisDepth: input.analysisDepth,
2987
3145
  toolCalls: proposalToolCalls,
2988
3146
  providerPayloadGuard: input.providerPayloadGuard,
3147
+ ...(frozenExploratoryRepair
3148
+ ? {
3149
+ dispatchPhase: 'repair',
3150
+ egressPurpose: 'repair_sql',
3151
+ maxProviderDispatches: 1,
3152
+ }
3153
+ : {}),
2989
3154
  });
2990
3155
  parsed = parseProposal(proposed);
2991
3156
  }
@@ -3060,6 +3225,49 @@ async function runAnswerLoop(input) {
3060
3225
  }
3061
3226
  if (parsed.sql) {
3062
3227
  const frozenPlan = input.resolvedAnalyticalPlan;
3228
+ // The result contract is host-owned even in tests or embedded callers
3229
+ // that provide their own execution adapter. Stop before capability
3230
+ // preparation when a generated alias is backed by a different physical
3231
+ // column than the frozen plan (for example `product_id AS order_id`).
3232
+ // The local runtime repeats this check while minting the capability and
3233
+ // persists the matching proof on its receipt.
3234
+ if (input.selectedCascadeTier === 'exploratory_sql' && frozenPlan) {
3235
+ const requiredOutputValidation = validateFrozenRequiredOutputProjection({
3236
+ plan: frozenPlan,
3237
+ sql: parsed.sql,
3238
+ ...(input.semanticDriver ? { dialect: input.semanticDriver } : {}),
3239
+ });
3240
+ if (!requiredOutputValidation.ok) {
3241
+ const failedOutputs = [...new Set([
3242
+ ...requiredOutputValidation.missingOutputs,
3243
+ ...requiredOutputValidation.bindingMismatches,
3244
+ ])];
3245
+ const message = `The generated exploratory query did not prove the frozen source binding for required output ${failedOutputs.join(', ')}, so DQL did not execute it.`;
3246
+ return {
3247
+ kind: 'no_answer',
3248
+ sourceTier: 'no_answer',
3249
+ certification: 'analyst_review_required',
3250
+ reviewStatus: 'none',
3251
+ confidence: 0,
3252
+ text: message,
3253
+ answer: message,
3254
+ refusalCode: 'policy_blocked',
3255
+ refusalDetails: {
3256
+ code: 'OUTPUT_BINDING_TUPLE_DRIFT',
3257
+ message,
3258
+ },
3259
+ validationWarnings: [
3260
+ 'OUTPUT_BINDING_TUPLE_DRIFT',
3261
+ ...failedOutputs.map((output) => `Frozen output binding not proven: ${output}`),
3262
+ ],
3263
+ citations: contextPackCitations(input.contextPack, 8),
3264
+ memoryContext: input.memoryContext,
3265
+ contextPack: input.contextPack,
3266
+ considered,
3267
+ providerUsed: provider.name,
3268
+ };
3269
+ }
3270
+ }
3063
3271
  if (!governedMetricAnswer && frozenPlan?.mode === 'authoritative' && frozenPlan.schemaVersion === 2) {
3064
3272
  const targetFingerprint = input.generatedProposalTargetFingerprint ?? '';
3065
3273
  const validation = validateGeneratedAnalyticalProposal({
@@ -3664,12 +3872,14 @@ async function runAnswerLoop(input) {
3664
3872
  let executionError;
3665
3873
  let warehouseFailure;
3666
3874
  let exploratoryClosureDenied = false;
3875
+ let semanticFanoutProbeFailure;
3667
3876
  let repairAttempts = 0;
3668
3877
  // A forced proposal may originate from the bounded analyst loop, but it
3669
3878
  // cannot bypass a router-owned exploratory decision. In that case the host
3670
3879
  // still prepares and freezes the exact validated SQL before execution.
3671
3880
  const exploratoryExecutionSelected = input.selectedCascadeTier === 'exploratory_sql';
3672
- let preparedExploratoryExecution;
3881
+ let initialExploratoryExecution;
3882
+ let exploratoryRepairExecution;
3673
3883
  // Repair candidates that were generated but rejected before execution. These
3674
3884
  // are diagnostics ABOUT the recovery attempt, not the reason the run failed,
3675
3885
  // so they are reported alongside the original error rather than replacing it.
@@ -3684,7 +3894,7 @@ async function runAnswerLoop(input) {
3684
3894
  executionError = deepCandidateExecutionError;
3685
3895
  warehouseFailure = normalizeWarehouseSqlFailure(deepCandidateExecutionError, input.semanticDriver);
3686
3896
  }
3687
- const executeCurrentSql = async () => {
3897
+ const executeCurrentSql = async (authorizationAttempt) => {
3688
3898
  const requestedLimit = questionPlan.requestedShape.topN?.scope === 'per_group'
3689
3899
  ? 200
3690
3900
  : questionPlan.requestedShape.topN?.n ?? 200;
@@ -3725,17 +3935,23 @@ async function runAnswerLoop(input) {
3725
3935
  });
3726
3936
  }
3727
3937
  // This is the only point at which a router-selected exploratory proposal
3728
- // crosses from validated text into executable authority. The callback
3729
- // rechecks the snapshot, physical target, and qualified runtime columns,
3730
- // then returns a one-shot capability bound to these exact SQL bytes.
3731
- preparedExploratoryExecution ??= await input.prepareExploratorySqlExecution(parsed.sql, boundedArtifact);
3732
- return input.executeAgenticGeneratedSql(preparedExploratoryExecution.capability, parsed.sql, boundedArtifact);
3938
+ // crosses from validated text into executable authority. The first call
3939
+ // receives the original one-shot capability. A retryable warehouse
3940
+ // failure may mint exactly one *new* repair capability, explicitly bound
3941
+ // to the first SQL fingerprint; it never reopens meaning or routing.
3942
+ const prepared = authorizationAttempt
3943
+ ? await input.prepareExploratorySqlExecution(parsed.sql, boundedArtifact, authorizationAttempt)
3944
+ : (initialExploratoryExecution ??= await input.prepareExploratorySqlExecution(parsed.sql, boundedArtifact));
3945
+ if (authorizationAttempt)
3946
+ exploratoryRepairExecution = prepared;
3947
+ return input.executeAgenticGeneratedSql(prepared.capability, parsed.sql, boundedArtifact);
3733
3948
  }
3734
3949
  if (!input.executeGeneratedSql)
3735
3950
  throw new Error('No generated SQL executor is configured.');
3736
3951
  return input.executeGeneratedSql(parsed.sql, boundedArtifact);
3737
3952
  };
3738
3953
  if ((input.executeGeneratedSql
3954
+ || input.executeDqlArtifact
3739
3955
  || (input.forcedGeneratedProposal && input.executeAgenticGeneratedSql)
3740
3956
  || exploratoryExecutionSelected) && !result) {
3741
3957
  // Fanout gate for native semantic direct-joins: a duplicate join-key row on
@@ -3744,17 +3960,31 @@ async function runAnswerLoop(input) {
3744
3960
  // answers). Probe first; on structural contradiction, refuse with the cause
3745
3961
  // instead of presenting wrong numbers — and never hand the error to SQL
3746
3962
  // repair, which would regenerate the same multiplying join.
3747
- let fanoutContradiction = false;
3963
+ let semanticFanoutBlocked = false;
3748
3964
  if (!executionError
3749
3965
  && !input.forcedGeneratedProposal
3750
3966
  && !exploratoryExecutionSelected
3751
- && input.executeGeneratedSql
3752
3967
  && semanticBridgeAnswer?.composeResult?.fanoutProbeSql
3753
3968
  && semanticBridgeAnswer.sql.trim() === parsed.sql?.trim()) {
3754
- const inflationError = await probeSemanticJoinFanout(semanticBridgeAnswer.composeResult.fanoutProbeSql, semanticBridgeAnswer.composeResult.tables, input.executeGeneratedSql);
3755
- if (inflationError) {
3756
- executionError = inflationError;
3757
- fanoutContradiction = true;
3969
+ // A DQL-artifact executor owns the semantic artifact, not an arbitrary
3970
+ // one-row structural SQL probe. If the host did not also provide the
3971
+ // read-only SQL executor needed for that probe, native direct joins fail
3972
+ // closed rather than treating an artifact handoff as proof of uniqueness.
3973
+ const fanoutProbe = input.executeGeneratedSql
3974
+ ? await probeSemanticJoinFanout(semanticBridgeAnswer.composeResult.fanoutProbeSql, semanticBridgeAnswer.composeResult.tables, input.executeGeneratedSql)
3975
+ : {
3976
+ status: 'blocked',
3977
+ code: 'SEMANTIC_FANOUT_PROBE_ERROR',
3978
+ message: 'DQL did not execute this governed semantic answer because the host could not verify native join fanout before aggregation. Configure the semantic runtime or a read-only SQL probe executor, then retry.',
3979
+ };
3980
+ if (fanoutProbe.status === 'blocked') {
3981
+ // A governed semantic route freezes before execution. If DQL cannot
3982
+ // prove its native join is fanout-safe, it must fail closed rather than
3983
+ // execute, label the result governed, or quietly repair into a
3984
+ // different plan.
3985
+ executionError = fanoutProbe.message;
3986
+ semanticFanoutProbeFailure = fanoutProbe;
3987
+ semanticFanoutBlocked = true;
3758
3988
  }
3759
3989
  }
3760
3990
  try {
@@ -3776,9 +4006,15 @@ async function runAnswerLoop(input) {
3776
4006
  executionError = warehouseFailure.redactedMessage;
3777
4007
  }
3778
4008
  if (executionError
3779
- && !input.forcedGeneratedProposal
3780
- && !exploratoryExecutionSelected
3781
- && !fanoutContradiction
4009
+ // A host-supplied SQL proposal normally bypasses repair. A router-owned
4010
+ // exploratory proposal is the exception: its frozen plan may authorize
4011
+ // one corrected SQL statement after a retryable warehouse failure.
4012
+ && (!input.forcedGeneratedProposal || exploratoryExecutionSelected)
4013
+ // A router-selected exploratory plan may receive one bounded
4014
+ // same-plan repair. The repair still goes through a fresh host
4015
+ // authorization; it does not reuse the consumed initial capability.
4016
+ && (!exploratoryExecutionSelected || Boolean(initialExploratoryExecution))
4017
+ && !semanticFanoutBlocked
3782
4018
  && !authoritativeSemanticBinding) {
3783
4019
  warehouseFailure ??= normalizeWarehouseSqlFailure(executionError, input.semanticDriver);
3784
4020
  if (isRetryableGeneratedSqlError(warehouseFailure)) {
@@ -3804,7 +4040,13 @@ async function runAnswerLoop(input) {
3804
4040
  ],
3805
4041
  };
3806
4042
  try {
3807
- result = await executeCurrentSql();
4043
+ result = await executeCurrentSql(exploratoryExecutionSelected
4044
+ ? {
4045
+ version: 1,
4046
+ index: 1,
4047
+ parentSqlFingerprint: initialExploratoryExecution.freeze.sqlFingerprint,
4048
+ }
4049
+ : undefined);
3808
4050
  executionError = undefined;
3809
4051
  warehouseFailure = undefined;
3810
4052
  }
@@ -3836,6 +4078,18 @@ async function runAnswerLoop(input) {
3836
4078
  schemaContext,
3837
4079
  signal: input.signal,
3838
4080
  reasoningEffort: input.reasoningEffort,
4081
+ // Only the router-frozen exploratory lane has authority to spend
4082
+ // the exceptional post-warehouse repair transport. Legacy callers
4083
+ // keep their prior unlabelled repair behavior.
4084
+ ...(exploratoryExecutionSelected
4085
+ && Boolean(initialExploratoryExecution)
4086
+ && frozenExploratoryModelRepairAuthority(input)
4087
+ ? {
4088
+ dispatchPhase: 'repair',
4089
+ egressPurpose: 'repair_sql',
4090
+ maxProviderDispatches: 1,
4091
+ }
4092
+ : {}),
3839
4093
  });
3840
4094
  const repaired = parseProposal(repairedRaw);
3841
4095
  if (repaired.sql) {
@@ -3854,7 +4108,12 @@ async function runAnswerLoop(input) {
3854
4108
  repairNarrative = repaired.text?.trim() || undefined;
3855
4109
  parsed.sql = repaired.sql;
3856
4110
  parsed.viz = repaired.viz ?? parsed.viz;
3857
- applyParsedProposalMetadata(parsed, repaired);
4111
+ // A repair fixes SQL bytes only. On the router-owned
4112
+ // exploratory lane, model prose or metadata must never mutate
4113
+ // the host-selected frame, route, outputs, or trust state after
4114
+ // the plan froze.
4115
+ if (!exploratoryExecutionSelected)
4116
+ applyParsedProposalMetadata(parsed, repaired);
3858
4117
  contextValidation = {
3859
4118
  ok: true,
3860
4119
  warnings: [
@@ -3863,7 +4122,13 @@ async function runAnswerLoop(input) {
3863
4122
  ],
3864
4123
  };
3865
4124
  try {
3866
- result = await executeCurrentSql();
4125
+ result = await executeCurrentSql(exploratoryExecutionSelected
4126
+ ? {
4127
+ version: 1,
4128
+ index: 1,
4129
+ parentSqlFingerprint: initialExploratoryExecution.freeze.sqlFingerprint,
4130
+ }
4131
+ : undefined);
3867
4132
  executionError = undefined;
3868
4133
  warehouseFailure = undefined;
3869
4134
  }
@@ -3893,12 +4158,14 @@ async function runAnswerLoop(input) {
3893
4158
  // A router-selected physical-closure denial happens before a capability
3894
4159
  // exists. Its rejected bytes are diagnostics for the host, never a SQL
3895
4160
  // preview that a user can review, copy, or turn into a draft.
3896
- sql: exploratoryClosureDenied ? undefined : parsed.sql,
4161
+ sql: exploratoryClosureDenied || semanticFanoutProbeFailure ? undefined : parsed.sql,
3897
4162
  suggestedViz: parsed.viz ?? 'table',
3898
4163
  assumptions: [
3899
4164
  ...(exploratoryClosureDenied
3900
4165
  ? ['The router-selected physical closure rejected the generated SQL before capability minting.']
3901
- : ['The SQL preview is uncertified until an analyst reviews and promotes the DQL artifact.']),
4166
+ : semanticFanoutProbeFailure
4167
+ ? ['The native semantic join could not be proven fanout-safe before plan freeze, so DQL did not execute or expose its SQL.']
4168
+ : ['The SQL preview is uncertified until an analyst reviews and promotes the DQL artifact.']),
3902
4169
  ...(certifiedExecutionFallback
3903
4170
  ? [`Certified block ${certifiedExecutionFallback.node.name} failed execution and was bypassed: ${certifiedExecutionFallback.error}`]
3904
4171
  : []),
@@ -3922,6 +4189,23 @@ async function runAnswerLoop(input) {
3922
4189
  }
3923
4190
  }
3924
4191
  const resultShape = result ? validateAnswerResultShape(questionPlan, result) : undefined;
4192
+ // A router-selected exploratory plan carries a host-bound output
4193
+ // projection. The ordinary generated-answer UX may show a partial result
4194
+ // with a warning, but a frozen exploratory plan may not: displaying it
4195
+ // would silently change the tuple DQL authorized. Keep the connector rows
4196
+ // out of the answer/artifact when a broken executor returns them anyway.
4197
+ const missingFrozenExploratoryOutputs = exploratoryExecutionSelected && result
4198
+ ? missingFrozenRequiredOutputProjection(input.resolvedAnalyticalPlan, result)
4199
+ : [];
4200
+ const activeExploratoryOutputFreeze = exploratoryRepairExecution?.freeze ?? initialExploratoryExecution?.freeze;
4201
+ const missingFrozenExploratoryBindingProofs = exploratoryExecutionSelected && result
4202
+ ? missingFrozenRequiredOutputBindingProof(input.resolvedAnalyticalPlan, activeExploratoryOutputFreeze)
4203
+ : [];
4204
+ const frozenExploratoryOutputFailures = [...new Set([
4205
+ ...missingFrozenExploratoryOutputs,
4206
+ ...missingFrozenExploratoryBindingProofs,
4207
+ ])];
4208
+ const frozenExploratoryOutputContractFailure = frozenExploratoryOutputFailures.length > 0;
3925
4209
  // ANY question whose SQL executed but dropped multiple requested columns used to
3926
4210
  // REFUSE outright ("no governed answer"), throwing away a result that actually
3927
4211
  // ran. Instead, SURFACE the partial result (review-required) with a warning that
@@ -3944,13 +4228,16 @@ async function runAnswerLoop(input) {
3944
4228
  ...repairNotes,
3945
4229
  ...(resultShape?.warnings ?? []),
3946
4230
  ...(partialShapeWarning ? [partialShapeWarning] : []),
4231
+ ...(frozenExploratoryOutputContractFailure
4232
+ ? [`The executed exploratory result violated the frozen output contract: ${frozenExploratoryOutputFailures.join(', ')}.`]
4233
+ : []),
3947
4234
  ...(topNTrimNote ? [topNTrimNote] : []),
3948
4235
  ...(executionError ? ['The preview execution error must be reviewed before reuse.'] : []),
3949
4236
  ];
3950
4237
  const generatedOutputs = parsed.outputs?.length ? parsed.outputs : resultColumnNames(result);
3951
4238
  const generatedRequestedFilters = mergeProposalStringLists(input.followUp?.filters, parsed.requestedFilters);
3952
4239
  const generatedRequestedDimensions = mergeProposalStringLists(input.followUp?.dimensions, parsed.requestedDimensions);
3953
- const baseDqlArtifact = exploratoryClosureDenied
4240
+ const baseDqlArtifact = exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure
3954
4241
  ? undefined
3955
4242
  : result?.dqlArtifact ?? semanticBridgeAnswer?.dqlArtifact ?? buildGeneratedSqlDqlArtifact({
3956
4243
  question,
@@ -3979,7 +4266,7 @@ async function runAnswerLoop(input) {
3979
4266
  : undefined;
3980
4267
  let draftBlock;
3981
4268
  let draftCaptureError;
3982
- if (!exploratoryClosureDenied && input.captureGeneratedDraft && parsed.sql) {
4269
+ if (!exploratoryClosureDenied && !semanticFanoutProbeFailure && !frozenExploratoryOutputContractFailure && input.captureGeneratedDraft && parsed.sql) {
3983
4270
  try {
3984
4271
  draftBlock = await input.captureGeneratedDraft({
3985
4272
  question,
@@ -4026,10 +4313,21 @@ async function runAnswerLoop(input) {
4026
4313
  : undefined;
4027
4314
  const certifiedMetricAnswer = semanticMetricCertification === 'certified' || semanticMetricCertification === 'reviewed';
4028
4315
  const governedMetricExecutionFailure = governedMetricAnswer && Boolean(executionError);
4029
- const terminalExecutionFailure = governedMetricExecutionFailure || exploratoryClosureDenied;
4316
+ const terminalExecutionFailure = governedMetricExecutionFailure || exploratoryClosureDenied || frozenExploratoryOutputContractFailure;
4317
+ const semanticFanoutTraceError = semanticFanoutProbeFailure
4318
+ ? Object.assign(new Error(semanticFanoutProbeFailure.message), {
4319
+ code: semanticFanoutProbeFailure.code,
4320
+ details: { phase: 'validation' },
4321
+ })
4322
+ : executionError;
4323
+ const terminalFailureMessage = frozenExploratoryOutputContractFailure
4324
+ ? `The exploratory execution result did not retain proven frozen output binding ${frozenExploratoryOutputFailures.join(', ')}, so DQL did not display it.`
4325
+ : exploratoryClosureDenied
4326
+ ? 'The generated SQL referenced a relation outside the router-selected physical closure, so DQL did not execute it.'
4327
+ : semanticFanoutProbeFailure?.message;
4030
4328
  const finalSemanticExecutionTrace = semanticTraceAfterExecution(semanticExecutionTrace, {
4031
4329
  executed: Boolean(result),
4032
- ...(executionError ? { error: executionError } : {}),
4330
+ ...(semanticFanoutTraceError ? { error: semanticFanoutTraceError } : {}),
4033
4331
  ...(result ? { result } : {}),
4034
4332
  });
4035
4333
  return {
@@ -4039,21 +4337,25 @@ async function runAnswerLoop(input) {
4039
4337
  reviewStatus: terminalExecutionFailure ? 'none' : governedMetricAnswer ? 'governed' : 'draft_ready',
4040
4338
  semanticMetricCertification,
4041
4339
  confidence: terminalExecutionFailure ? 0 : certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
4042
- text: exploratoryClosureDenied
4043
- ? 'The generated SQL referenced a relation outside the router-selected physical closure, so DQL did not execute it.'
4044
- : generatedText,
4045
- answer: exploratoryClosureDenied
4046
- ? 'The generated SQL referenced a relation outside the router-selected physical closure, so DQL did not execute it.'
4047
- : generatedText,
4048
- ...(exploratoryClosureDenied ? {} : { proposedSql: parsed.sql, sql: parsed.sql }),
4049
- result,
4340
+ text: terminalFailureMessage ?? generatedText,
4341
+ answer: terminalFailureMessage ?? generatedText,
4342
+ ...(exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure ? {} : { proposedSql: parsed.sql, sql: parsed.sql }),
4343
+ result: frozenExploratoryOutputContractFailure ? undefined : result,
4050
4344
  executionError,
4051
4345
  ...(warehouseFailure ? { warehouseFailure } : {}),
4052
4346
  ...(finalSemanticExecutionTrace ? { semanticExecutionTrace: finalSemanticExecutionTrace } : {}),
4053
- ...(terminalExecutionFailure ? { refusalCode: 'grounding_gap' } : {}),
4347
+ ...(terminalExecutionFailure ? {
4348
+ refusalCode: semanticFanoutProbeFailure ? 'policy_blocked' : 'grounding_gap',
4349
+ } : {}),
4350
+ ...(semanticFanoutProbeFailure ? {
4351
+ refusalDetails: {
4352
+ code: semanticFanoutProbeFailure.code,
4353
+ message: semanticFanoutProbeFailure.message,
4354
+ },
4355
+ } : {}),
4054
4356
  suggestedViz: parsed.viz ?? 'table',
4055
- ...(exploratoryClosureDenied ? {} : { dqlArtifact: answerDqlArtifact }),
4056
- ...(exploratoryClosureDenied ? {} : {
4357
+ ...(exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure ? {} : { dqlArtifact: answerDqlArtifact }),
4358
+ ...(exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure ? {} : {
4057
4359
  draftBlock,
4058
4360
  draftBlockId: draftBlock?.path,
4059
4361
  promoteCommand: draftBlock ? `dql certify --from-draft ${draftBlock.path}` : undefined,
@@ -4095,7 +4397,8 @@ async function runAnswerLoop(input) {
4095
4397
  considered,
4096
4398
  providerUsed: provider.name,
4097
4399
  aggregationSafetyProof,
4098
- ...(preparedExploratoryExecution ? { exploratoryExecutionFreeze: preparedExploratoryExecution.freeze } : {}),
4400
+ ...(initialExploratoryExecution ? { exploratoryExecutionFreeze: initialExploratoryExecution.freeze } : {}),
4401
+ ...(exploratoryRepairExecution ? { exploratoryRepairExecutionFreeze: exploratoryRepairExecution.freeze } : {}),
4099
4402
  // Carry the governed metric match so the exit point can name a
4100
4403
  // `semantic_metric` route (spec 17, part C).
4101
4404
  _semanticMetricMatch: governedMetricAnswer ? semanticMetricMatch ?? undefined : undefined,
@@ -4135,6 +4438,70 @@ function resultColumnNames(result) {
4135
4438
  .filter(Boolean);
4136
4439
  return columns?.length ? columns : undefined;
4137
4440
  }
4441
+ /**
4442
+ * Check only the host-bound aliases from a frozen plan. This is intentionally
4443
+ * stricter than the conversational shape helper: synonym matching is useful
4444
+ * for ordinary narration, but it cannot substitute a requested order/product
4445
+ * identifier after exploratory SQL has been authorized.
4446
+ */
4447
+ function missingFrozenRequiredOutputProjection(plan, result) {
4448
+ const required = plan?.outputContract?.requiredOutputs ?? [];
4449
+ if (required.length === 0)
4450
+ return [];
4451
+ const columns = new Set((resultColumnNames(result) ?? []).map(canonicalFrozenOutputAlias));
4452
+ return required.flatMap((binding) => {
4453
+ const alias = binding.outputName
4454
+ ?? binding.qualifiedId?.split(/[:.]/).at(-1)
4455
+ ?? binding.requested;
4456
+ const normalized = canonicalFrozenOutputAlias(alias);
4457
+ return normalized && columns.has(normalized) ? [] : [alias];
4458
+ });
4459
+ }
4460
+ /**
4461
+ * A column name in a returned result does not prove where it came from. The
4462
+ * result is accepted only when the server-owned authorization receipt carries
4463
+ * the exact frozen output-source binding that was parser-validated for the SQL
4464
+ * handed to the connector. Old persisted receipts remain readable; they
4465
+ * simply cannot authorize a new result for a plan that has explicit outputs.
4466
+ */
4467
+ function missingFrozenRequiredOutputBindingProof(plan, freeze) {
4468
+ const required = plan?.outputContract?.requiredOutputs ?? [];
4469
+ if (required.length === 0)
4470
+ return [];
4471
+ const expected = frozenRequiredOutputBindingProofsForPlan(plan);
4472
+ const missingSourceBindingAliases = required
4473
+ .filter((binding) => !expected.some((proof) => canonicalFrozenOutputAlias(proof.outputName)
4474
+ === canonicalFrozenOutputAlias(binding.outputName
4475
+ ?? binding.qualifiedId?.split(/[:.]/).at(-1)
4476
+ ?? binding.requested)))
4477
+ .map((binding) => binding.outputName
4478
+ ?? binding.qualifiedId?.split(/[:.]/).at(-1)
4479
+ ?? binding.requested);
4480
+ const actual = freeze?.requiredOutputBindings ?? [];
4481
+ const missingProofAliases = expected
4482
+ .filter((proof) => !actual.some((candidate) => sameFrozenRequiredOutputBindingProof(candidate, proof)))
4483
+ .map((proof) => proof.outputName);
4484
+ return [...new Set([...missingSourceBindingAliases, ...missingProofAliases])];
4485
+ }
4486
+ function sameFrozenRequiredOutputBindingProof(actual, expected) {
4487
+ return actual.version === 1
4488
+ && actual.qualifiedId === expected.qualifiedId
4489
+ && canonicalFrozenOutputAlias(actual.outputName) === canonicalFrozenOutputAlias(expected.outputName)
4490
+ && canonicalFrozenSourceIdentifier(actual.relation) === canonicalFrozenSourceIdentifier(expected.relation)
4491
+ && canonicalFrozenSourceIdentifier(actual.column) === canonicalFrozenSourceIdentifier(expected.column);
4492
+ }
4493
+ function canonicalFrozenOutputAlias(value) {
4494
+ return value.toLowerCase()
4495
+ .replace(/["`]/g, '')
4496
+ .replace(/[^a-z0-9]+/g, '_')
4497
+ .replace(/^_+|_+$/g, '');
4498
+ }
4499
+ function canonicalFrozenSourceIdentifier(value) {
4500
+ return value.toLowerCase()
4501
+ .replace(/["`\[\]]/g, '')
4502
+ .replace(/\s*\.\s*/g, '.')
4503
+ .trim();
4504
+ }
4138
4505
  function analyticalCapabilityForPlan(plan, contextPack, registry = []) {
4139
4506
  const selected = normalizeMetricCapabilityContract(plan.selectedCapability);
4140
4507
  if (selected) {
@@ -4501,8 +4868,11 @@ async function executeSemanticAnalyticalGraph(input) {
4501
4868
  });
4502
4869
  aggregationSafetyProof = proof;
4503
4870
  if (proof.status !== 'safe') {
4504
- return analyticalGraphFailureAnswer(input, 'EXECUTION_GRAPH_MISMATCH', semanticAggregationSafetyFailure(proof), {
4505
- phase: 'validation',
4871
+ return analyticalGraphFailureAnswer(input, 'COMPILATION_FAILED', semanticAggregationSafetyFailure(proof), {
4872
+ // This is compiler/plan validation before a warehouse statement is
4873
+ // dispatched. It must be represented as semantic.compile, never as
4874
+ // result normalization or a SQL execution failure.
4875
+ phase: 'compilation',
4506
4876
  dqlArtifact: composed.dqlArtifact,
4507
4877
  compiledSql: composed.sql,
4508
4878
  semanticExecutionTrace,
@@ -4528,11 +4898,25 @@ async function executeSemanticAnalyticalGraph(input) {
4528
4898
  }
4529
4899
  catch (error) {
4530
4900
  const message = error instanceof Error ? error.message : String(error);
4901
+ // A missing local execution target fails at the host's pre-execution
4902
+ // boundary. It must not be recast as a semantic compiler failure merely
4903
+ // because the semantic graph owns the callback. Preserve the narrow,
4904
+ // producer-tagged fact for the durable Ask receipt; it does not alter
4905
+ // the frozen route, trust, or user-facing analytical failure.
4906
+ const observabilityExecutionFailure = observabilityFailureForFrozenExecution(analyticalErrorDetail(error));
4531
4907
  return analyticalGraphFailureAnswer(input, 'EXECUTION_FAILED', message, {
4532
4908
  phase: 'execution',
4533
4909
  dqlArtifact: composed.dqlArtifact,
4534
4910
  compiledSql: composed.sql,
4535
4911
  semanticExecutionTrace: semanticTraceAfterExecution(semanticExecutionTrace, { executed: false, error }),
4912
+ // The exact aggregation proof already passed before the connector
4913
+ // call. Preserve it so presentation never recasts a warehouse binder
4914
+ // error as a failed semantic aggregation proof.
4915
+ aggregationSafetyProof,
4916
+ // This producer-tagged fact is diagnostic only: it does not alter
4917
+ // routing, trust, or the immutable frozen plan.
4918
+ warehouseFailure: normalizeWarehouseSqlFailure(error, input.input.semanticDriver),
4919
+ ...(observabilityExecutionFailure ? { observabilityExecutionFailure } : {}),
4536
4920
  });
4537
4921
  }
4538
4922
  const normalized = normalizeAnalyticalSourceResult({
@@ -4962,6 +5346,10 @@ function analyticalGraphFailureAnswer(input, code, reason, artifacts = {}) {
4962
5346
  ...(artifacts.dqlArtifact ? { dqlArtifact: artifacts.dqlArtifact } : {}),
4963
5347
  ...(artifacts.semanticExecutionTrace ? { semanticExecutionTrace: artifacts.semanticExecutionTrace } : {}),
4964
5348
  ...(artifacts.aggregationSafetyProof ? { aggregationSafetyProof: artifacts.aggregationSafetyProof } : {}),
5349
+ ...(artifacts.warehouseFailure ? { warehouseFailure: artifacts.warehouseFailure } : {}),
5350
+ ...(artifacts.observabilityExecutionFailure
5351
+ ? { observabilityExecutionFailure: artifacts.observabilityExecutionFailure }
5352
+ : {}),
4965
5353
  citations: contextPackCitations(input.input.contextPack, 8),
4966
5354
  considered: input.considered,
4967
5355
  contextPack: input.input.contextPack,
@@ -5133,6 +5521,38 @@ Rules:
5133
5521
  // combined dataset — show them separately" refusal into the join the user asked
5134
5522
  // for, while still allowing an honest refusal if context is truly missing.
5135
5523
  const FORCE_JOIN_INSTRUCTION = `Your previous attempt declined to produce SQL. Re-read the supplied schema, metadata, and any "Knowledge graph join routes": this question CAN be answered by joining the grounded tables along their documented keys. Do NOT refuse, and do NOT suggest showing the datasets separately — compose ONE read-only SELECT/WITH that joins the relevant tables to answer it directly, following the JSON contract from rule 3. State the grain and the exact join path in the summary. Only if a required table, column, or join key is truly absent from the supplied context may you still ask a clarifying question.`;
5524
+ /**
5525
+ * A model-decline correction is only a repair when the router already froze a
5526
+ * bounded exploratory RAP and the host supplied the matching capability
5527
+ * mint/consume closures. Anything weaker is a new planning attempt and must
5528
+ * not borrow the repair transport allowance.
5529
+ */
5530
+ function frozenExploratoryModelRepairAuthority(input) {
5531
+ const plan = input.resolvedAnalyticalPlan;
5532
+ return input.selectedCascadeTier === 'exploratory_sql'
5533
+ && plan?.mode === 'authoritative'
5534
+ && plan.capability === 'bounded_exploration'
5535
+ && Boolean(plan.planId && plan.fingerprint && plan.snapshotId)
5536
+ && Boolean(input.prepareExploratorySqlExecution && input.executeAgenticGeneratedSql);
5537
+ }
5538
+ /**
5539
+ * The corrective prompt repeats only host-owned tuple facts. It never asks
5540
+ * the model to choose a new route, relation closure, output contract, or
5541
+ * trust label; SQL authorization re-proves those facts after the response.
5542
+ */
5543
+ function renderFrozenExploratoryRepairContract(plan, questionPlan) {
5544
+ const requiredOutputs = plan.outputContract?.requiredOutputs?.filter((output) => output.status === 'resolved' && output.outputName && output.qualifiedId)
5545
+ .map((output) => `${output.outputName} <- ${output.qualifiedId}`)
5546
+ ?? [];
5547
+ return [
5548
+ 'This is the one permitted correction for an already frozen, review-required exploratory plan.',
5549
+ renderRequestedShapeForRepair(questionPlan),
5550
+ requiredOutputs.length > 0
5551
+ ? `Frozen required output bindings (preserve exactly): ${requiredOutputs.join('; ')}`
5552
+ : 'Preserve the frozen requested tuple exactly.',
5553
+ 'Use only the relations and columns already supplied in the bounded context. Do not add a relation, change the ranking, remove an output, change the limit, or alter the analytical meaning. The host will validate the same frozen snapshot and closure before execution.',
5554
+ ].join('\n');
5555
+ }
5136
5556
  /**
5137
5557
  * Produces the prompt-facing subset of a broad local context pack. Global
5138
5558
  * records remain available, while a record explicitly owned by another domain
@@ -7842,6 +8262,11 @@ async function requestSqlRepair(input) {
7842
8262
  // bumping would let an internal preview-repair exceed that cap. Escalation-level
7843
8263
  // repairs bump-then-clamp in the host (resolveRunReasoningEffort).
7844
8264
  reasoningEffort: input.reasoningEffort,
8265
+ ...(input.dispatchPhase ? { dispatchPhase: input.dispatchPhase } : {}),
8266
+ ...(input.egressPurpose ? { egressPurpose: input.egressPurpose } : {}),
8267
+ ...(input.maxProviderDispatches !== undefined
8268
+ ? { maxProviderDispatches: input.maxProviderDispatches }
8269
+ : {}),
7845
8270
  });
7846
8271
  }
7847
8272
  async function generateProposalWithOptionalTools(input) {
@@ -7849,6 +8274,11 @@ async function generateProposalWithOptionalTools(input) {
7849
8274
  const options = {
7850
8275
  signal: input.signal,
7851
8276
  reasoningEffort: input.reasoningEffort,
8277
+ ...(input.dispatchPhase ? { dispatchPhase: input.dispatchPhase } : {}),
8278
+ ...(input.egressPurpose ? { egressPurpose: input.egressPurpose } : {}),
8279
+ ...(input.maxProviderDispatches !== undefined
8280
+ ? { maxProviderDispatches: input.maxProviderDispatches }
8281
+ : {}),
7852
8282
  };
7853
8283
  // No tools → plain generation (nothing for the loop to drive).
7854
8284
  if (tools.length === 0) {
@@ -8245,10 +8675,25 @@ function isRetryableGeneratedSqlError(failure) {
8245
8675
  return failure.retryDisposition === 'model_repair';
8246
8676
  }
8247
8677
  /**
8248
- * Only SQL-shape/binding failures may leave the certified lane. Connectivity,
8249
- * authorization, cancellation, and timeout failures would affect every query
8250
- * route and must remain terminal rather than triggering wasteful provider work.
8678
+ * Only SQL-shape/binding failures may leave a frozen governed lane.
8679
+ * Connectivity, authorization, cancellation, and timeout failures would affect
8680
+ * every query route and must remain terminal rather than triggering wasteful
8681
+ * provider work.
8251
8682
  */
8683
+ function observabilityFailureForFrozenExecution(detail) {
8684
+ // Do not infer this from user-facing error text. The local runtime tags the
8685
+ // physical pre-execution boundary; this projection only carries that exact
8686
+ // typed fact into the redacted trace after a governed plan has frozen.
8687
+ if (detail?.origin !== 'host' || detail.stage !== 'execute' || detail.code !== 'connection_not_configured') {
8688
+ return undefined;
8689
+ }
8690
+ return {
8691
+ version: 1,
8692
+ phase: 'execution',
8693
+ cause: 'connection_not_configured',
8694
+ safeAction: 'configure_connection',
8695
+ };
8696
+ }
8252
8697
  function isRetryableCertifiedExecutionError(error) {
8253
8698
  return /\b(?:binder|parser|catalog)\s+error\b/i.test(error)
8254
8699
  // DuckDB says "ambiguous reference"; Snowflake says "ambiguous column name".
@@ -9213,29 +9658,45 @@ function uniqueAssets(assets) {
9213
9658
  return Array.from(byId.values());
9214
9659
  }
9215
9660
  /**
9216
- * Execute the semantic layer's fanout probe and translate a structural
9217
- * contradiction into an actionable refusal message. Probe failures (missing
9218
- * permissions, dialect quirks) return undefined the probe protects against
9219
- * silent inflation but must never become a new way for a healthy query to fail.
9661
+ * Execute the semantic layer's fanout probe before a governed native semantic
9662
+ * join freezes. A native join that cannot be checked is not a governed-safe
9663
+ * join: fail closed with a redacted typed failure instead of executing a query
9664
+ * that could multiply aggregates. MetricFlow/dbt Cloud paths have their own
9665
+ * compiler guarantees and do not use this native direct-join guard.
9220
9666
  */
9221
9667
  export async function probeSemanticJoinFanout(probeSql, joinedTables, executeSql) {
9222
9668
  try {
9223
9669
  const payload = await executeSql(probeSql);
9224
9670
  const counts = parseFanoutProbeCounts(payload);
9225
- if (!counts || counts.base <= 0 || counts.joined <= counts.base)
9226
- return undefined;
9671
+ if (!counts || counts.base <= 0) {
9672
+ return {
9673
+ status: 'blocked',
9674
+ code: 'SEMANTIC_FANOUT_PROBE_UNPARSEABLE',
9675
+ message: 'DQL did not execute this governed semantic answer because its join-safety probe did not return verifiable row counts. Check the declared relationship or run the metric through MetricFlow / dbt Cloud, then retry.',
9676
+ };
9677
+ }
9678
+ if (counts.joined <= counts.base)
9679
+ return { status: 'safe' };
9227
9680
  const factor = counts.joined / counts.base;
9228
9681
  const tables = joinedTables.filter(Boolean).join(', ');
9229
- return [
9230
- `Blocked a governed semantic answer whose join inflates results: joining ${tables || 'the declared tables'}`,
9231
- `turned ${counts.base.toLocaleString('en-US')} base rows into ${counts.joined.toLocaleString('en-US')} (×${factor >= 10 ? Math.round(factor) : factor.toFixed(1)}).`,
9232
- 'The declared join key is not unique on the joined side, so every aggregated value would be multiplied.',
9233
- 'Deduplicate the joined model (for example, filter a slowly-changing dimension to current records)',
9234
- 'or execute this metric through MetricFlow / dbt Cloud, then retry. No numbers were shown because they would be wrong.',
9235
- ].join(' ');
9682
+ return {
9683
+ status: 'blocked',
9684
+ code: 'SEMANTIC_FANOUT_DUPLICATE_KEY',
9685
+ message: [
9686
+ `Blocked a governed semantic answer whose join inflates results: joining ${tables || 'the declared tables'}`,
9687
+ `turned ${counts.base.toLocaleString('en-US')} base rows into ${counts.joined.toLocaleString('en-US')} (×${factor >= 10 ? Math.round(factor) : factor.toFixed(1)}).`,
9688
+ 'The declared join key is not unique on the joined side, so every aggregated value would be multiplied.',
9689
+ 'Deduplicate the joined model (for example, filter a slowly-changing dimension to current records)',
9690
+ 'or execute this metric through MetricFlow / dbt Cloud, then retry. No numbers were shown because they would be wrong.',
9691
+ ].join(' '),
9692
+ };
9236
9693
  }
9237
9694
  catch {
9238
- return undefined;
9695
+ return {
9696
+ status: 'blocked',
9697
+ code: 'SEMANTIC_FANOUT_PROBE_ERROR',
9698
+ message: 'DQL did not execute this governed semantic answer because it could not verify join fanout before aggregation. Check the declared relationship or run the metric through MetricFlow / dbt Cloud, then retry.',
9699
+ };
9239
9700
  }
9240
9701
  }
9241
9702
  function parseFanoutProbeCounts(payload) {