@duckcodeailabs/dql-agent 1.14.1 → 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 (174) hide show
  1. package/dist/agent-run-engine.d.ts +71 -2
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +1048 -31
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-gates.d.ts.map +1 -1
  6. package/dist/agent-run-gates.js +12 -0
  7. package/dist/agent-run-gates.js.map +1 -1
  8. package/dist/agent-run-planner.d.ts +6 -0
  9. package/dist/agent-run-planner.d.ts.map +1 -1
  10. package/dist/agent-run-planner.js +2 -0
  11. package/dist/agent-run-planner.js.map +1 -1
  12. package/dist/agent-run-store.d.ts.map +1 -1
  13. package/dist/agent-run-store.js +48 -3
  14. package/dist/agent-run-store.js.map +1 -1
  15. package/dist/agentic/analyst-loop.d.ts +7 -1
  16. package/dist/agentic/analyst-loop.d.ts.map +1 -1
  17. package/dist/agentic/analyst-loop.js +211 -28
  18. package/dist/agentic/analyst-loop.js.map +1 -1
  19. package/dist/agentic/assumptions.d.ts +1 -1
  20. package/dist/agentic/assumptions.d.ts.map +1 -1
  21. package/dist/agentic/assumptions.js.map +1 -1
  22. package/dist/agentic/research/synthesis.d.ts +4 -0
  23. package/dist/agentic/research/synthesis.d.ts.map +1 -1
  24. package/dist/agentic/research/synthesis.js +23 -8
  25. package/dist/agentic/research/synthesis.js.map +1 -1
  26. package/dist/agentic/research-agent.d.ts.map +1 -1
  27. package/dist/agentic/research-agent.js +13 -3
  28. package/dist/agentic/research-agent.js.map +1 -1
  29. package/dist/agentic/sql-authorization.d.ts +8 -0
  30. package/dist/agentic/sql-authorization.d.ts.map +1 -1
  31. package/dist/agentic/sql-authorization.js +233 -2
  32. package/dist/agentic/sql-authorization.js.map +1 -1
  33. package/dist/analytical-compatibility.d.ts.map +1 -1
  34. package/dist/analytical-compatibility.js +8 -1
  35. package/dist/analytical-compatibility.js.map +1 -1
  36. package/dist/analytical-failure-repair.d.ts.map +1 -1
  37. package/dist/analytical-failure-repair.js +6 -2
  38. package/dist/analytical-failure-repair.js.map +1 -1
  39. package/dist/analytical-frame.d.ts +23 -1
  40. package/dist/analytical-frame.d.ts.map +1 -1
  41. package/dist/analytical-frame.js +332 -17
  42. package/dist/analytical-frame.js.map +1 -1
  43. package/dist/analytical-orchestration.d.ts +613 -0
  44. package/dist/analytical-orchestration.d.ts.map +1 -1
  45. package/dist/analytical-orchestration.js +1007 -0
  46. package/dist/analytical-orchestration.js.map +1 -1
  47. package/dist/answer-loop.d.ts +110 -6
  48. package/dist/answer-loop.d.ts.map +1 -1
  49. package/dist/answer-loop.js +1180 -125
  50. package/dist/answer-loop.js.map +1 -1
  51. package/dist/answer-shape.d.ts +32 -1
  52. package/dist/answer-shape.d.ts.map +1 -1
  53. package/dist/answer-shape.js +49 -3
  54. package/dist/answer-shape.js.map +1 -1
  55. package/dist/ask-observability/index.d.ts +11 -0
  56. package/dist/ask-observability/index.d.ts.map +1 -0
  57. package/dist/ask-observability/index.js +7 -0
  58. package/dist/ask-observability/index.js.map +1 -0
  59. package/dist/ask-observability/instrumentation.d.ts +35 -0
  60. package/dist/ask-observability/instrumentation.d.ts.map +1 -0
  61. package/dist/ask-observability/instrumentation.js +472 -0
  62. package/dist/ask-observability/instrumentation.js.map +1 -0
  63. package/dist/ask-observability/observer.d.ts +62 -0
  64. package/dist/ask-observability/observer.d.ts.map +1 -0
  65. package/dist/ask-observability/observer.js +537 -0
  66. package/dist/ask-observability/observer.js.map +1 -0
  67. package/dist/ask-observability/portable.d.ts +98 -0
  68. package/dist/ask-observability/portable.d.ts.map +1 -0
  69. package/dist/ask-observability/portable.js +527 -0
  70. package/dist/ask-observability/portable.js.map +1 -0
  71. package/dist/ask-observability/store.d.ts +130 -0
  72. package/dist/ask-observability/store.d.ts.map +1 -0
  73. package/dist/ask-observability/store.js +1114 -0
  74. package/dist/ask-observability/store.js.map +1 -0
  75. package/dist/ask-observability/types.d.ts +357 -0
  76. package/dist/ask-observability/types.d.ts.map +1 -0
  77. package/dist/ask-observability/types.js +2 -0
  78. package/dist/ask-observability/types.js.map +1 -0
  79. package/dist/ask-observability/utils.d.ts +21 -0
  80. package/dist/ask-observability/utils.d.ts.map +1 -0
  81. package/dist/ask-observability/utils.js +102 -0
  82. package/dist/ask-observability/utils.js.map +1 -0
  83. package/dist/conversation/snapshot.d.ts +20 -0
  84. package/dist/conversation/snapshot.d.ts.map +1 -1
  85. package/dist/conversation/snapshot.js +30 -1
  86. package/dist/conversation/snapshot.js.map +1 -1
  87. package/dist/fixtures/ask-ai-office-shaped.d.ts +153 -0
  88. package/dist/fixtures/ask-ai-office-shaped.d.ts.map +1 -0
  89. package/dist/fixtures/ask-ai-office-shaped.js +94 -0
  90. package/dist/fixtures/ask-ai-office-shaped.js.map +1 -0
  91. package/dist/generated-analytical-proposal.d.ts +37 -1
  92. package/dist/generated-analytical-proposal.d.ts.map +1 -1
  93. package/dist/generated-analytical-proposal.js +211 -3
  94. package/dist/generated-analytical-proposal.js.map +1 -1
  95. package/dist/governed-relational-compiler.d.ts +15 -0
  96. package/dist/governed-relational-compiler.d.ts.map +1 -1
  97. package/dist/governed-relational-compiler.js +99 -1
  98. package/dist/governed-relational-compiler.js.map +1 -1
  99. package/dist/index.d.ts +11 -9
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +7 -5
  102. package/dist/index.js.map +1 -1
  103. package/dist/intent-controller.d.ts +38 -0
  104. package/dist/intent-controller.d.ts.map +1 -1
  105. package/dist/intent-controller.js +31 -1
  106. package/dist/intent-controller.js.map +1 -1
  107. package/dist/meaning-resolution.d.ts +141 -2
  108. package/dist/meaning-resolution.d.ts.map +1 -1
  109. package/dist/meaning-resolution.js +444 -30
  110. package/dist/meaning-resolution.js.map +1 -1
  111. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  112. package/dist/metadata/analysis-planner.js +144 -7
  113. package/dist/metadata/analysis-planner.js.map +1 -1
  114. package/dist/metadata/block-fit.d.ts +18 -0
  115. package/dist/metadata/block-fit.d.ts.map +1 -1
  116. package/dist/metadata/block-fit.js +250 -38
  117. package/dist/metadata/block-fit.js.map +1 -1
  118. package/dist/metadata/catalog.d.ts.map +1 -1
  119. package/dist/metadata/catalog.js +58 -12
  120. package/dist/metadata/catalog.js.map +1 -1
  121. package/dist/metadata/meaning-evidence.d.ts +19 -3
  122. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  123. package/dist/metadata/meaning-evidence.js +518 -54
  124. package/dist/metadata/meaning-evidence.js.map +1 -1
  125. package/dist/plan-execution-adapter.d.ts +9 -1
  126. package/dist/plan-execution-adapter.d.ts.map +1 -1
  127. package/dist/plan-execution-adapter.js +93 -13
  128. package/dist/plan-execution-adapter.js.map +1 -1
  129. package/dist/provider-egress.d.ts +24 -10
  130. package/dist/provider-egress.d.ts.map +1 -1
  131. package/dist/provider-egress.js +22 -17
  132. package/dist/provider-egress.js.map +1 -1
  133. package/dist/providers/claude.d.ts.map +1 -1
  134. package/dist/providers/claude.js +10 -9
  135. package/dist/providers/claude.js.map +1 -1
  136. package/dist/providers/dispatch.d.ts +27 -1
  137. package/dist/providers/dispatch.d.ts.map +1 -1
  138. package/dist/providers/dispatch.js +63 -1
  139. package/dist/providers/dispatch.js.map +1 -1
  140. package/dist/providers/gemini.js +10 -10
  141. package/dist/providers/gemini.js.map +1 -1
  142. package/dist/providers/index.d.ts +2 -2
  143. package/dist/providers/index.d.ts.map +1 -1
  144. package/dist/providers/index.js +1 -1
  145. package/dist/providers/index.js.map +1 -1
  146. package/dist/providers/ollama.js +15 -15
  147. package/dist/providers/ollama.js.map +1 -1
  148. package/dist/providers/openai.js +22 -22
  149. package/dist/providers/openai.js.map +1 -1
  150. package/dist/providers/types.d.ts +52 -0
  151. package/dist/providers/types.d.ts.map +1 -1
  152. package/dist/providers/types.js.map +1 -1
  153. package/dist/relationship-proof.d.ts +81 -0
  154. package/dist/relationship-proof.d.ts.map +1 -1
  155. package/dist/relationship-proof.js +228 -4
  156. package/dist/relationship-proof.js.map +1 -1
  157. package/dist/research-loop.d.ts.map +1 -1
  158. package/dist/research-loop.js +23 -1
  159. package/dist/research-loop.js.map +1 -1
  160. package/dist/resolved-analytical-plan.d.ts +22 -1
  161. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  162. package/dist/resolved-analytical-plan.js +773 -49
  163. package/dist/resolved-analytical-plan.js.map +1 -1
  164. package/dist/router.d.ts +14 -4
  165. package/dist/router.d.ts.map +1 -1
  166. package/dist/router.js +3668 -425
  167. package/dist/router.js.map +1 -1
  168. package/dist/semantic-bridge/compose.d.ts.map +1 -1
  169. package/dist/semantic-bridge/compose.js +23 -4
  170. package/dist/semantic-bridge/compose.js.map +1 -1
  171. package/dist/semantic-bridge/member-select.d.ts.map +1 -1
  172. package/dist/semantic-bridge/member-select.js +39 -21
  173. package/dist/semantic-bridge/member-select.js.map +1 -1
  174. package/package.json +5 -5
@@ -30,12 +30,12 @@ import { certifiedFitAllowsTier1, certifiedTerminationVerdict, evaluateCertified
30
30
  import { buildGovernedMetricFirstSql, matchSemanticMetric, resolveGovernedMetricDefinition, resolveGovernedMetricSql, semanticMetricEmbeddingOptions } from './metadata/metric-match.js';
31
31
  import { decideAgentAction } from './intent-controller.js';
32
32
  import { createContextLedger } from './grounding/context-ledger.js';
33
- import { validateAnswerResultShape } from './answer-shape.js';
33
+ import { validateAnswerResultShape, } from './answer-shape.js';
34
34
  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';
@@ -384,6 +384,142 @@ function certifiedInvocationInputs(block, plan) {
384
384
  }
385
385
  return Object.keys(parameters).length > 0 ? { parameters, parameterSources } : {};
386
386
  }
387
+ /**
388
+ * Return only output aliases that the selected certified artifact itself
389
+ * declares. This is deliberately built after the router has frozen the plan:
390
+ * retrieval context, tags, examples, and sibling metrics never enter the
391
+ * result-shape proof.
392
+ *
393
+ * A block may express an entity role (`customer`) through a readable label
394
+ * column (`customer_name`), or a declared dimension role through a physical
395
+ * projection alias (`product_type`). Those are valid execution contracts only
396
+ * when the selected block owns both sides of the binding.
397
+ */
398
+ function frozenCertifiedResultShapeBindings(plan, block, questionPlan, options = {}) {
399
+ if (!plan || block.kind !== 'block')
400
+ return [];
401
+ const canonical = (value) => value
402
+ .toLowerCase()
403
+ .replace(/[_\-./]+/g, ' ')
404
+ .replace(/[^a-z0-9 ]+/g, ' ')
405
+ .split(/\s+/)
406
+ .filter(Boolean)
407
+ .map((token) => token.endsWith('ies') && token.length > 4
408
+ ? `${token.slice(0, -3)}y`
409
+ : token.endsWith('s') && token.length > 3
410
+ ? token.slice(0, -1)
411
+ : token)
412
+ .join('_');
413
+ const declared = (block.outputContract?.length
414
+ ? block.outputContract.map((output) => ({ name: output.name, role: output.role }))
415
+ : (block.declaredOutputs ?? []).map((name) => ({ name })))
416
+ .filter((output) => Boolean(output.name?.trim()));
417
+ const declaredByCanonical = new Map(declared.map((output) => [canonical(output.name), output]));
418
+ const bindings = [];
419
+ const add = (binding) => {
420
+ const key = `${binding.role}\0${canonical(binding.requested)}\0${canonical(binding.output)}`;
421
+ const existing = bindings.find((candidate) => `${candidate.role}\0${canonical(candidate.requested)}\0${canonical(candidate.output)}` === key);
422
+ if (existing) {
423
+ const aliases = [...(existing.aliases ?? []), ...(binding.aliases ?? [])]
424
+ .map((alias) => alias.trim())
425
+ .filter(Boolean);
426
+ if (aliases.length > 0)
427
+ existing.aliases = Array.from(new Set(aliases));
428
+ return;
429
+ }
430
+ bindings.push(binding);
431
+ };
432
+ // Measures are only admitted when the frozen plan retained the block's
433
+ // strict, artifact-local declared output identity. Do not infer an alias
434
+ // from output role here: that would reintroduce lifetime_spend -> revenue.
435
+ for (const measure of plan.query.measures) {
436
+ if (!measure.outputName)
437
+ continue;
438
+ const output = declaredByCanonical.get(canonical(measure.outputName));
439
+ if (output)
440
+ add({ requested: measure.requested, output: output.name, role: 'measure' });
441
+ }
442
+ const requestedDimensions = Array.from(new Set([
443
+ ...plan.query.dimensions.map((binding) => binding.requested),
444
+ ...questionPlan.requestedShape.dimensions,
445
+ ].map((value) => canonical(value)).filter(Boolean)));
446
+ const declaredEntities = new Set((block.entities ?? []).map(canonical).filter(Boolean));
447
+ const declaredDimensions = new Set((block.dimensions ?? []).map(canonical).filter(Boolean));
448
+ const typedDimensionOutputs = declared.filter((output) => output.role === 'dimension');
449
+ for (const requested of requestedDimensions) {
450
+ // The declared entity role can be rendered as a standard display label,
451
+ // e.g. `customer` -> `customer_name`. Require both the authored entity
452
+ // declaration and the output's explicit entity-label shape.
453
+ if (declaredEntities.has(requested)) {
454
+ const entityLabel = declared.find((output) => {
455
+ const outputName = canonical(output.name);
456
+ return output.role === 'entity_label'
457
+ || outputName === `${requested}_name`
458
+ || outputName === `${requested}_label`;
459
+ });
460
+ if (entityLabel)
461
+ add({ requested, output: entityLabel.name, role: 'entity_label' });
462
+ }
463
+ // An exact declared output is an artifact-local business-dimension binding
464
+ // even when an older authored block predates explicit `role = dimension`.
465
+ // This is not a pooled or lexical alias: both the requested business
466
+ // dimension and the selected artifact's own projection must agree.
467
+ const exactDeclaredDimension = declaredByCanonical.get(requested);
468
+ if (declaredDimensions.has(requested) && exactDeclaredDimension) {
469
+ add({ requested, output: exactDeclaredDimension.name, role: 'dimension' });
470
+ // Otherwise a physical output can stand for a business dimension only when
471
+ // the block declares that dimension and has one unambiguous typed
472
+ // dimension projection. Multiple physical outputs require an authored
473
+ // alias rather than a guess.
474
+ }
475
+ else if (declaredDimensions.has(requested) && typedDimensionOutputs.length === 1) {
476
+ add({ requested, output: typedDimensionOutputs[0].name, role: 'dimension' });
477
+ }
478
+ }
479
+ // An exact *unique* authored example can prove that a parser token is a
480
+ // member value of the selected block's already-declared dimension. For
481
+ // example, the block-owned example "revenue by food and drink" returns the
482
+ // declared `category` output; `food` is not an independently requested
483
+ // physical column. Keep this binding intentionally local to the frozen
484
+ // block and its resolved dimension: no tags, descriptions, sibling evidence,
485
+ // or general lexical aliases are admitted here. Measures remain strict.
486
+ if (options.uniqueExactExampleContract === true) {
487
+ const plannedDimensions = new Set(plan.query.dimensions.map((binding) => canonical(binding.requested)));
488
+ const resolvedDimensionBindings = bindings.filter((binding) => binding.role === 'dimension'
489
+ && plannedDimensions.has(canonical(binding.requested))
490
+ && declaredByCanonical.has(canonical(binding.output)));
491
+ if (resolvedDimensionBindings.length === 1) {
492
+ const dimensionBinding = resolvedDimensionBindings[0];
493
+ const protectedTerms = new Set([
494
+ ...questionPlan.requestedShape.measures,
495
+ ...plan.query.measures.map((binding) => binding.requested),
496
+ ...questionPlan.requestedShape.filters,
497
+ ].map(canonical).filter(Boolean));
498
+ const structuralRoles = new Set([
499
+ 'product', 'customer', 'account', 'user', 'member', 'category', 'segment',
500
+ 'region', 'channel', 'order', 'day', 'week', 'month', 'quarter', 'year',
501
+ ]);
502
+ const memberNoise = Array.from(new Set([
503
+ ...questionPlan.requestedShape.dimensions,
504
+ ...questionPlan.requestedShape.requiredOutputs,
505
+ ].map(canonical).filter(Boolean))).filter((token) => token !== canonical(dimensionBinding.requested)
506
+ && !declaredByCanonical.has(token)
507
+ && !declaredEntities.has(token)
508
+ && !declaredDimensions.has(token)
509
+ && !protectedTerms.has(token)
510
+ && !structuralRoles.has(token));
511
+ if (memberNoise.length > 0) {
512
+ add({
513
+ requested: dimensionBinding.requested,
514
+ output: dimensionBinding.output,
515
+ role: 'dimension',
516
+ aliases: memberNoise,
517
+ });
518
+ }
519
+ }
520
+ }
521
+ return bindings;
522
+ }
387
523
  function damerauLevenshteinDistance(left, right) {
388
524
  const rows = left.length + 1;
389
525
  const columns = right.length + 1;
@@ -1025,9 +1161,17 @@ export async function answer(input) {
1025
1161
  const analyticalExecutionGraph = analyticalGraphBuild?.status === 'ready'
1026
1162
  ? analyticalGraphBuild.graph
1027
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));
1028
1171
  const analyticalExecutionGraphFailure = normalizedInput.resolvedAnalyticalPlan?.schemaVersion === 2
1029
1172
  && !multiMetricPlan
1030
1173
  && !generatedProposalPlan
1174
+ && routerFrozenSemanticPlan
1031
1175
  ? analyticalGraphBuild?.status === 'blocked'
1032
1176
  ? {
1033
1177
  code: analyticalGraphBuild.code,
@@ -1112,7 +1256,23 @@ export async function answer(input) {
1112
1256
  }
1113
1257
  : compiledInput;
1114
1258
  const loopResult = await runAnswerLoop(executionInput);
1115
- 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
+ }
1116
1276
  // Attach the canonical trust label once, at the single exit point, so every
1117
1277
  // return site inside runAnswerLoop stays untouched and backward compatible.
1118
1278
  // Freshness-aware trust: for a certified answer, fold the source block's data
@@ -1120,7 +1280,7 @@ export async function answer(input) {
1120
1280
  // stale data" / "Certified · upstream failed". Non-certified or fresh answers
1121
1281
  // are unaffected.
1122
1282
  const { _semanticMetricMatch, ...publicResult } = result;
1123
- const chosenRoute = result.route ?? deriveAiRoute(result, _semanticMetricMatch);
1283
+ const chosenRoute = frozenSemanticRoute ?? result.route ?? deriveAiRoute(result, _semanticMetricMatch);
1124
1284
  // P0 — record the high-level action this turn warranted, so callers can route
1125
1285
  // (compose_app → app build, investigate → research) and the UI can show the
1126
1286
  // agent's reasoning. Computed once at the single exit from the finished answer.
@@ -1201,6 +1361,20 @@ function cascadeExecutionStatus(result) {
1201
1361
  */
1202
1362
  function deriveAiRoute(result, metricMatch) {
1203
1363
  if (result.kind === 'no_answer') {
1364
+ // Preserve the frozen certified tier as provenance when that exact block
1365
+ // executed but failed its own result contract. The route is not a trust
1366
+ // badge: `certification` remains review-required and no generated fallback
1367
+ // may be selected after freeze.
1368
+ if (result.certifiedResultShapeFailure && result.sourceTier === 'certified_artifact') {
1369
+ const ref = result.sourceCertifiedBlock ?? result.block?.name ?? result.citations[0]?.name;
1370
+ return {
1371
+ tier: 'certified_block',
1372
+ label: ref
1373
+ ? `Certified block ${ref} did not return the complete requested output shape.`
1374
+ : 'The selected certified block did not return the complete requested output shape.',
1375
+ ref,
1376
+ };
1377
+ }
1204
1378
  if (result.exploratoryCandidate) {
1205
1379
  return {
1206
1380
  tier: 'no_answer',
@@ -1240,6 +1414,69 @@ function deriveAiRoute(result, metricMatch) {
1240
1414
  ? { tier: 'generated_sql', label: 'Prepared review-required DQL artifact with SQL preview.' }
1241
1415
  : { tier: 'generated_sql', label: 'Prepared review-required SQL preview.' };
1242
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
+ }
1243
1480
  /** "customers_customer_name" → "customers" for a question the user has to read. */
1244
1481
  function humanizeDeicticDimension(dimension) {
1245
1482
  const leaf = dimension.split(/[.:]/).pop() ?? dimension;
@@ -1264,6 +1501,18 @@ async function runAnswerLoop(input) {
1264
1501
  const scopedContextPack = questionDomainScope.length > 0
1265
1502
  ? scopeContextPackToQuestionDomains(input.contextPack, questionDomainScope, input.manifest)
1266
1503
  : input.contextPack;
1504
+ // The router has already chosen the physical exploratory closure. Treat it
1505
+ // as an execution authority boundary here, before any prompt is constructed
1506
+ // or provider is dispatched. Full retrieval stays on `input.contextPack`
1507
+ // for receipts and diagnostics only.
1508
+ const forcedExploratoryTier = input.selectedCascadeTier === 'exploratory_sql';
1509
+ const frozenSemanticRoute = frozenSemanticRouteForInput(input);
1510
+ const exploratoryClosureContextPack = forcedExploratoryTier
1511
+ ? scopeContextPackToExploratoryCandidateClosure(scopedContextPack, input.exploratoryCandidateIds)
1512
+ : undefined;
1513
+ const executionContextPack = forcedExploratoryTier
1514
+ ? exploratoryClosureContextPack
1515
+ : scopedContextPack;
1267
1516
  // Select the RELEVANT skills (not all) for this question; keep pinned project
1268
1517
  // skills (SQL conventions). Block hints still come from the full set so a
1269
1518
  // preferred-block mapping is never lost.
@@ -1305,7 +1554,7 @@ async function runAnswerLoop(input) {
1305
1554
  const semanticHits = kg.search({ query: question, ...searchScope, kinds: SEMANTIC_KINDS, limit: 12 });
1306
1555
  const manifestHits = kg.search({ query: question, ...searchScope, kinds: MANIFEST_KINDS, limit: 12 });
1307
1556
  const considered = mergeHits(artifactHits, semanticHits, manifestHits, kg.search({ query: question, domain, limit: 10 })).slice(0, 30);
1308
- const schemaContext = schemaContextWithAllowedSqlContext(schemaContextWithinQuestionScope(input.schemaContext ?? [], input.contextPack, scopedContextPack), scopedContextPack);
1557
+ const schemaContext = schemaContextWithAllowedSqlContext(schemaContextWithinQuestionScope(input.schemaContext ?? [], input.contextPack, executionContextPack), executionContextPack);
1309
1558
  const catalogRoute = input.contextPack?.routeDecision;
1310
1559
  const baseQuestionPlan = input.contextPack?.questionPlan?.requestedShape
1311
1560
  ? input.contextPack.questionPlan
@@ -1321,12 +1570,60 @@ async function runAnswerLoop(input) {
1321
1570
  // "exact certified" route into the generation prompt. The model should decide
1322
1571
  // from compatible certified evidence, semantic members, and dbt/runtime
1323
1572
  // columns—not copy a customer-grain worked example into a product-grain ask.
1324
- const promptContextPack = contextPackForRequestedShape(scopedContextPack, question, questionPlan, kg);
1573
+ const promptContextPack = contextPackForRequestedShape(executionContextPack, question, questionPlan, kg);
1325
1574
  const repairBudgetState = createCascadeBudgetState(input.cascadeBudgetModel);
1326
- const authoritativePlanBinding = input.resolvedAnalyticalPlan?.mode === 'authoritative'
1575
+ // A pre-freeze exploratory selection is already the router's result of
1576
+ // evaluating certified → semantic → relational eligibility. Do not let the
1577
+ // legacy answer loop independently rediscover a broad block or metric and
1578
+ // replace that decision before it has attempted the selected bounded SQL
1579
+ // path. Frozen tiers retain their existing plan binding behavior.
1580
+ if (forcedExploratoryTier && input.contextPack && !exploratoryClosureContextPack) {
1581
+ const text = 'The router-selected exploratory path no longer has a complete same-snapshot physical relation closure, so DQL did not send SQL generation or execute a query.';
1582
+ return {
1583
+ kind: 'no_answer',
1584
+ sourceTier: 'no_answer',
1585
+ certification: 'analyst_review_required',
1586
+ reviewStatus: 'none',
1587
+ confidence: 0,
1588
+ text,
1589
+ answer: text,
1590
+ refusalCode: 'grounding_gap',
1591
+ refusalDetails: { code: 'grounding_gap', message: text },
1592
+ citations: contextPackCitations(input.contextPack, 8),
1593
+ contextPack: input.contextPack,
1594
+ considered,
1595
+ providerUsed: provider.name,
1596
+ };
1597
+ }
1598
+ const authoritativePlanBinding = !forcedExploratoryTier && input.resolvedAnalyticalPlan?.mode === 'authoritative'
1327
1599
  ? input.resolvedPlanExecutionBinding
1328
1600
  : undefined;
1329
- if (input.analyticalPeriodResolutionFailure) {
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
+ }
1626
+ if (!forcedExploratoryTier && input.analyticalPeriodResolutionFailure) {
1330
1627
  const failure = input.analyticalPeriodResolutionFailure;
1331
1628
  const structuredFailureCode = failure.error && typeof failure.error === 'object'
1332
1629
  && typeof failure.error.code === 'string'
@@ -1362,7 +1659,7 @@ async function runAnswerLoop(input) {
1362
1659
  providerUsed: provider.name,
1363
1660
  };
1364
1661
  }
1365
- if (input.analyticalExecutionGraphFailure) {
1662
+ if (!forcedExploratoryTier && input.analyticalExecutionGraphFailure) {
1366
1663
  const failure = input.analyticalExecutionGraphFailure;
1367
1664
  const analyticalFailure = analyticalFailureForInput(input, {
1368
1665
  error: { code: failure.code, message: failure.reason },
@@ -1387,7 +1684,7 @@ async function runAnswerLoop(input) {
1387
1684
  providerUsed: provider.name,
1388
1685
  };
1389
1686
  }
1390
- if (input.semanticGraphExecutionBinding?.status === 'blocked') {
1687
+ if (!forcedExploratoryTier && input.semanticGraphExecutionBinding?.status === 'blocked') {
1391
1688
  const failure = input.semanticGraphExecutionBinding;
1392
1689
  const analyticalFailure = analyticalFailureForInput(input, {
1393
1690
  error: { code: failure.code, message: failure.reason },
@@ -1412,17 +1709,21 @@ async function runAnswerLoop(input) {
1412
1709
  providerUsed: provider.name,
1413
1710
  };
1414
1711
  }
1415
- if (input.semanticGraphExecutionBinding?.status === 'ready' && input.analyticalExecutionGraph) {
1712
+ if (!forcedExploratoryTier && input.semanticGraphExecutionBinding?.status === 'ready' && input.analyticalExecutionGraph) {
1416
1713
  return executeSemanticAnalyticalGraph({
1417
1714
  input,
1418
1715
  binding: input.semanticGraphExecutionBinding,
1419
1716
  graph: input.analyticalExecutionGraph,
1420
- 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,
1421
1722
  considered,
1422
1723
  providerName: provider.name,
1423
1724
  });
1424
1725
  }
1425
- if (input.governedAnalyticalGraphCompilation?.status === 'blocked') {
1726
+ if (!forcedExploratoryTier && input.governedAnalyticalGraphCompilation?.status === 'blocked') {
1426
1727
  const failure = input.governedAnalyticalGraphCompilation;
1427
1728
  const analyticalFailure = analyticalFailureForInput(input, {
1428
1729
  error: { code: failure.code, message: failure.reason },
@@ -1447,7 +1748,7 @@ async function runAnswerLoop(input) {
1447
1748
  providerUsed: provider.name,
1448
1749
  };
1449
1750
  }
1450
- if (input.governedAnalyticalGraphCompilation?.status === 'compiled' && input.analyticalExecutionGraph) {
1751
+ if (!forcedExploratoryTier && input.governedAnalyticalGraphCompilation?.status === 'compiled' && input.analyticalExecutionGraph) {
1451
1752
  return executeGovernedRelationalAnalyticalGraph({
1452
1753
  input,
1453
1754
  compilation: input.governedAnalyticalGraphCompilation,
@@ -1458,7 +1759,7 @@ async function runAnswerLoop(input) {
1458
1759
  });
1459
1760
  }
1460
1761
  const governedRelationalCompilation = input.governedRelationalCompilation;
1461
- if (governedRelationalCompilation?.status === 'blocked') {
1762
+ if (!forcedExploratoryTier && governedRelationalCompilation?.status === 'blocked') {
1462
1763
  const analyticalFailure = analyticalFailureForInput(input, {
1463
1764
  error: { code: governedRelationalCompilation.code, message: governedRelationalCompilation.reason },
1464
1765
  phase: 'compilation',
@@ -1485,7 +1786,7 @@ async function runAnswerLoop(input) {
1485
1786
  providerUsed: provider.name,
1486
1787
  };
1487
1788
  }
1488
- if (governedRelationalCompilation?.status === 'compiled') {
1789
+ if (!forcedExploratoryTier && governedRelationalCompilation?.status === 'compiled') {
1489
1790
  const dqlArtifact = renderGovernedRelationalDqlArtifact(governedRelationalCompilation);
1490
1791
  let result;
1491
1792
  let executionError;
@@ -1532,7 +1833,7 @@ async function runAnswerLoop(input) {
1532
1833
  providerUsed: provider.name,
1533
1834
  };
1534
1835
  }
1535
- if (authoritativePlanBinding?.status === 'blocked') {
1836
+ if (authoritativePlanBinding?.status === 'blocked' && !legacyDirectSemanticPlan) {
1536
1837
  // An ambiguous singular reference is answerable the moment the user says
1537
1838
  // which one they meant, so offer the candidates rather than dead-ending.
1538
1839
  // The old refusal shipped the binder's internal sentence as the answer
@@ -1549,9 +1850,15 @@ async function runAnswerLoop(input) {
1549
1850
  const text = clarificationOptions?.length
1550
1851
  ? `The previous answer listed ${choices.values.length} ${humanizeDeicticDimension(choices.dimension)}. Which one did you mean?`
1551
1852
  : `The resolved analytical plan cannot execute safely: ${authoritativePlanBinding.reason}`;
1853
+ const frozenCertifiedPlan = input.resolvedAnalyticalPlan?.mode === 'authoritative'
1854
+ && input.resolvedAnalyticalPlan.capability === 'certified_execution';
1855
+ const frozenCertifiedRef = input.resolvedAnalyticalPlan?.executionId;
1552
1856
  return {
1553
1857
  kind: 'no_answer',
1554
- sourceTier: 'no_answer',
1858
+ // A binding/adapter failure after the router selected a certified
1859
+ // execution authority is still a certified-tier terminal. It must not
1860
+ // be relabelled as generated merely because no rows were produced.
1861
+ sourceTier: frozenCertifiedPlan ? 'certified_artifact' : 'no_answer',
1555
1862
  certification: 'analyst_review_required',
1556
1863
  reviewStatus: 'none',
1557
1864
  confidence: 0,
@@ -1563,6 +1870,15 @@ async function runAnswerLoop(input) {
1563
1870
  : 'grounding_gap',
1564
1871
  refusalDetails: { code: authoritativePlanBinding.code, message: authoritativePlanBinding.reason },
1565
1872
  ...(clarificationOptions?.length ? { clarificationOptions } : {}),
1873
+ ...(frozenCertifiedPlan ? {
1874
+ route: {
1875
+ tier: 'certified_block',
1876
+ label: frozenCertifiedRef
1877
+ ? `The selected certified block ${frozenCertifiedRef} could not execute its frozen plan.`
1878
+ : 'The selected certified block could not execute its frozen plan.',
1879
+ ...(frozenCertifiedRef ? { ref: frozenCertifiedRef } : {}),
1880
+ },
1881
+ } : {}),
1566
1882
  citations: contextPackCitations(input.contextPack, 8),
1567
1883
  considered,
1568
1884
  contextPack: input.contextPack,
@@ -1593,24 +1909,26 @@ async function runAnswerLoop(input) {
1593
1909
  ? authoritativePlanBinding
1594
1910
  : undefined;
1595
1911
  const preferredSemanticMetric = authoritativeSemanticBinding?.metricNode
1596
- ?? (input.resolvedAnalyticalPlan?.mode === 'authoritative'
1912
+ ?? (input.resolvedAnalyticalPlan?.mode === 'authoritative' && !legacyDirectSemanticPlan
1597
1913
  ? undefined
1598
1914
  : resolvePreferredSemanticMetric([input.preferredExecutionId, ...(input.preferredEvidenceIds ?? [])], semanticMetricNodes, kg));
1599
1915
  const semanticLayerForExec = input.semanticLayer;
1600
1916
  const canExecuteSemanticMetricForMatch = input.canExecuteSemanticMetric
1601
1917
  ?? (semanticLayerForExec ? (name) => semanticLayerForExec.canComposeMetric(name) : undefined);
1602
- let semanticMetricMatch = preferredSemanticMetric
1603
- ? { metric: preferredSemanticMetric, score: 1, basis: 'name' }
1604
- : input.resolvedAnalyticalPlan?.mode === 'authoritative'
1605
- ? null
1606
- : await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
1607
- measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
1608
- ...(canExecuteSemanticMetricForMatch ? { canExecute: canExecuteSemanticMetricForMatch } : {}),
1609
- ...(snapshotVectorMetricShortlist(input.contextPack).length
1610
- ? { vectorMetricShortlist: snapshotVectorMetricShortlist(input.contextPack) }
1611
- : {}),
1612
- ...semanticMetricEmbeddingOptions(input.embeddingProvider),
1613
- }).catch(() => null);
1918
+ let semanticMetricMatch = forcedExploratoryTier
1919
+ ? null
1920
+ : preferredSemanticMetric
1921
+ ? { metric: preferredSemanticMetric, score: 1, basis: 'name' }
1922
+ : input.resolvedAnalyticalPlan?.mode === 'authoritative' && !legacyDirectSemanticPlan
1923
+ ? null
1924
+ : await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
1925
+ measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
1926
+ ...(canExecuteSemanticMetricForMatch ? { canExecute: canExecuteSemanticMetricForMatch } : {}),
1927
+ ...(snapshotVectorMetricShortlist(input.contextPack).length
1928
+ ? { vectorMetricShortlist: snapshotVectorMetricShortlist(input.contextPack) }
1929
+ : {}),
1930
+ ...semanticMetricEmbeddingOptions(input.embeddingProvider),
1931
+ }).catch(() => null);
1614
1932
  // Stage 1: certified artifact match. Blocks can be executed; dashboards,
1615
1933
  // Apps, and notebooks are returned as governed citations/navigation targets.
1616
1934
  const drilldownCertifiedHit = input.followUp?.kind === 'drilldown'
@@ -1630,8 +1948,10 @@ async function runAnswerLoop(input) {
1630
1948
  && objectNameInQuestion(question, node)
1631
1949
  && /\b(run|use|open|show|execute|certified|saved|block)\b/i.test(question));
1632
1950
  });
1633
- const shouldTryCertifiedRoute = shouldUseCertifiedRoute(catalogRoute, intent)
1634
- || explicitlyRequestedCertifiedBlock;
1951
+ const shouldTryCertifiedRoute = !forcedExploratoryTier
1952
+ && (shouldUseCertifiedRoute(catalogRoute, intent)
1953
+ || questionPlan.mode === 'definition'
1954
+ || explicitlyRequestedCertifiedBlock);
1635
1955
  const catalogCertifiedHit = shouldTryCertifiedRoute
1636
1956
  ? certifiedHitFromContextPack(input.contextPack, kg)
1637
1957
  : null;
@@ -1641,7 +1961,9 @@ async function runAnswerLoop(input) {
1641
1961
  // metric, so a high-scoring `top_beverage_customers` catalog hit could answer a
1642
1962
  // product-type → product-name flow request with customer rows.
1643
1963
  const unsafeCatalogCertifiedHit = catalogCertifiedHit?.node.kind === 'block'
1644
- && !hasCertifiedNodeFit(question, questionPlan, catalogCertifiedHit.node)
1964
+ && !hasCertifiedNodeFit(question, questionPlan, catalogCertifiedHit.node, {
1965
+ uniqueExactExampleContract: hasUniqueExactCatalogExample(question, input.contextPack, catalogCertifiedHit.node),
1966
+ })
1645
1967
  ? null
1646
1968
  : catalogCertifiedHit;
1647
1969
  const fallbackCertifiedHit = shouldTryCertifiedRoute ? pickCertifiedArtifact({
@@ -1658,13 +1980,85 @@ async function runAnswerLoop(input) {
1658
1980
  && authoritativePlanBinding.kind === 'certified'
1659
1981
  ? authoritativePlanBinding
1660
1982
  : undefined;
1983
+ const namedDefinitionArtifact = input.resolvedAnalyticalPlan?.mode === 'authoritative'
1984
+ ? null
1985
+ : uniqueNamedCertifiedDefinitionArtifact(question, kg, authorizedDomains);
1661
1986
  let artifactHit = authoritativeCertifiedBinding
1662
1987
  ? { node: authoritativeCertifiedBinding.node, score: 1 }
1663
1988
  : input.resolvedAnalyticalPlan?.mode === 'authoritative'
1664
1989
  ? null
1665
- : drilldownCertifiedHit ?? unsafeCatalogCertifiedHit
1990
+ : namedDefinitionArtifact ?? drilldownCertifiedHit ?? unsafeCatalogCertifiedHit
1666
1991
  ?? (catalogCertifiedHit ? null : fallbackCertifiedHit);
1667
1992
  let certifiedExecutionFallback;
1993
+ // A named certified artifact-definition request is documentation about that
1994
+ // artifact, not a request to execute its SQL. The planner strips the
1995
+ // artifact identifier from its output contract, and this deterministic path
1996
+ // returns only authored metadata. It is deliberately narrower than a bare
1997
+ // "what is revenue" question: the user must name the certified block and use
1998
+ // an explicit definition form before execution is bypassed.
1999
+ if (artifactHit?.node.kind === 'block'
2000
+ && artifactHit.node.status === 'certified'
2001
+ && isUniqueNamedCertifiedDefinitionQuestion(question, artifactHit.node, kg, authorizedDomains)) {
2002
+ const artifact = artifactHit.node;
2003
+ const outputs = [...new Set([
2004
+ ...(artifact.outputContract ?? []).map((output) => output.name),
2005
+ ...(artifact.declaredOutputs ?? []),
2006
+ ...(artifact.outputs ?? []).map((output) => output.name),
2007
+ ].map((output) => output.trim()).filter(Boolean))].slice(0, 16);
2008
+ const metadataLines = [
2009
+ artifact.description ?? artifact.llmContext ?? 'No authored description is available for this certified artifact.',
2010
+ outputs.length > 0 ? `Declared outputs: ${outputs.join(', ')}.` : undefined,
2011
+ artifact.grain ? `Grain: ${artifact.grain}.` : undefined,
2012
+ artifact.owner ? `Owner: ${artifact.owner}.` : undefined,
2013
+ 'This is certified artifact metadata, not a query result.',
2014
+ ].filter((line) => Boolean(line));
2015
+ const citations = [{
2016
+ nodeId: artifact.nodeId,
2017
+ kind: artifact.kind,
2018
+ name: artifact.name,
2019
+ gitSha: artifact.gitSha,
2020
+ sourceTier: 'certified_artifact',
2021
+ provenance: artifact.provenance,
2022
+ }];
2023
+ const analysisPlan = buildAnalysisPlan({
2024
+ question,
2025
+ intent: 'definition_lookup',
2026
+ routeReason: 'The question explicitly requested the meaning of one named certified artifact; no SQL execution was requested.',
2027
+ selectedNodes: [artifact],
2028
+ schemaContext,
2029
+ });
2030
+ const text = `Certified artifact **${artifact.name}**\n\n${metadataLines.join('\n\n')}`;
2031
+ return {
2032
+ kind: 'certified',
2033
+ sourceTier: 'certified_artifact',
2034
+ certification: 'certified',
2035
+ reviewStatus: 'certified',
2036
+ confidence: 0.99,
2037
+ text,
2038
+ answer: text,
2039
+ block: artifact,
2040
+ sourceCertifiedBlock: artifact.name,
2041
+ trustLabel: 'certified',
2042
+ citations,
2043
+ memoryContext: input.memoryContext,
2044
+ analysisPlan,
2045
+ evidence: buildCertifiedEvidence({
2046
+ question,
2047
+ artifact,
2048
+ businessHits,
2049
+ semanticHits,
2050
+ manifestHits,
2051
+ considered,
2052
+ executorWasAvailable: false,
2053
+ citations,
2054
+ memoryContext: input.memoryContext ?? [],
2055
+ analysisPlan,
2056
+ }),
2057
+ contextPack: input.contextPack,
2058
+ considered,
2059
+ providerUsed: provider.name,
2060
+ };
2061
+ }
1668
2062
  // Certified remains first when it actually covers the question. If the
1669
2063
  // retrieved block does not fit but a governed semantic metric does, never
1670
2064
  // let the broad catalog match pre-empt Lane 2.
@@ -1704,6 +2098,7 @@ async function runAnswerLoop(input) {
1704
2098
  if (artifactHit && businessContextTerminal) {
1705
2099
  let result;
1706
2100
  let executionError;
2101
+ let executionFailureDetail;
1707
2102
  if (artifactHit.node.kind === 'block' && input.executeCertifiedBlock) {
1708
2103
  try {
1709
2104
  result = await input.executeCertifiedBlock(artifactHit.node, {
@@ -1716,6 +2111,7 @@ async function runAnswerLoop(input) {
1716
2111
  result = trimResultToRequestedTopN(result, questionPlan);
1717
2112
  }
1718
2113
  catch (err) {
2114
+ executionFailureDetail = analyticalErrorDetail(err);
1719
2115
  executionError = err instanceof Error ? err.message : String(err);
1720
2116
  }
1721
2117
  }
@@ -1773,7 +2169,16 @@ async function runAnswerLoop(input) {
1773
2169
  providerUsed: provider.name,
1774
2170
  };
1775
2171
  }
1776
- const resultShapeWarnings = result ? validateAnswerResultShape(questionPlan, result).warnings : [];
2172
+ const resultShapeWarnings = result
2173
+ ? validateAnswerResultShape(questionPlan, result, {
2174
+ outputBindings: authoritativeCertifiedBinding
2175
+ ? frozenCertifiedResultShapeBindings(input.resolvedAnalyticalPlan, artifactHit.node, questionPlan, {
2176
+ uniqueExactExampleContract: hasUniqueExactCatalogExample(question, input.contextPack, artifactHit.node),
2177
+ })
2178
+ : [],
2179
+ requireBoundMeasures: Boolean(authoritativeCertifiedBinding),
2180
+ }).warnings
2181
+ : [];
1777
2182
  // When a certified block's execution was ATTEMPTED and FAILED, the answer
1778
2183
  // cannot wear the certified badge — a failed run has no data to stand behind.
1779
2184
  // Downgrade to analyst_review_required (the error is surfaced in the text).
@@ -1812,6 +2217,13 @@ async function runAnswerLoop(input) {
1812
2217
  ? undefined
1813
2218
  : questionPlan.requestedShape.topN?.n);
1814
2219
  const authoritativeCertifiedFailure = Boolean(authoritativeCertifiedBinding && executionError);
2220
+ const observabilityExecutionFailure = authoritativeCertifiedFailure
2221
+ ? observabilityFailureForFrozenExecution(executionFailureDetail)
2222
+ : undefined;
2223
+ // A selected certified block which returns an incomplete tuple is a
2224
+ // same-tier terminal failure. It is not a reason to reinterpret meaning,
2225
+ // label the partial rows generated, or retry another route after freeze.
2226
+ const authoritativeCertifiedShapeFailure = Boolean(authoritativeCertifiedBinding && !executionError && resultShapeWarnings.length > 0);
1815
2227
  const recoverableCertifiedFailure = artifactHit.node.kind === 'block'
1816
2228
  && executionError !== undefined
1817
2229
  && !authoritativeCertifiedFailure
@@ -1826,18 +2238,39 @@ async function runAnswerLoop(input) {
1826
2238
  artifactHit = null;
1827
2239
  }
1828
2240
  else {
2241
+ const frozenCertifiedRoute = authoritativeCertifiedBinding
2242
+ ? {
2243
+ tier: 'certified_block',
2244
+ label: certifiedShapePassed
2245
+ ? `Answered from certified block ${artifactHit.node.name}`
2246
+ : `The selected certified block ${artifactHit.node.name} could not complete its frozen output contract.`,
2247
+ ref: artifactHit.node.name,
2248
+ }
2249
+ : undefined;
1829
2250
  return {
1830
- kind: authoritativeCertifiedFailure ? 'no_answer' : certifiedShapePassed ? 'certified' : 'uncertified',
1831
- sourceTier: authoritativeCertifiedFailure ? 'no_answer' : sourceTier,
2251
+ kind: authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
2252
+ ? 'no_answer'
2253
+ : certifiedShapePassed ? 'certified' : 'uncertified',
2254
+ // Preserve the selected certified authority even when its own
2255
+ // execution/result contract fails. The failure is terminal and has
2256
+ // review-required trust, but it is not generated SQL.
2257
+ sourceTier,
1832
2258
  certification: certifiedShapePassed ? 'certified' : 'analyst_review_required',
1833
- reviewStatus: authoritativeCertifiedFailure ? 'none' : certifiedShapePassed ? 'certified' : 'analyst_review_required',
2259
+ reviewStatus: authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
2260
+ ? 'none'
2261
+ : certifiedShapePassed ? 'certified' : 'analyst_review_required',
1834
2262
  confidence: certifiedShapePassed ? 0.95 : 0.45,
1835
2263
  text,
1836
2264
  answer: text,
1837
2265
  block: artifactHit.node.kind === 'block' ? artifactHit.node : undefined,
1838
2266
  result,
1839
2267
  executionError,
1840
- ...(authoritativeCertifiedFailure ? { refusalCode: 'grounding_gap' } : {}),
2268
+ ...(observabilityExecutionFailure ? { observabilityExecutionFailure } : {}),
2269
+ ...(authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
2270
+ ? { refusalCode: 'grounding_gap' }
2271
+ : {}),
2272
+ ...(authoritativeCertifiedShapeFailure ? { certifiedResultShapeFailure: true } : {}),
2273
+ ...(frozenCertifiedRoute ? { route: frozenCertifiedRoute } : {}),
1841
2274
  sql: result?.sql,
1842
2275
  dqlArtifact,
1843
2276
  trustLabel: certifiedShapePassed ? input.contextPack?.trustLabel ?? 'certified' : 'mixed',
@@ -1875,7 +2308,7 @@ async function runAnswerLoop(input) {
1875
2308
  // a governed metric can answer. Match by name + synonyms + measure family + hybrid
1876
2309
  // rank over the FTS semantic hits, then ALL metric KG nodes (revenue ⇄
1877
2310
  // cumulative_revenue). Certified-first is still preserved (checked above).
1878
- const clarifyBeforeGeneration = shouldClarifyBeforeGeneration({
2311
+ const clarifyBeforeGeneration = !forcedExploratoryTier && shouldClarifyBeforeGeneration({
1879
2312
  intent,
1880
2313
  routeDecision: catalogRoute,
1881
2314
  hasSemanticMetricMatch: Boolean(semanticMetricMatch),
@@ -1933,12 +2366,14 @@ async function runAnswerLoop(input) {
1933
2366
  // wins over raw dbt manifest context; memory is appended last as advisory.
1934
2367
  // A confident metric match forces the semantic tier even when FTS returned no
1935
2368
  // semantic hits, so the governed metric (not refusal) answers the question.
1936
- const activeTier = sourceTierFromContextPack(input.contextPack)
1937
- ?? (semanticHits.length > 0 || semanticMetricMatch
1938
- ? 'semantic_layer'
1939
- : manifestHits.length > 0
1940
- ? 'dbt_manifest'
1941
- : 'dbt_manifest');
2369
+ const activeTier = forcedExploratoryTier
2370
+ ? 'dbt_manifest'
2371
+ : sourceTierFromContextPack(input.contextPack)
2372
+ ?? (semanticHits.length > 0 || semanticMetricMatch
2373
+ ? 'semantic_layer'
2374
+ : manifestHits.length > 0
2375
+ ? 'dbt_manifest'
2376
+ : 'dbt_manifest');
1942
2377
  const reviewRequiredArtifactHits = artifactHits
1943
2378
  .filter((hit) => hit.score >= CERTIFIED_HIT_THRESHOLD && !isCertifiedHit(hit, kg))
1944
2379
  .slice(0, 4);
@@ -1971,7 +2406,13 @@ async function runAnswerLoop(input) {
1971
2406
  { hits: businessHits.slice(0, 4), reserve: 3 },
1972
2407
  { hits: manifestHits, reserve: 3 },
1973
2408
  ], 14);
1974
- const contextNodes = mergeNodes(followUpSourceBlock && input.followUp?.kind === 'drilldown' ? [followUpSourceBlock] : [], (contextHits.length > 0 ? contextHits : considered.slice(0, 6)).map((h) => h.node)).filter((node) => questionDomainScope.length === 0 || !node.domain || questionDomainScope.includes(node.domain));
2409
+ // A frozen exploratory selection gets its relation/column/proof context only
2410
+ // from `executionContextPack`. KG hit prose is useful diagnostics, but it
2411
+ // is not execution authority and must not tell a provider about another
2412
+ // same-snapshot relation it could then attempt to query.
2413
+ const contextNodes = forcedExploratoryTier
2414
+ ? []
2415
+ : mergeNodes(followUpSourceBlock && input.followUp?.kind === 'drilldown' ? [followUpSourceBlock] : [], (contextHits.length > 0 ? contextHits : considered.slice(0, 6)).map((h) => h.node)).filter((node) => questionDomainScope.length === 0 || !node.domain || questionDomainScope.includes(node.domain));
1975
2416
  const kgJoinPathHints = buildKgJoinPathHints(kg, contextNodes, questionPlan);
1976
2417
  const contextBlocks = contextNodes.filter((node) => {
1977
2418
  if (node.kind !== 'block')
@@ -1991,10 +2432,10 @@ async function runAnswerLoop(input) {
1991
2432
  const messages = [
1992
2433
  { role: 'system', content: SYSTEM_PROMPT },
1993
2434
  ];
1994
- const skillsPrompt = buildSkillsPrompt(selectedSkills, userId ?? null);
2435
+ const skillsPrompt = forcedExploratoryTier ? '' : buildSkillsPrompt(selectedSkills, userId ?? null);
1995
2436
  if (skillsPrompt)
1996
2437
  messages.push({ role: 'system', content: skillsPrompt });
1997
- const analyticalPlan = input.manifest
2438
+ const analyticalPlan = !forcedExploratoryTier && input.manifest
1998
2439
  ? planAnalyticalPath(input.manifest, {
1999
2440
  entityIds: inferAnalyticalEntityIds(question, contextNodes, input.manifest),
2000
2441
  ownerDomain: input.domainContext?.activeDomain ?? input.domain,
@@ -2319,8 +2760,21 @@ async function runAnswerLoop(input) {
2319
2760
  contextPack: input.contextPack,
2320
2761
  considered,
2321
2762
  providerUsed: provider.name,
2763
+ ...(frozenSemanticRoute ? { route: frozenSemanticRoute } : {}),
2322
2764
  };
2323
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
+ }
2324
2778
  const metricFirst = semanticMetricMatch
2325
2779
  ? buildGovernedMetricFirstSql({
2326
2780
  metric: semanticMetricMatch.metric,
@@ -2346,7 +2800,7 @@ async function runAnswerLoop(input) {
2346
2800
  // (pre-validated, since the wrapper only restricts an already-certified result)
2347
2801
  // and is labeled BELOW certified. Falls through to generation on any miss.
2348
2802
  let certifiedAdaptation;
2349
- if (!semanticBridgeAnswer && !metricFirst && input.executeGeneratedSql) {
2803
+ if (!forcedExploratoryTier && !semanticBridgeAnswer && !metricFirst && input.executeGeneratedSql) {
2350
2804
  const fit = input.contextPack?.routeDecision?.blockFit;
2351
2805
  const sourceBlock = input.contextPack?.allowedSqlContext?.sourceBlockSql?.[0];
2352
2806
  if (fit && sourceBlock?.sql) {
@@ -2652,14 +3106,36 @@ async function runAnswerLoop(input) {
2652
3106
  || (input.contextPack?.allowedSqlContext?.relations.length ?? 0) > 0
2653
3107
  || (input.contextPack?.allowedSqlContext?.sourceBlockSql.length ?? 0) > 0
2654
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);
2655
3117
  if (!parsed.sql && !governedMetricAnswer && wantsGeneratedData && hasGeneratableContext
2656
- && (analyticalPlan?.safe !== false || analyticalPlanAllowsDbtExploration(analyticalPlan))) {
3118
+ && (frozenExploratoryRepair
3119
+ || analyticalPlan?.safe !== false
3120
+ || analyticalPlanAllowsDbtExploration(analyticalPlan))) {
2657
3121
  try {
2658
3122
  proposed = await generateProposalWithOptionalTools({
2659
3123
  provider,
2660
- messages: [...messages, { role: 'system', content: FORCE_JOIN_INSTRUCTION }],
2661
- // One tool round per ordinary Ask. The second generation is a bounded
2662
- // 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.
2663
3139
  tools: [],
2664
3140
  questionPlan,
2665
3141
  intent,
@@ -2668,6 +3144,13 @@ async function runAnswerLoop(input) {
2668
3144
  analysisDepth: input.analysisDepth,
2669
3145
  toolCalls: proposalToolCalls,
2670
3146
  providerPayloadGuard: input.providerPayloadGuard,
3147
+ ...(frozenExploratoryRepair
3148
+ ? {
3149
+ dispatchPhase: 'repair',
3150
+ egressPurpose: 'repair_sql',
3151
+ maxProviderDispatches: 1,
3152
+ }
3153
+ : {}),
2671
3154
  });
2672
3155
  parsed = parseProposal(proposed);
2673
3156
  }
@@ -2742,6 +3225,49 @@ async function runAnswerLoop(input) {
2742
3225
  }
2743
3226
  if (parsed.sql) {
2744
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
+ }
2745
3271
  if (!governedMetricAnswer && frozenPlan?.mode === 'authoritative' && frozenPlan.schemaVersion === 2) {
2746
3272
  const targetFingerprint = input.generatedProposalTargetFingerprint ?? '';
2747
3273
  const validation = validateGeneratedAnalyticalProposal({
@@ -3345,7 +3871,15 @@ async function runAnswerLoop(input) {
3345
3871
  let result;
3346
3872
  let executionError;
3347
3873
  let warehouseFailure;
3874
+ let exploratoryClosureDenied = false;
3875
+ let semanticFanoutProbeFailure;
3348
3876
  let repairAttempts = 0;
3877
+ // A forced proposal may originate from the bounded analyst loop, but it
3878
+ // cannot bypass a router-owned exploratory decision. In that case the host
3879
+ // still prepares and freezes the exact validated SQL before execution.
3880
+ const exploratoryExecutionSelected = input.selectedCascadeTier === 'exploratory_sql';
3881
+ let initialExploratoryExecution;
3882
+ let exploratoryRepairExecution;
3349
3883
  // Repair candidates that were generated but rejected before execution. These
3350
3884
  // are diagnostics ABOUT the recovery attempt, not the reason the run failed,
3351
3885
  // so they are reported alongside the original error rather than replacing it.
@@ -3360,7 +3894,7 @@ async function runAnswerLoop(input) {
3360
3894
  executionError = deepCandidateExecutionError;
3361
3895
  warehouseFailure = normalizeWarehouseSqlFailure(deepCandidateExecutionError, input.semanticDriver);
3362
3896
  }
3363
- const executeCurrentSql = () => {
3897
+ const executeCurrentSql = async (authorizationAttempt) => {
3364
3898
  const requestedLimit = questionPlan.requestedShape.topN?.scope === 'per_group'
3365
3899
  ? 200
3366
3900
  : questionPlan.requestedShape.topN?.n ?? 200;
@@ -3386,7 +3920,7 @@ async function runAnswerLoop(input) {
3386
3920
  outputs: parsed.outputs,
3387
3921
  });
3388
3922
  const boundedArtifact = artifact ? { ...artifact, limit: requestedLimit } : undefined;
3389
- if (input.forcedGeneratedProposal) {
3923
+ if (input.forcedGeneratedProposal && !exploratoryExecutionSelected) {
3390
3924
  if (!input.agenticSqlExecutionCapability || !input.executeAgenticGeneratedSql) {
3391
3925
  throw Object.assign(new Error('The analyst-approved SQL has no live execution capability, so DQL did not execute it.'), {
3392
3926
  code: 'AGENTIC_EXECUTION_CAPABILITY_REQUIRED',
@@ -3394,27 +3928,63 @@ async function runAnswerLoop(input) {
3394
3928
  }
3395
3929
  return input.executeAgenticGeneratedSql(input.agenticSqlExecutionCapability, parsed.sql, boundedArtifact);
3396
3930
  }
3931
+ if (exploratoryExecutionSelected) {
3932
+ if (!input.prepareExploratorySqlExecution || !input.executeAgenticGeneratedSql) {
3933
+ throw Object.assign(new Error('The selected exploratory SQL has no host-authorized execution capability, so DQL did not execute it.'), {
3934
+ code: 'EXPLORATORY_EXECUTION_CAPABILITY_REQUIRED',
3935
+ });
3936
+ }
3937
+ // This is the only point at which a router-selected exploratory proposal
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);
3948
+ }
3397
3949
  if (!input.executeGeneratedSql)
3398
3950
  throw new Error('No generated SQL executor is configured.');
3399
3951
  return input.executeGeneratedSql(parsed.sql, boundedArtifact);
3400
3952
  };
3401
- if ((input.executeGeneratedSql || (input.forcedGeneratedProposal && input.executeAgenticGeneratedSql)) && !result) {
3953
+ if ((input.executeGeneratedSql
3954
+ || input.executeDqlArtifact
3955
+ || (input.forcedGeneratedProposal && input.executeAgenticGeneratedSql)
3956
+ || exploratoryExecutionSelected) && !result) {
3402
3957
  // Fanout gate for native semantic direct-joins: a duplicate join-key row on
3403
3958
  // the joined side multiplies fact rows BEFORE aggregation, so every summed
3404
3959
  // value inflates silently (seen in the field as trillions-scale "governed"
3405
3960
  // answers). Probe first; on structural contradiction, refuse with the cause
3406
3961
  // instead of presenting wrong numbers — and never hand the error to SQL
3407
3962
  // repair, which would regenerate the same multiplying join.
3408
- let fanoutContradiction = false;
3963
+ let semanticFanoutBlocked = false;
3409
3964
  if (!executionError
3410
3965
  && !input.forcedGeneratedProposal
3411
- && input.executeGeneratedSql
3966
+ && !exploratoryExecutionSelected
3412
3967
  && semanticBridgeAnswer?.composeResult?.fanoutProbeSql
3413
3968
  && semanticBridgeAnswer.sql.trim() === parsed.sql?.trim()) {
3414
- const inflationError = await probeSemanticJoinFanout(semanticBridgeAnswer.composeResult.fanoutProbeSql, semanticBridgeAnswer.composeResult.tables, input.executeGeneratedSql);
3415
- if (inflationError) {
3416
- executionError = inflationError;
3417
- 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;
3418
3988
  }
3419
3989
  }
3420
3990
  try {
@@ -3422,10 +3992,30 @@ async function runAnswerLoop(input) {
3422
3992
  result = await executeCurrentSql();
3423
3993
  }
3424
3994
  catch (err) {
3995
+ const executionCode = err && typeof err === 'object'
3996
+ && typeof err.code === 'string'
3997
+ ? err.code
3998
+ : undefined;
3999
+ // A router-selected exploratory proposal that leaves its candidate
4000
+ // closure is not an executable draft. It is a pre-capability safety
4001
+ // denial: do not present its SQL/artifact as review-required work after
4002
+ // the host rejected it.
4003
+ exploratoryClosureDenied = exploratoryExecutionSelected
4004
+ && executionCode === 'UNAUTHORIZED_SQL';
3425
4005
  warehouseFailure = normalizeWarehouseSqlFailure(err, input.semanticDriver);
3426
4006
  executionError = warehouseFailure.redactedMessage;
3427
4007
  }
3428
- if (executionError && !input.forcedGeneratedProposal && !fanoutContradiction && !authoritativeSemanticBinding) {
4008
+ if (executionError
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
4018
+ && !authoritativeSemanticBinding) {
3429
4019
  warehouseFailure ??= normalizeWarehouseSqlFailure(executionError, input.semanticDriver);
3430
4020
  if (isRetryableGeneratedSqlError(warehouseFailure)) {
3431
4021
  const localRepairSql = repairGeneratedSqlLocally(parsed.sql, executionError, schemaContext);
@@ -3450,7 +4040,13 @@ async function runAnswerLoop(input) {
3450
4040
  ],
3451
4041
  };
3452
4042
  try {
3453
- result = await executeCurrentSql();
4043
+ result = await executeCurrentSql(exploratoryExecutionSelected
4044
+ ? {
4045
+ version: 1,
4046
+ index: 1,
4047
+ parentSqlFingerprint: initialExploratoryExecution.freeze.sqlFingerprint,
4048
+ }
4049
+ : undefined);
3454
4050
  executionError = undefined;
3455
4051
  warehouseFailure = undefined;
3456
4052
  }
@@ -3482,6 +4078,18 @@ async function runAnswerLoop(input) {
3482
4078
  schemaContext,
3483
4079
  signal: input.signal,
3484
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
+ : {}),
3485
4093
  });
3486
4094
  const repaired = parseProposal(repairedRaw);
3487
4095
  if (repaired.sql) {
@@ -3500,7 +4108,12 @@ async function runAnswerLoop(input) {
3500
4108
  repairNarrative = repaired.text?.trim() || undefined;
3501
4109
  parsed.sql = repaired.sql;
3502
4110
  parsed.viz = repaired.viz ?? parsed.viz;
3503
- 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);
3504
4117
  contextValidation = {
3505
4118
  ok: true,
3506
4119
  warnings: [
@@ -3509,7 +4122,13 @@ async function runAnswerLoop(input) {
3509
4122
  ],
3510
4123
  };
3511
4124
  try {
3512
- result = await executeCurrentSql();
4125
+ result = await executeCurrentSql(exploratoryExecutionSelected
4126
+ ? {
4127
+ version: 1,
4128
+ index: 1,
4129
+ parentSqlFingerprint: initialExploratoryExecution.freeze.sqlFingerprint,
4130
+ }
4131
+ : undefined);
3513
4132
  executionError = undefined;
3514
4133
  warehouseFailure = undefined;
3515
4134
  }
@@ -3536,10 +4155,17 @@ async function runAnswerLoop(input) {
3536
4155
  : 'The question asks for a custom analysis, ranking, breakdown, comparison, or grain that should not be answered by a loose certified block match.'),
3537
4156
  selectedNodes: contextNodes,
3538
4157
  schemaContext,
3539
- sql: parsed.sql,
4158
+ // A router-selected physical-closure denial happens before a capability
4159
+ // exists. Its rejected bytes are diagnostics for the host, never a SQL
4160
+ // preview that a user can review, copy, or turn into a draft.
4161
+ sql: exploratoryClosureDenied || semanticFanoutProbeFailure ? undefined : parsed.sql,
3540
4162
  suggestedViz: parsed.viz ?? 'table',
3541
4163
  assumptions: [
3542
- 'The SQL preview is uncertified until an analyst reviews and promotes the DQL artifact.',
4164
+ ...(exploratoryClosureDenied
4165
+ ? ['The router-selected physical closure rejected the generated SQL before capability minting.']
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.']),
3543
4169
  ...(certifiedExecutionFallback
3544
4170
  ? [`Certified block ${certifiedExecutionFallback.node.name} failed execution and was bypassed: ${certifiedExecutionFallback.error}`]
3545
4171
  : []),
@@ -3563,6 +4189,23 @@ async function runAnswerLoop(input) {
3563
4189
  }
3564
4190
  }
3565
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;
3566
4209
  // ANY question whose SQL executed but dropped multiple requested columns used to
3567
4210
  // REFUSE outright ("no governed answer"), throwing away a result that actually
3568
4211
  // ran. Instead, SURFACE the partial result (review-required) with a warning that
@@ -3585,27 +4228,32 @@ async function runAnswerLoop(input) {
3585
4228
  ...repairNotes,
3586
4229
  ...(resultShape?.warnings ?? []),
3587
4230
  ...(partialShapeWarning ? [partialShapeWarning] : []),
4231
+ ...(frozenExploratoryOutputContractFailure
4232
+ ? [`The executed exploratory result violated the frozen output contract: ${frozenExploratoryOutputFailures.join(', ')}.`]
4233
+ : []),
3588
4234
  ...(topNTrimNote ? [topNTrimNote] : []),
3589
4235
  ...(executionError ? ['The preview execution error must be reviewed before reuse.'] : []),
3590
4236
  ];
3591
4237
  const generatedOutputs = parsed.outputs?.length ? parsed.outputs : resultColumnNames(result);
3592
4238
  const generatedRequestedFilters = mergeProposalStringLists(input.followUp?.filters, parsed.requestedFilters);
3593
4239
  const generatedRequestedDimensions = mergeProposalStringLists(input.followUp?.dimensions, parsed.requestedDimensions);
3594
- const baseDqlArtifact = result?.dqlArtifact ?? semanticBridgeAnswer?.dqlArtifact ?? buildGeneratedSqlDqlArtifact({
3595
- question,
3596
- sql: parsed.sql,
3597
- intent,
3598
- domain,
3599
- followUp: input.followUp,
3600
- contextPack: input.contextPack,
3601
- sourceBlock: followUpSourceBlock ?? undefined,
3602
- sourceDqlArtifact: input.followUp?.priorDqlArtifact,
3603
- proposedEntity: parsed.proposedEntity,
3604
- requestedFilters: generatedRequestedFilters,
3605
- requestedDimensions: generatedRequestedDimensions,
3606
- validationWarnings,
3607
- outputs: generatedOutputs,
3608
- });
4240
+ const baseDqlArtifact = exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure
4241
+ ? undefined
4242
+ : result?.dqlArtifact ?? semanticBridgeAnswer?.dqlArtifact ?? buildGeneratedSqlDqlArtifact({
4243
+ question,
4244
+ sql: parsed.sql,
4245
+ intent,
4246
+ domain,
4247
+ followUp: input.followUp,
4248
+ contextPack: input.contextPack,
4249
+ sourceBlock: followUpSourceBlock ?? undefined,
4250
+ sourceDqlArtifact: input.followUp?.priorDqlArtifact,
4251
+ proposedEntity: parsed.proposedEntity,
4252
+ requestedFilters: generatedRequestedFilters,
4253
+ requestedDimensions: generatedRequestedDimensions,
4254
+ validationWarnings,
4255
+ outputs: generatedOutputs,
4256
+ });
3609
4257
  const requestedTopN = questionPlan.requestedShape.topN?.scope === 'per_group'
3610
4258
  ? undefined
3611
4259
  : questionPlan.requestedShape.topN?.n;
@@ -3618,7 +4266,7 @@ async function runAnswerLoop(input) {
3618
4266
  : undefined;
3619
4267
  let draftBlock;
3620
4268
  let draftCaptureError;
3621
- if (input.captureGeneratedDraft && parsed.sql) {
4269
+ if (!exploratoryClosureDenied && !semanticFanoutProbeFailure && !frozenExploratoryOutputContractFailure && input.captureGeneratedDraft && parsed.sql) {
3622
4270
  try {
3623
4271
  draftBlock = await input.captureGeneratedDraft({
3624
4272
  question,
@@ -3665,32 +4313,53 @@ async function runAnswerLoop(input) {
3665
4313
  : undefined;
3666
4314
  const certifiedMetricAnswer = semanticMetricCertification === 'certified' || semanticMetricCertification === 'reviewed';
3667
4315
  const governedMetricExecutionFailure = governedMetricAnswer && Boolean(executionError);
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;
3668
4328
  const finalSemanticExecutionTrace = semanticTraceAfterExecution(semanticExecutionTrace, {
3669
4329
  executed: Boolean(result),
3670
- ...(executionError ? { error: executionError } : {}),
4330
+ ...(semanticFanoutTraceError ? { error: semanticFanoutTraceError } : {}),
3671
4331
  ...(result ? { result } : {}),
3672
4332
  });
3673
4333
  return {
3674
- kind: governedMetricExecutionFailure ? 'no_answer' : 'uncertified',
3675
- sourceTier: governedMetricExecutionFailure ? 'no_answer' : governedMetricAnswer ? 'semantic_layer' : activeTier,
3676
- certification: governedMetricExecutionFailure ? 'analyst_review_required' : governedMetricAnswer ? 'governed' : 'ai_generated',
3677
- reviewStatus: governedMetricExecutionFailure ? 'none' : governedMetricAnswer ? 'governed' : 'draft_ready',
4334
+ kind: terminalExecutionFailure ? 'no_answer' : 'uncertified',
4335
+ sourceTier: terminalExecutionFailure ? 'no_answer' : governedMetricAnswer ? 'semantic_layer' : activeTier,
4336
+ certification: terminalExecutionFailure ? 'analyst_review_required' : governedMetricAnswer ? 'governed' : 'ai_generated',
4337
+ reviewStatus: terminalExecutionFailure ? 'none' : governedMetricAnswer ? 'governed' : 'draft_ready',
3678
4338
  semanticMetricCertification,
3679
- confidence: governedMetricExecutionFailure ? 0 : certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
3680
- text: generatedText,
3681
- answer: generatedText,
3682
- proposedSql: parsed.sql,
3683
- sql: parsed.sql,
3684
- result,
4339
+ confidence: terminalExecutionFailure ? 0 : certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
4340
+ text: terminalFailureMessage ?? generatedText,
4341
+ answer: terminalFailureMessage ?? generatedText,
4342
+ ...(exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure ? {} : { proposedSql: parsed.sql, sql: parsed.sql }),
4343
+ result: frozenExploratoryOutputContractFailure ? undefined : result,
3685
4344
  executionError,
3686
4345
  ...(warehouseFailure ? { warehouseFailure } : {}),
3687
4346
  ...(finalSemanticExecutionTrace ? { semanticExecutionTrace: finalSemanticExecutionTrace } : {}),
3688
- ...(governedMetricExecutionFailure ? { 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
+ } : {}),
3689
4356
  suggestedViz: parsed.viz ?? 'table',
3690
- dqlArtifact: answerDqlArtifact,
3691
- draftBlock,
3692
- draftBlockId: draftBlock?.path,
3693
- promoteCommand: draftBlock ? `dql certify --from-draft ${draftBlock.path}` : undefined,
4357
+ ...(exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure ? {} : { dqlArtifact: answerDqlArtifact }),
4358
+ ...(exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure ? {} : {
4359
+ draftBlock,
4360
+ draftBlockId: draftBlock?.path,
4361
+ promoteCommand: draftBlock ? `dql certify --from-draft ${draftBlock.path}` : undefined,
4362
+ }),
3694
4363
  trustLabel: input.contextPack?.trustLabel,
3695
4364
  sourceCertifiedBlock,
3696
4365
  contextPackId: input.contextPack?.id,
@@ -3728,6 +4397,8 @@ async function runAnswerLoop(input) {
3728
4397
  considered,
3729
4398
  providerUsed: provider.name,
3730
4399
  aggregationSafetyProof,
4400
+ ...(initialExploratoryExecution ? { exploratoryExecutionFreeze: initialExploratoryExecution.freeze } : {}),
4401
+ ...(exploratoryRepairExecution ? { exploratoryRepairExecutionFreeze: exploratoryRepairExecution.freeze } : {}),
3731
4402
  // Carry the governed metric match so the exit point can name a
3732
4403
  // `semantic_metric` route (spec 17, part C).
3733
4404
  _semanticMetricMatch: governedMetricAnswer ? semanticMetricMatch ?? undefined : undefined,
@@ -3767,6 +4438,70 @@ function resultColumnNames(result) {
3767
4438
  .filter(Boolean);
3768
4439
  return columns?.length ? columns : undefined;
3769
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
+ }
3770
4505
  function analyticalCapabilityForPlan(plan, contextPack, registry = []) {
3771
4506
  const selected = normalizeMetricCapabilityContract(plan.selectedCapability);
3772
4507
  if (selected) {
@@ -4133,8 +4868,11 @@ async function executeSemanticAnalyticalGraph(input) {
4133
4868
  });
4134
4869
  aggregationSafetyProof = proof;
4135
4870
  if (proof.status !== 'safe') {
4136
- return analyticalGraphFailureAnswer(input, 'EXECUTION_GRAPH_MISMATCH', semanticAggregationSafetyFailure(proof), {
4137
- 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',
4138
4876
  dqlArtifact: composed.dqlArtifact,
4139
4877
  compiledSql: composed.sql,
4140
4878
  semanticExecutionTrace,
@@ -4160,11 +4898,25 @@ async function executeSemanticAnalyticalGraph(input) {
4160
4898
  }
4161
4899
  catch (error) {
4162
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));
4163
4907
  return analyticalGraphFailureAnswer(input, 'EXECUTION_FAILED', message, {
4164
4908
  phase: 'execution',
4165
4909
  dqlArtifact: composed.dqlArtifact,
4166
4910
  compiledSql: composed.sql,
4167
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 } : {}),
4168
4920
  });
4169
4921
  }
4170
4922
  const normalized = normalizeAnalyticalSourceResult({
@@ -4594,6 +5346,10 @@ function analyticalGraphFailureAnswer(input, code, reason, artifacts = {}) {
4594
5346
  ...(artifacts.dqlArtifact ? { dqlArtifact: artifacts.dqlArtifact } : {}),
4595
5347
  ...(artifacts.semanticExecutionTrace ? { semanticExecutionTrace: artifacts.semanticExecutionTrace } : {}),
4596
5348
  ...(artifacts.aggregationSafetyProof ? { aggregationSafetyProof: artifacts.aggregationSafetyProof } : {}),
5349
+ ...(artifacts.warehouseFailure ? { warehouseFailure: artifacts.warehouseFailure } : {}),
5350
+ ...(artifacts.observabilityExecutionFailure
5351
+ ? { observabilityExecutionFailure: artifacts.observabilityExecutionFailure }
5352
+ : {}),
4597
5353
  citations: contextPackCitations(input.input.contextPack, 8),
4598
5354
  considered: input.considered,
4599
5355
  contextPack: input.input.contextPack,
@@ -4765,6 +5521,38 @@ Rules:
4765
5521
  // combined dataset — show them separately" refusal into the join the user asked
4766
5522
  // for, while still allowing an honest refusal if context is truly missing.
4767
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
+ }
4768
5556
  /**
4769
5557
  * Produces the prompt-facing subset of a broad local context pack. Global
4770
5558
  * records remain available, while a record explicitly owned by another domain
@@ -4816,6 +5604,147 @@ function scopeContextPackToQuestionDomains(contextPack, domains, manifest) {
4816
5604
  },
4817
5605
  };
4818
5606
  }
5607
+ /**
5608
+ * Make the router-selected exploratory physical closure the only SQL authority
5609
+ * visible to generation and execution. The broad context pack remains on the
5610
+ * run as diagnostics, but must not make a same-snapshot neighbouring relation
5611
+ * executable merely because it ranked well for the surrounding question.
5612
+ *
5613
+ * Matching is canonical-ID only. In particular this deliberately does not
5614
+ * fall back to relation/model leaf names: duplicate `orders` relations across
5615
+ * schemas/domains must not inherit each other's proof.
5616
+ */
5617
+ export function scopeContextPackToExploratoryCandidateClosure(contextPack, candidateIds) {
5618
+ if (!contextPack || !candidateIds?.length)
5619
+ return undefined;
5620
+ const requestedIds = new Set(candidateIds.map(normalizeExploratoryCandidateIdentity).filter(Boolean));
5621
+ if (requestedIds.size === 0)
5622
+ return undefined;
5623
+ const intersects = (values) => {
5624
+ for (const value of values) {
5625
+ const normalized = normalizeExploratoryCandidateIdentity(value ?? '');
5626
+ if (normalized && requestedIds.has(normalized))
5627
+ return true;
5628
+ }
5629
+ return false;
5630
+ };
5631
+ const objectIdentities = (object) => {
5632
+ const payload = object.payload ?? {};
5633
+ return [
5634
+ object.objectKey,
5635
+ object.fullName,
5636
+ payload.qualifiedId,
5637
+ payload.uniqueId,
5638
+ payload.id,
5639
+ payload.localId,
5640
+ payload.relation,
5641
+ payload.table,
5642
+ payload.model,
5643
+ ...metadataStringValues(payload.sourceObjects),
5644
+ ...metadataStringValues(payload.sourceRelations),
5645
+ ...metadataStringValues(payload.tableDependencies),
5646
+ ].filter((value) => typeof value === 'string');
5647
+ };
5648
+ const objectRelationReferences = (object) => {
5649
+ const payload = object.payload ?? {};
5650
+ return [
5651
+ payload.relation,
5652
+ payload.table,
5653
+ payload.model,
5654
+ ...metadataStringValues(payload.sourceObjects),
5655
+ ...metadataStringValues(payload.sourceRelations),
5656
+ ...metadataStringValues(payload.tableDependencies),
5657
+ ].filter((value) => typeof value === 'string');
5658
+ };
5659
+ const directObjects = contextPack.objects.filter((object) => intersects(objectIdentities(object)));
5660
+ const requestedRelations = new Set();
5661
+ for (const object of directObjects) {
5662
+ for (const relation of objectRelationReferences(object)) {
5663
+ const normalized = normalizeExploratoryCandidateIdentity(relation);
5664
+ if (normalized)
5665
+ requestedRelations.add(normalized);
5666
+ }
5667
+ }
5668
+ for (const relation of contextPack.allowedSqlContext.relations) {
5669
+ if (intersects([relation.objectKey, relation.relation])) {
5670
+ requestedRelations.add(normalizeExploratoryCandidateIdentity(relation.relation));
5671
+ }
5672
+ }
5673
+ const relations = contextPack.allowedSqlContext.relations.filter((relation) => {
5674
+ const normalized = normalizeExploratoryCandidateIdentity(relation.relation);
5675
+ return requestedIds.has(normalized)
5676
+ || requestedRelations.has(normalized)
5677
+ || Boolean(relation.objectKey && directObjects.some((object) => object.objectKey === relation.objectKey));
5678
+ });
5679
+ // A router-selected exploratory attempt without a physical relation is not
5680
+ // executable. Returning undefined is intentionally fail-closed; callers may
5681
+ // preserve the full pack for receipts but may not use it as SQL authority.
5682
+ if (relations.length === 0)
5683
+ return undefined;
5684
+ const relationIds = new Set(relations.map((relation) => normalizeExploratoryCandidateIdentity(relation.relation)));
5685
+ const relationshipEndpointIds = new Set();
5686
+ for (const object of directObjects.filter((object) => object.objectType === 'relationship')) {
5687
+ const payload = object.payload ?? {};
5688
+ for (const endpoint of [payload.from, payload.to]) {
5689
+ if (typeof endpoint === 'string') {
5690
+ const normalized = normalizeExploratoryCandidateIdentity(endpoint);
5691
+ if (normalized)
5692
+ relationshipEndpointIds.add(normalized);
5693
+ }
5694
+ }
5695
+ }
5696
+ const objects = contextPack.objects.filter((object) => {
5697
+ if (directObjects.includes(object))
5698
+ return true;
5699
+ const identities = objectIdentities(object).map(normalizeExploratoryCandidateIdentity);
5700
+ if (identities.some((identity) => relationIds.has(identity) || relationshipEndpointIds.has(identity)))
5701
+ return true;
5702
+ return objectRelationReferences(object)
5703
+ .map(normalizeExploratoryCandidateIdentity)
5704
+ .some((identity) => relationIds.has(identity));
5705
+ });
5706
+ const objectKeys = new Set(objects.map((object) => object.objectKey));
5707
+ const relationKeys = new Set(relations.map((relation) => normalizeRelationKey(relation.relation)));
5708
+ const selectedRelations = contextPack.retrievalDiagnostics.selectedRelations?.filter((relation) => relationKeys.has(normalizeRelationKey(relation.relation)));
5709
+ const selectedJoinPaths = contextPack.retrievalDiagnostics.selectedJoinPaths?.filter((path) => relationKeys.has(normalizeRelationKey(path.leftRelation))
5710
+ && relationKeys.has(normalizeRelationKey(path.rightRelation)));
5711
+ return {
5712
+ ...contextPack,
5713
+ focusObjectKey: contextPack.focusObjectKey && objectKeys.has(contextPack.focusObjectKey)
5714
+ ? contextPack.focusObjectKey
5715
+ : objects[0]?.objectKey ?? null,
5716
+ objects,
5717
+ skills: [],
5718
+ edges: contextPack.edges.filter((edge) => objectKeys.has(edge.fromKey) && objectKeys.has(edge.toKey)),
5719
+ citations: contextPack.citations.filter((citation) => objectKeys.has(citation.objectKey)),
5720
+ evidenceSummaries: contextPack.evidenceSummaries.filter((summary) => !summary.objectKey || objectKeys.has(summary.objectKey)),
5721
+ evidenceRoles: contextPack.evidenceRoles.filter((role) => objectKeys.has(role.objectKey)),
5722
+ allowedSqlContext: {
5723
+ relations,
5724
+ // Authored block SQL is not a physical exploratory authority. The
5725
+ // selected dbt/runtime relation closure above is the whole prompt/SQL
5726
+ // boundary for this tier.
5727
+ sourceBlockSql: [],
5728
+ },
5729
+ retrievalDiagnostics: {
5730
+ ...contextPack.retrievalDiagnostics,
5731
+ selectedObjects: objects.length,
5732
+ selectedEvidence: contextPack.retrievalDiagnostics.selectedEvidence.filter((evidence) => objectKeys.has(evidence.objectKey)),
5733
+ selectedRelations,
5734
+ selectedJoinPaths,
5735
+ schemaShapeCandidates: contextPack.retrievalDiagnostics.schemaShapeCandidates?.filter((candidate) => objectKeys.has(candidate.objectKey)),
5736
+ certifiedCandidateFits: contextPack.retrievalDiagnostics.certifiedCandidateFits.filter((candidate) => objectKeys.has(candidate.objectKey)),
5737
+ },
5738
+ };
5739
+ }
5740
+ function metadataStringValues(value) {
5741
+ return Array.isArray(value)
5742
+ ? value.filter((item) => typeof item === 'string')
5743
+ : [];
5744
+ }
5745
+ function normalizeExploratoryCandidateIdentity(value) {
5746
+ return value.trim().replace(/["`\[\]]/g, '').replace(/\s*\.\s*/g, '.').toLowerCase();
5747
+ }
4819
5748
  /**
4820
5749
  * Build the prompt-facing evidence pack after enforcing the certified output
4821
5750
  * contract. Incompatible blocks remain in the returned answer's `considered`
@@ -4831,7 +5760,13 @@ function contextPackForRequestedShape(contextPack, question, questionPlan, kg) {
4831
5760
  const blockName = object.name || object.objectKey.replace(/^dql:block:/, '');
4832
5761
  const node = kg.getNode(`block:${blockName}`);
4833
5762
  if (node?.kind === 'block') {
4834
- const fit = evaluateCertifiedBlockFit({ question, plan: questionPlan, block: node });
5763
+ const fit = evaluateCertifiedBlockFit({
5764
+ question,
5765
+ plan: questionPlan,
5766
+ block: node,
5767
+ exactExampleMatch: hasUniqueExactCatalogExample(question, contextPack, node),
5768
+ uniqueExactExampleContract: hasUniqueExactCatalogExample(question, contextPack, node),
5769
+ });
4835
5770
  if (fit.kind === 'context_only' || fit.kind === 'not_applicable') {
4836
5771
  incompatibleBlockKeys.add(object.objectKey);
4837
5772
  }
@@ -5384,7 +6319,6 @@ function renderContextPackForPrompt(contextPack, budget) {
5384
6319
  : '';
5385
6320
  const sourceSql = renderSourceBlockSqlContext(contextPack, budget);
5386
6321
  return [
5387
- `context_pack_id: ${contextPack.id}`,
5388
6322
  `trust_label: ${contextPack.trustLabel}`,
5389
6323
  contextPack.trustLabelInfo ? `trust_label_canonical: ${contextPack.trustLabelInfo.display}` : '',
5390
6324
  renderDomainBriefingForPrompt(contextPack).trim(),
@@ -5678,6 +6612,72 @@ function certifiedHitFromContextPack(contextPack, kg) {
5678
6612
  const node = nodeId ? kg.getNode(nodeId) : null;
5679
6613
  return node ? { node, score: 1, snippet: object.snippet } : null;
5680
6614
  }
6615
+ /**
6616
+ * Preserve the catalog's unique exact-example proof when the answer boundary
6617
+ * rechecks the frozen block. The context pack is the same retrieval snapshot
6618
+ * that produced `exactObjectKey`; broad KG search is intentionally not used to
6619
+ * manufacture example uniqueness.
6620
+ */
6621
+ function hasUniqueExactCatalogExample(question, contextPack, node) {
6622
+ const normalizedQuestion = normalizeQuestion(question);
6623
+ if (!normalizedQuestion || !contextPack)
6624
+ return false;
6625
+ const matches = contextPack.objects.filter((object) => {
6626
+ if (object.objectType !== 'dql_block')
6627
+ return false;
6628
+ if (object.status !== 'certified' && object.status !== 'approved' && object.payload?.certification !== 'certified')
6629
+ return false;
6630
+ const examples = Array.isArray(object.payload?.examples) ? object.payload.examples : [];
6631
+ return examples.some((example) => example
6632
+ && typeof example === 'object'
6633
+ && normalizeQuestion(String(example.question ?? '')) === normalizedQuestion);
6634
+ });
6635
+ return matches.length === 1 && normalizeQuestion(matches[0].name) === normalizeQuestion(node.name);
6636
+ }
6637
+ /**
6638
+ * A metadata-only definition answer is safe only for one certified artifact
6639
+ * named exactly by the user. Do not let FTS ranking decide this: a generic
6640
+ * definition phrase must never select a neighbouring block. Payload aliases
6641
+ * are authored block aliases; descriptions and tags are deliberately excluded.
6642
+ */
6643
+ function uniqueNamedCertifiedDefinitionArtifact(question, kg, authorizedDomains) {
6644
+ const subject = namedArtifactDefinitionSubject(question);
6645
+ if (!subject)
6646
+ return null;
6647
+ const matches = kg.getNodesByKind('block', 1000).filter((node) => {
6648
+ if (node.status !== 'certified')
6649
+ return false;
6650
+ if (authorizedDomains.length > 0 && node.domain && !authorizedDomains.includes(node.domain))
6651
+ return false;
6652
+ return certifiedArtifactNames(node).some((name) => normalizeQuestion(name) === subject);
6653
+ });
6654
+ return matches.length === 1 ? { node: matches[0], score: 1, snippet: undefined } : null;
6655
+ }
6656
+ function isUniqueNamedCertifiedDefinitionQuestion(question, node, kg, authorizedDomains) {
6657
+ const selected = uniqueNamedCertifiedDefinitionArtifact(question, kg, authorizedDomains);
6658
+ return selected?.node.nodeId === node.nodeId;
6659
+ }
6660
+ function namedArtifactDefinitionSubject(question) {
6661
+ const match = /^\s*what\s+does\s+(?:the\s+)?(.+?)\s+(?:measure|mean|define|represent)\s*[?.!]*\s*$/i.exec(question);
6662
+ if (!match?.[1])
6663
+ return undefined;
6664
+ // Remove an optional explicit artifact noun while preserving the exact
6665
+ // authored identifier. `top_customers block` and `top_customers` therefore
6666
+ // resolve identically, while arbitrary definition questions have no subject
6667
+ // match and remain on the normal route.
6668
+ const subject = match[1]
6669
+ .replace(/\b(?:certified\s+)?(?:block|artifact|model|metric)\b/gi, ' ')
6670
+ .replace(/\s+/g, ' ')
6671
+ .trim();
6672
+ const normalized = normalizeQuestion(subject);
6673
+ return normalized || undefined;
6674
+ }
6675
+ function certifiedArtifactNames(node) {
6676
+ const aliases = Array.isArray(node.payload?.aliases)
6677
+ ? node.payload.aliases.filter((alias) => typeof alias === 'string')
6678
+ : [];
6679
+ return [node.name, ...aliases];
6680
+ }
5681
6681
  /**
5682
6682
  * Reduce a possibly-composed prompt to the user's actual question for display.
5683
6683
  * Callers (e.g. the Build-SQL-draft modal) sometimes prepend a multi-line instruction
@@ -6765,7 +7765,12 @@ function isBusinessDefinitionQuestion(question) {
6765
7765
  }
6766
7766
  /** Definition that may terminate with documentation instead of executing data. */
6767
7767
  function isPureBusinessDefinitionQuestion(question) {
6768
- return questionTypeFromText(question) === 'definition';
7768
+ return questionTypeFromText(question) === 'definition'
7769
+ // `questionTypeFromText` intentionally recognizes ranking before generic
7770
+ // definitions, so a named artifact such as `top_customers` would otherwise
7771
+ // look like a ranking request even in the explicit "what does … measure?"
7772
+ // form. Object-name matching remains mandatory at the terminal site.
7773
+ || /^\s*what\s+does\s+(?:the\s+)?[^?.!]{1,160}\s+(?:measure|mean|define|represent)\s*[?.!]*\s*$/i.test(question);
6769
7774
  }
6770
7775
  function isBreakdownOrDrilldownQuestion(question) {
6771
7776
  return /\b(break\s*down|breakdown|drill\s*(?:down|into)|slice|segment|split|by\s+[a-z][\w\s-]{1,40})\b/i.test(question);
@@ -6828,12 +7833,21 @@ function hasCertifiedNodeFit(question, plan, node, options = {}) {
6828
7833
  plan,
6829
7834
  block: node,
6830
7835
  exactExampleMatch: exactExampleMatch || exactObjectRequest,
7836
+ uniqueExactExampleContract: options.uniqueExactExampleContract === true,
6831
7837
  definitionLookup,
6832
7838
  });
6833
7839
  // Delegate the termination decision to THE single authority instead of a
6834
7840
  // loop-local re-derivation (the seams between such copies produced certified
6835
7841
  // answers that ignored member-scoped follow-ups).
6836
- return certifiedTerminationVerdict({ fit, allowInferredContract: options.allowInferredContract }).allow;
7842
+ return certifiedTerminationVerdict({
7843
+ fit,
7844
+ // A named `what does <artifact> measure?` request is an artifact metadata
7845
+ // lookup, not a query-result claim. The termination authority still
7846
+ // rejects missing declared measures and unsupported filters before this
7847
+ // narrow bypass is considered.
7848
+ ...(definitionLookup ? { bypass: 'definition_lookup' } : {}),
7849
+ allowInferredContract: options.allowInferredContract,
7850
+ }).allow;
6837
7851
  }
6838
7852
  function objectNameInQuestion(question, node) {
6839
7853
  const questionText = normalizeQuestion(question);
@@ -7248,6 +8262,11 @@ async function requestSqlRepair(input) {
7248
8262
  // bumping would let an internal preview-repair exceed that cap. Escalation-level
7249
8263
  // repairs bump-then-clamp in the host (resolveRunReasoningEffort).
7250
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
+ : {}),
7251
8270
  });
7252
8271
  }
7253
8272
  async function generateProposalWithOptionalTools(input) {
@@ -7255,6 +8274,11 @@ async function generateProposalWithOptionalTools(input) {
7255
8274
  const options = {
7256
8275
  signal: input.signal,
7257
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
+ : {}),
7258
8282
  };
7259
8283
  // No tools → plain generation (nothing for the loop to drive).
7260
8284
  if (tools.length === 0) {
@@ -7651,10 +8675,25 @@ function isRetryableGeneratedSqlError(failure) {
7651
8675
  return failure.retryDisposition === 'model_repair';
7652
8676
  }
7653
8677
  /**
7654
- * Only SQL-shape/binding failures may leave the certified lane. Connectivity,
7655
- * authorization, cancellation, and timeout failures would affect every query
7656
- * 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.
7657
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
+ }
7658
8697
  function isRetryableCertifiedExecutionError(error) {
7659
8698
  return /\b(?:binder|parser|catalog)\s+error\b/i.test(error)
7660
8699
  // DuckDB says "ambiguous reference"; Snowflake says "ambiguous column name".
@@ -8619,29 +9658,45 @@ function uniqueAssets(assets) {
8619
9658
  return Array.from(byId.values());
8620
9659
  }
8621
9660
  /**
8622
- * Execute the semantic layer's fanout probe and translate a structural
8623
- * contradiction into an actionable refusal message. Probe failures (missing
8624
- * permissions, dialect quirks) return undefined the probe protects against
8625
- * 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.
8626
9666
  */
8627
9667
  export async function probeSemanticJoinFanout(probeSql, joinedTables, executeSql) {
8628
9668
  try {
8629
9669
  const payload = await executeSql(probeSql);
8630
9670
  const counts = parseFanoutProbeCounts(payload);
8631
- if (!counts || counts.base <= 0 || counts.joined <= counts.base)
8632
- 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' };
8633
9680
  const factor = counts.joined / counts.base;
8634
9681
  const tables = joinedTables.filter(Boolean).join(', ');
8635
- return [
8636
- `Blocked a governed semantic answer whose join inflates results: joining ${tables || 'the declared tables'}`,
8637
- `turned ${counts.base.toLocaleString('en-US')} base rows into ${counts.joined.toLocaleString('en-US')} (×${factor >= 10 ? Math.round(factor) : factor.toFixed(1)}).`,
8638
- 'The declared join key is not unique on the joined side, so every aggregated value would be multiplied.',
8639
- 'Deduplicate the joined model (for example, filter a slowly-changing dimension to current records)',
8640
- 'or execute this metric through MetricFlow / dbt Cloud, then retry. No numbers were shown because they would be wrong.',
8641
- ].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
+ };
8642
9693
  }
8643
9694
  catch {
8644
- 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
+ };
8645
9700
  }
8646
9701
  }
8647
9702
  function parseFanoutProbeCounts(payload) {