@duckcodeailabs/dql-agent 1.14.0 → 1.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/dist/agent-run-engine.d.ts +68 -2
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +520 -26
  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 +25 -0
  7. package/dist/agent-run-gates.js.map +1 -1
  8. package/dist/agentic/analyst-loop.d.ts +91 -0
  9. package/dist/agentic/analyst-loop.d.ts.map +1 -0
  10. package/dist/agentic/analyst-loop.js +331 -0
  11. package/dist/agentic/analyst-loop.js.map +1 -0
  12. package/dist/agentic/assumptions.d.ts +62 -0
  13. package/dist/agentic/assumptions.d.ts.map +1 -0
  14. package/dist/agentic/assumptions.js +73 -0
  15. package/dist/agentic/assumptions.js.map +1 -0
  16. package/dist/agentic/business-explanation.d.ts +56 -0
  17. package/dist/agentic/business-explanation.d.ts.map +1 -0
  18. package/dist/agentic/business-explanation.js +130 -0
  19. package/dist/agentic/business-explanation.js.map +1 -0
  20. package/dist/agentic/identifier-ledger.d.ts +74 -0
  21. package/dist/agentic/identifier-ledger.d.ts.map +1 -0
  22. package/dist/agentic/identifier-ledger.js +151 -0
  23. package/dist/agentic/identifier-ledger.js.map +1 -0
  24. package/dist/agentic/ledger-tools.d.ts +68 -0
  25. package/dist/agentic/ledger-tools.d.ts.map +1 -0
  26. package/dist/agentic/ledger-tools.js +198 -0
  27. package/dist/agentic/ledger-tools.js.map +1 -0
  28. package/dist/agentic/orchestrator-policy.d.ts +60 -0
  29. package/dist/agentic/orchestrator-policy.d.ts.map +1 -0
  30. package/dist/agentic/orchestrator-policy.js +73 -0
  31. package/dist/agentic/orchestrator-policy.js.map +1 -0
  32. package/dist/agentic/orchestrator.d.ts +54 -0
  33. package/dist/agentic/orchestrator.d.ts.map +1 -0
  34. package/dist/agentic/orchestrator.js +67 -0
  35. package/dist/agentic/orchestrator.js.map +1 -0
  36. package/dist/agentic/preview-tool.d.ts +36 -0
  37. package/dist/agentic/preview-tool.d.ts.map +1 -0
  38. package/dist/agentic/preview-tool.js +71 -0
  39. package/dist/agentic/preview-tool.js.map +1 -0
  40. package/dist/agentic/rerank.d.ts +50 -0
  41. package/dist/agentic/rerank.d.ts.map +1 -0
  42. package/dist/agentic/rerank.js +140 -0
  43. package/dist/agentic/rerank.js.map +1 -0
  44. package/dist/agentic/research/hypothesis.d.ts +110 -0
  45. package/dist/agentic/research/hypothesis.d.ts.map +1 -0
  46. package/dist/agentic/research/hypothesis.js +125 -0
  47. package/dist/agentic/research/hypothesis.js.map +1 -0
  48. package/dist/agentic/research/synthesis.d.ts +36 -0
  49. package/dist/agentic/research/synthesis.d.ts.map +1 -0
  50. package/dist/agentic/research/synthesis.js +109 -0
  51. package/dist/agentic/research/synthesis.js.map +1 -0
  52. package/dist/agentic/research-agent.d.ts +54 -0
  53. package/dist/agentic/research-agent.d.ts.map +1 -0
  54. package/dist/agentic/research-agent.js +177 -0
  55. package/dist/agentic/research-agent.js.map +1 -0
  56. package/dist/agentic/sql-authorization.d.ts +131 -0
  57. package/dist/agentic/sql-authorization.d.ts.map +1 -0
  58. package/dist/agentic/sql-authorization.js +460 -0
  59. package/dist/agentic/sql-authorization.js.map +1 -0
  60. package/dist/agentic/tool-loop.d.ts +29 -0
  61. package/dist/agentic/tool-loop.d.ts.map +1 -1
  62. package/dist/agentic/tool-loop.js +129 -52
  63. package/dist/agentic/tool-loop.js.map +1 -1
  64. package/dist/agentic/toolset.d.ts.map +1 -1
  65. package/dist/agentic/toolset.js +155 -0
  66. package/dist/agentic/toolset.js.map +1 -1
  67. package/dist/agentic/turn-plan.d.ts +35 -0
  68. package/dist/agentic/turn-plan.d.ts.map +1 -0
  69. package/dist/agentic/turn-plan.js +108 -0
  70. package/dist/agentic/turn-plan.js.map +1 -0
  71. package/dist/agentic/value-probe.d.ts +89 -0
  72. package/dist/agentic/value-probe.d.ts.map +1 -0
  73. package/dist/agentic/value-probe.js +304 -0
  74. package/dist/agentic/value-probe.js.map +1 -0
  75. package/dist/analytical-frame.d.ts.map +1 -1
  76. package/dist/analytical-frame.js +10 -1
  77. package/dist/analytical-frame.js.map +1 -1
  78. package/dist/analytical-orchestration.d.ts +382 -0
  79. package/dist/analytical-orchestration.d.ts.map +1 -1
  80. package/dist/analytical-orchestration.js +782 -6
  81. package/dist/analytical-orchestration.js.map +1 -1
  82. package/dist/analytical-request-policy.d.ts +26 -0
  83. package/dist/analytical-request-policy.d.ts.map +1 -0
  84. package/dist/analytical-request-policy.js +82 -0
  85. package/dist/analytical-request-policy.js.map +1 -0
  86. package/dist/analytical-result-facts.d.ts +13 -0
  87. package/dist/analytical-result-facts.d.ts.map +1 -1
  88. package/dist/analytical-result-facts.js +37 -1
  89. package/dist/analytical-result-facts.js.map +1 -1
  90. package/dist/answer-loop.d.ts +105 -16
  91. package/dist/answer-loop.d.ts.map +1 -1
  92. package/dist/answer-loop.js +833 -108
  93. package/dist/answer-loop.js.map +1 -1
  94. package/dist/answer-shape.d.ts +32 -1
  95. package/dist/answer-shape.d.ts.map +1 -1
  96. package/dist/answer-shape.js +49 -3
  97. package/dist/answer-shape.js.map +1 -1
  98. package/dist/cascade/budgets.d.ts.map +1 -1
  99. package/dist/cascade/budgets.js +8 -1
  100. package/dist/cascade/budgets.js.map +1 -1
  101. package/dist/cascade/packer.d.ts +88 -0
  102. package/dist/cascade/packer.d.ts.map +1 -0
  103. package/dist/cascade/packer.js +106 -0
  104. package/dist/cascade/packer.js.map +1 -0
  105. package/dist/conversation/result-ops.d.ts +18 -3
  106. package/dist/conversation/result-ops.d.ts.map +1 -1
  107. package/dist/conversation/result-ops.js +77 -5
  108. package/dist/conversation/result-ops.js.map +1 -1
  109. package/dist/conversation/rolling-summary.d.ts.map +1 -1
  110. package/dist/conversation/rolling-summary.js +7 -2
  111. package/dist/conversation/rolling-summary.js.map +1 -1
  112. package/dist/conversation/session-store.d.ts +3 -0
  113. package/dist/conversation/session-store.d.ts.map +1 -1
  114. package/dist/conversation/session-store.js +8 -4
  115. package/dist/conversation/session-store.js.map +1 -1
  116. package/dist/conversation/snapshot.d.ts +20 -0
  117. package/dist/conversation/snapshot.d.ts.map +1 -1
  118. package/dist/conversation/snapshot.js +40 -2
  119. package/dist/conversation/snapshot.js.map +1 -1
  120. package/dist/fixtures/ask-ai-office-shaped.d.ts +153 -0
  121. package/dist/fixtures/ask-ai-office-shaped.d.ts.map +1 -0
  122. package/dist/fixtures/ask-ai-office-shaped.js +94 -0
  123. package/dist/fixtures/ask-ai-office-shaped.js.map +1 -0
  124. package/dist/hints/store.d.ts.map +1 -1
  125. package/dist/hints/store.js +15 -5
  126. package/dist/hints/store.js.map +1 -1
  127. package/dist/index.d.ts +29 -9
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +29 -6
  130. package/dist/index.js.map +1 -1
  131. package/dist/intent-controller.d.ts +36 -0
  132. package/dist/intent-controller.d.ts.map +1 -1
  133. package/dist/intent-controller.js +95 -0
  134. package/dist/intent-controller.js.map +1 -1
  135. package/dist/meaning-resolution.d.ts +69 -2
  136. package/dist/meaning-resolution.d.ts.map +1 -1
  137. package/dist/meaning-resolution.js +128 -7
  138. package/dist/meaning-resolution.js.map +1 -1
  139. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  140. package/dist/metadata/analysis-planner.js +124 -7
  141. package/dist/metadata/analysis-planner.js.map +1 -1
  142. package/dist/metadata/block-fit.d.ts +36 -0
  143. package/dist/metadata/block-fit.d.ts.map +1 -1
  144. package/dist/metadata/block-fit.js +290 -38
  145. package/dist/metadata/block-fit.js.map +1 -1
  146. package/dist/metadata/catalog.d.ts +20 -23
  147. package/dist/metadata/catalog.d.ts.map +1 -1
  148. package/dist/metadata/catalog.js +193 -22
  149. package/dist/metadata/catalog.js.map +1 -1
  150. package/dist/metadata/meaning-evidence.d.ts +10 -1
  151. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  152. package/dist/metadata/meaning-evidence.js +275 -53
  153. package/dist/metadata/meaning-evidence.js.map +1 -1
  154. package/dist/metadata/metric-match.d.ts +32 -0
  155. package/dist/metadata/metric-match.d.ts.map +1 -1
  156. package/dist/metadata/metric-match.js +120 -9
  157. package/dist/metadata/metric-match.js.map +1 -1
  158. package/dist/research-loop.d.ts +33 -2
  159. package/dist/research-loop.d.ts.map +1 -1
  160. package/dist/research-loop.js +66 -4
  161. package/dist/research-loop.js.map +1 -1
  162. package/dist/resolved-analytical-plan.d.ts +7 -0
  163. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  164. package/dist/resolved-analytical-plan.js +11 -4
  165. package/dist/resolved-analytical-plan.js.map +1 -1
  166. package/dist/router.d.ts +32 -19
  167. package/dist/router.d.ts.map +1 -1
  168. package/dist/router.js +1977 -73
  169. package/dist/router.js.map +1 -1
  170. package/dist/semantic-bridge/member-select.d.ts.map +1 -1
  171. package/dist/semantic-bridge/member-select.js +39 -21
  172. package/dist/semantic-bridge/member-select.js.map +1 -1
  173. package/package.json +5 -5
@@ -1,3 +1,4 @@
1
+ import { packContext } from './cascade/packer.js';
1
2
  /**
2
3
  * Dynamic-first governed answer loop.
3
4
  *
@@ -26,10 +27,10 @@ import { domainContextSearchDomains } from './domain-context.js';
26
27
  import { deriveGeneratedDraftSlug, renderGeneratedSqlDqlArtifact } from './metadata/drafts.js';
27
28
  import { buildAnalysisQuestionPlan } from './metadata/analysis-planner.js';
28
29
  import { certifiedFitAllowsTier1, certifiedTerminationVerdict, evaluateCertifiedBlockFit } from './metadata/block-fit.js';
29
- import { buildGovernedMetricFirstSql, matchSemanticMetric, resolveGovernedMetricDefinition, resolveGovernedMetricSql } from './metadata/metric-match.js';
30
+ import { buildGovernedMetricFirstSql, matchSemanticMetric, resolveGovernedMetricDefinition, resolveGovernedMetricSql, semanticMetricEmbeddingOptions } from './metadata/metric-match.js';
30
31
  import { decideAgentAction } from './intent-controller.js';
31
32
  import { createContextLedger } from './grounding/context-ledger.js';
32
- import { validateAnswerResultShape } from './answer-shape.js';
33
+ import { validateAnswerResultShape, } from './answer-shape.js';
33
34
  import { analyticalErrorDetail, } from './analytical-error.js';
34
35
  import { fanoutWarningsForSql } from './metadata/grain-ledger.js';
35
36
  import { buildAggregationSafetyProof, buildSemanticCompilationAggregationSafetyProof, } from './aggregation-safety-proof.js';
@@ -383,6 +384,142 @@ function certifiedInvocationInputs(block, plan) {
383
384
  }
384
385
  return Object.keys(parameters).length > 0 ? { parameters, parameterSources } : {};
385
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
+ }
386
523
  function damerauLevenshteinDistance(left, right) {
387
524
  const rows = left.length + 1;
388
525
  const columns = right.length + 1;
@@ -421,7 +558,35 @@ const MANIFEST_KINDS = ['dbt_model', 'dbt_source'];
421
558
  * surface shows THIS; the validator's machine message (relation ids, repair
422
559
  * tool guidance) belongs in refusalDetails/validationWarnings only.
423
560
  */
424
- function renderContextValidationRefusalForUser(code, machineError, memberBindings) {
561
+ /**
562
+ * Name the ONE aggregation check that actually fired.
563
+ *
564
+ * The refusal used to list all four possible causes — "rounding too early,
565
+ * losing decimal precision, summing a non-additive value, or multiplying rows
566
+ * across a join" — because the specific one was never plumbed into the message.
567
+ * The system knows: `aggregationIntegrityIssuesForSql` classifies each issue and
568
+ * the proof carries the codes. A menu of four causes reads as "something is
569
+ * wrong somewhere", which is exactly the report a user cannot act on.
570
+ */
571
+ function aggregationRefusalReason(issueCodes = []) {
572
+ const codes = new Set(issueCodes.map((code) => code.toUpperCase()));
573
+ // Ordered by how badly each distorts the number, so a query tripping several
574
+ // leads with the one that matters most.
575
+ if (codes.has('FANOUT')) {
576
+ return 'joining those tables multiplies rows, so the total would be inflated. Aggregate at the row-level grain first, then join';
577
+ }
578
+ if (codes.has('NON_ADDITIVE_MEASURE')) {
579
+ return 'it sums a measure that is not additive (an average or a ratio cannot be added up). Ask for the governed metric, which carries the right rule';
580
+ }
581
+ if (codes.has('PREMATURE_ROUNDING')) {
582
+ return 'it rounds each value before adding them, which loses money at scale. Rounding belongs on the final total';
583
+ }
584
+ if (codes.has('LOSSY_NUMERIC_CAST')) {
585
+ return 'it converts amounts to floating point before adding them, which drifts on decimal currency';
586
+ }
587
+ return undefined;
588
+ }
589
+ export function renderContextValidationRefusalForUser(code, machineError, memberBindings, aggregationIssueCodes) {
425
590
  switch (code) {
426
591
  case 'unknown_relation':
427
592
  return 'I drafted a query, but it uses a table that was not part of the metadata retrieved for this question, so I did not run it. Ask again (retrieval usually finds it on a follow-up), name the table or metric explicitly, or re-sync the dbt metadata.';
@@ -438,8 +603,15 @@ function renderContextValidationRefusalForUser(code, machineError, memberBinding
438
603
  return 'This comparison needs a baseline period or value that the drafted query did not include. Say what to compare against (for example, the prior month) and I will run it.';
439
604
  case 'unsafe_sql':
440
605
  return 'The drafted query used a statement type that is not allowed in this governed preview, so I did not run it.';
441
- case 'unsafe_aggregation':
442
- return 'I drafted a query, but it could change the metric meaning by rounding too early, losing decimal precision, summing a non-additive value, or multiplying rows across a join. I did not run it. Use the governed semantic metric, or review the native grain and join keys before retrying.';
606
+ case 'unsafe_aggregation': {
607
+ const reason = aggregationRefusalReason(aggregationIssueCodes);
608
+ if (reason) {
609
+ return `I drafted a query but did not run it, because ${reason}. Ask for the governed metric and I will use the safe version.`;
610
+ }
611
+ // No classified code reached us — say that plainly rather than listing
612
+ // every cause as if one of them were known to apply.
613
+ return 'I drafted a query, but it would change how the metric is calculated, so I did not run it. Ask for the governed semantic metric and I will use the safe version.';
614
+ }
443
615
  case 'insufficient_context':
444
616
  if (/could not be parsed|parse error|syntax/i.test(machineError)) {
445
617
  return 'I drafted a query, but its SQL syntax did not match the connected warehouse, so I did not run it. The failed draft is available in Inspect; retrying will generate a warehouse-specific query.';
@@ -601,7 +773,10 @@ function tryCrossResultAnswer(input) {
601
773
  const prior = input.followUp?.priorResult;
602
774
  if (!prior || !Array.isArray(prior.rows) || prior.rows.length === 0)
603
775
  return null;
604
- const op = detectResultSetOperation(input.question);
776
+ // Pass the prior result's shape so a follow-up that names one of its columns
777
+ // ("what's the average lifetime_spend?") is recognised without requiring a
778
+ // demonstrative pronoun.
779
+ const op = detectResultSetOperation(input.question, prior);
605
780
  if (!op)
606
781
  return null;
607
782
  const computed = computeResultSetOperation(op, prior);
@@ -696,6 +871,10 @@ async function freezeLegacySemanticSelection(input) {
696
871
  const match = await matchSemanticMetric(input.question, metricNodes, {
697
872
  measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
698
873
  ...(semanticLayer ? { canExecute: (name) => semanticLayer.canComposeMetric(name) } : {}),
874
+ ...(snapshotVectorMetricShortlist(input.contextPack).length
875
+ ? { vectorMetricShortlist: snapshotVectorMetricShortlist(input.contextPack) }
876
+ : {}),
877
+ ...semanticMetricEmbeddingOptions(input.embeddingProvider),
699
878
  }).catch(() => null);
700
879
  if (!match)
701
880
  return undefined;
@@ -813,6 +992,26 @@ async function freezeLegacySemanticSelection(input) {
813
992
  mode: 'authoritative',
814
993
  });
815
994
  }
995
+ /**
996
+ * Reuse the immutable context pack's independent vector lane. Metric matching
997
+ * never opens the catalog or runs a second metadata retrieval pass; it only
998
+ * narrows its vector-only candidate admission to IDs already retrieved for this
999
+ * request snapshot.
1000
+ */
1001
+ function snapshotVectorMetricShortlist(contextPack) {
1002
+ const lane = contextPack?.retrievalDiagnostics.lanes?.find((item) => item.lane === 'vector');
1003
+ if (!lane?.candidates.length || !contextPack)
1004
+ return [];
1005
+ const objects = new Map(contextPack.objects.map((object) => [object.objectKey, object]));
1006
+ const result = [];
1007
+ for (const candidate of lane.candidates) {
1008
+ const object = objects.get(candidate.objectKey);
1009
+ if (!object || (object.objectType !== 'semantic_metric' && object.objectType !== 'semantic_measure'))
1010
+ continue;
1011
+ result.push(candidate.objectKey, object.name);
1012
+ }
1013
+ return [...new Set(result)];
1014
+ }
816
1015
  export async function answer(input) {
817
1016
  const inheritedPlan = !input.resolvedAnalyticalPlan
818
1017
  && input.followUp?.priorResolvedAnalyticalPlan
@@ -1138,6 +1337,20 @@ function cascadeExecutionStatus(result) {
1138
1337
  */
1139
1338
  function deriveAiRoute(result, metricMatch) {
1140
1339
  if (result.kind === 'no_answer') {
1340
+ // Preserve the frozen certified tier as provenance when that exact block
1341
+ // executed but failed its own result contract. The route is not a trust
1342
+ // badge: `certification` remains review-required and no generated fallback
1343
+ // may be selected after freeze.
1344
+ if (result.certifiedResultShapeFailure && result.sourceTier === 'certified_artifact') {
1345
+ const ref = result.sourceCertifiedBlock ?? result.block?.name ?? result.citations[0]?.name;
1346
+ return {
1347
+ tier: 'certified_block',
1348
+ label: ref
1349
+ ? `Certified block ${ref} did not return the complete requested output shape.`
1350
+ : 'The selected certified block did not return the complete requested output shape.',
1351
+ ref,
1352
+ };
1353
+ }
1141
1354
  if (result.exploratoryCandidate) {
1142
1355
  return {
1143
1356
  tier: 'no_answer',
@@ -1201,6 +1414,17 @@ async function runAnswerLoop(input) {
1201
1414
  const scopedContextPack = questionDomainScope.length > 0
1202
1415
  ? scopeContextPackToQuestionDomains(input.contextPack, questionDomainScope, input.manifest)
1203
1416
  : input.contextPack;
1417
+ // The router has already chosen the physical exploratory closure. Treat it
1418
+ // as an execution authority boundary here, before any prompt is constructed
1419
+ // or provider is dispatched. Full retrieval stays on `input.contextPack`
1420
+ // for receipts and diagnostics only.
1421
+ const forcedExploratoryTier = input.selectedCascadeTier === 'exploratory_sql';
1422
+ const exploratoryClosureContextPack = forcedExploratoryTier
1423
+ ? scopeContextPackToExploratoryCandidateClosure(scopedContextPack, input.exploratoryCandidateIds)
1424
+ : undefined;
1425
+ const executionContextPack = forcedExploratoryTier
1426
+ ? exploratoryClosureContextPack
1427
+ : scopedContextPack;
1204
1428
  // Select the RELEVANT skills (not all) for this question; keep pinned project
1205
1429
  // skills (SQL conventions). Block hints still come from the full set so a
1206
1430
  // preferred-block mapping is never lost.
@@ -1242,7 +1466,7 @@ async function runAnswerLoop(input) {
1242
1466
  const semanticHits = kg.search({ query: question, ...searchScope, kinds: SEMANTIC_KINDS, limit: 12 });
1243
1467
  const manifestHits = kg.search({ query: question, ...searchScope, kinds: MANIFEST_KINDS, limit: 12 });
1244
1468
  const considered = mergeHits(artifactHits, semanticHits, manifestHits, kg.search({ query: question, domain, limit: 10 })).slice(0, 30);
1245
- const schemaContext = schemaContextWithAllowedSqlContext(schemaContextWithinQuestionScope(input.schemaContext ?? [], input.contextPack, scopedContextPack), scopedContextPack);
1469
+ const schemaContext = schemaContextWithAllowedSqlContext(schemaContextWithinQuestionScope(input.schemaContext ?? [], input.contextPack, executionContextPack), executionContextPack);
1246
1470
  const catalogRoute = input.contextPack?.routeDecision;
1247
1471
  const baseQuestionPlan = input.contextPack?.questionPlan?.requestedShape
1248
1472
  ? input.contextPack.questionPlan
@@ -1258,12 +1482,35 @@ async function runAnswerLoop(input) {
1258
1482
  // "exact certified" route into the generation prompt. The model should decide
1259
1483
  // from compatible certified evidence, semantic members, and dbt/runtime
1260
1484
  // columns—not copy a customer-grain worked example into a product-grain ask.
1261
- const promptContextPack = contextPackForRequestedShape(scopedContextPack, question, questionPlan, kg);
1485
+ const promptContextPack = contextPackForRequestedShape(executionContextPack, question, questionPlan, kg);
1262
1486
  const repairBudgetState = createCascadeBudgetState(input.cascadeBudgetModel);
1263
- const authoritativePlanBinding = input.resolvedAnalyticalPlan?.mode === 'authoritative'
1487
+ // A pre-freeze exploratory selection is already the router's result of
1488
+ // evaluating certified → semantic → relational eligibility. Do not let the
1489
+ // legacy answer loop independently rediscover a broad block or metric and
1490
+ // replace that decision before it has attempted the selected bounded SQL
1491
+ // path. Frozen tiers retain their existing plan binding behavior.
1492
+ if (forcedExploratoryTier && input.contextPack && !exploratoryClosureContextPack) {
1493
+ 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.';
1494
+ return {
1495
+ kind: 'no_answer',
1496
+ sourceTier: 'no_answer',
1497
+ certification: 'analyst_review_required',
1498
+ reviewStatus: 'none',
1499
+ confidence: 0,
1500
+ text,
1501
+ answer: text,
1502
+ refusalCode: 'grounding_gap',
1503
+ refusalDetails: { code: 'grounding_gap', message: text },
1504
+ citations: contextPackCitations(input.contextPack, 8),
1505
+ contextPack: input.contextPack,
1506
+ considered,
1507
+ providerUsed: provider.name,
1508
+ };
1509
+ }
1510
+ const authoritativePlanBinding = !forcedExploratoryTier && input.resolvedAnalyticalPlan?.mode === 'authoritative'
1264
1511
  ? input.resolvedPlanExecutionBinding
1265
1512
  : undefined;
1266
- if (input.analyticalPeriodResolutionFailure) {
1513
+ if (!forcedExploratoryTier && input.analyticalPeriodResolutionFailure) {
1267
1514
  const failure = input.analyticalPeriodResolutionFailure;
1268
1515
  const structuredFailureCode = failure.error && typeof failure.error === 'object'
1269
1516
  && typeof failure.error.code === 'string'
@@ -1299,7 +1546,7 @@ async function runAnswerLoop(input) {
1299
1546
  providerUsed: provider.name,
1300
1547
  };
1301
1548
  }
1302
- if (input.analyticalExecutionGraphFailure) {
1549
+ if (!forcedExploratoryTier && input.analyticalExecutionGraphFailure) {
1303
1550
  const failure = input.analyticalExecutionGraphFailure;
1304
1551
  const analyticalFailure = analyticalFailureForInput(input, {
1305
1552
  error: { code: failure.code, message: failure.reason },
@@ -1324,7 +1571,7 @@ async function runAnswerLoop(input) {
1324
1571
  providerUsed: provider.name,
1325
1572
  };
1326
1573
  }
1327
- if (input.semanticGraphExecutionBinding?.status === 'blocked') {
1574
+ if (!forcedExploratoryTier && input.semanticGraphExecutionBinding?.status === 'blocked') {
1328
1575
  const failure = input.semanticGraphExecutionBinding;
1329
1576
  const analyticalFailure = analyticalFailureForInput(input, {
1330
1577
  error: { code: failure.code, message: failure.reason },
@@ -1349,7 +1596,7 @@ async function runAnswerLoop(input) {
1349
1596
  providerUsed: provider.name,
1350
1597
  };
1351
1598
  }
1352
- if (input.semanticGraphExecutionBinding?.status === 'ready' && input.analyticalExecutionGraph) {
1599
+ if (!forcedExploratoryTier && input.semanticGraphExecutionBinding?.status === 'ready' && input.analyticalExecutionGraph) {
1353
1600
  return executeSemanticAnalyticalGraph({
1354
1601
  input,
1355
1602
  binding: input.semanticGraphExecutionBinding,
@@ -1359,7 +1606,7 @@ async function runAnswerLoop(input) {
1359
1606
  providerName: provider.name,
1360
1607
  });
1361
1608
  }
1362
- if (input.governedAnalyticalGraphCompilation?.status === 'blocked') {
1609
+ if (!forcedExploratoryTier && input.governedAnalyticalGraphCompilation?.status === 'blocked') {
1363
1610
  const failure = input.governedAnalyticalGraphCompilation;
1364
1611
  const analyticalFailure = analyticalFailureForInput(input, {
1365
1612
  error: { code: failure.code, message: failure.reason },
@@ -1384,7 +1631,7 @@ async function runAnswerLoop(input) {
1384
1631
  providerUsed: provider.name,
1385
1632
  };
1386
1633
  }
1387
- if (input.governedAnalyticalGraphCompilation?.status === 'compiled' && input.analyticalExecutionGraph) {
1634
+ if (!forcedExploratoryTier && input.governedAnalyticalGraphCompilation?.status === 'compiled' && input.analyticalExecutionGraph) {
1388
1635
  return executeGovernedRelationalAnalyticalGraph({
1389
1636
  input,
1390
1637
  compilation: input.governedAnalyticalGraphCompilation,
@@ -1395,7 +1642,7 @@ async function runAnswerLoop(input) {
1395
1642
  });
1396
1643
  }
1397
1644
  const governedRelationalCompilation = input.governedRelationalCompilation;
1398
- if (governedRelationalCompilation?.status === 'blocked') {
1645
+ if (!forcedExploratoryTier && governedRelationalCompilation?.status === 'blocked') {
1399
1646
  const analyticalFailure = analyticalFailureForInput(input, {
1400
1647
  error: { code: governedRelationalCompilation.code, message: governedRelationalCompilation.reason },
1401
1648
  phase: 'compilation',
@@ -1422,7 +1669,7 @@ async function runAnswerLoop(input) {
1422
1669
  providerUsed: provider.name,
1423
1670
  };
1424
1671
  }
1425
- if (governedRelationalCompilation?.status === 'compiled') {
1672
+ if (!forcedExploratoryTier && governedRelationalCompilation?.status === 'compiled') {
1426
1673
  const dqlArtifact = renderGovernedRelationalDqlArtifact(governedRelationalCompilation);
1427
1674
  let result;
1428
1675
  let executionError;
@@ -1486,9 +1733,15 @@ async function runAnswerLoop(input) {
1486
1733
  const text = clarificationOptions?.length
1487
1734
  ? `The previous answer listed ${choices.values.length} ${humanizeDeicticDimension(choices.dimension)}. Which one did you mean?`
1488
1735
  : `The resolved analytical plan cannot execute safely: ${authoritativePlanBinding.reason}`;
1736
+ const frozenCertifiedPlan = input.resolvedAnalyticalPlan?.mode === 'authoritative'
1737
+ && input.resolvedAnalyticalPlan.capability === 'certified_execution';
1738
+ const frozenCertifiedRef = input.resolvedAnalyticalPlan?.executionId;
1489
1739
  return {
1490
1740
  kind: 'no_answer',
1491
- sourceTier: 'no_answer',
1741
+ // A binding/adapter failure after the router selected a certified
1742
+ // execution authority is still a certified-tier terminal. It must not
1743
+ // be relabelled as generated merely because no rows were produced.
1744
+ sourceTier: frozenCertifiedPlan ? 'certified_artifact' : 'no_answer',
1492
1745
  certification: 'analyst_review_required',
1493
1746
  reviewStatus: 'none',
1494
1747
  confidence: 0,
@@ -1500,6 +1753,15 @@ async function runAnswerLoop(input) {
1500
1753
  : 'grounding_gap',
1501
1754
  refusalDetails: { code: authoritativePlanBinding.code, message: authoritativePlanBinding.reason },
1502
1755
  ...(clarificationOptions?.length ? { clarificationOptions } : {}),
1756
+ ...(frozenCertifiedPlan ? {
1757
+ route: {
1758
+ tier: 'certified_block',
1759
+ label: frozenCertifiedRef
1760
+ ? `The selected certified block ${frozenCertifiedRef} could not execute its frozen plan.`
1761
+ : 'The selected certified block could not execute its frozen plan.',
1762
+ ...(frozenCertifiedRef ? { ref: frozenCertifiedRef } : {}),
1763
+ },
1764
+ } : {}),
1503
1765
  citations: contextPackCitations(input.contextPack, 8),
1504
1766
  considered,
1505
1767
  contextPack: input.contextPack,
@@ -1536,14 +1798,20 @@ async function runAnswerLoop(input) {
1536
1798
  const semanticLayerForExec = input.semanticLayer;
1537
1799
  const canExecuteSemanticMetricForMatch = input.canExecuteSemanticMetric
1538
1800
  ?? (semanticLayerForExec ? (name) => semanticLayerForExec.canComposeMetric(name) : undefined);
1539
- let semanticMetricMatch = preferredSemanticMetric
1540
- ? { metric: preferredSemanticMetric, score: 1, basis: 'name' }
1541
- : input.resolvedAnalyticalPlan?.mode === 'authoritative'
1542
- ? null
1543
- : await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
1544
- measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
1545
- ...(canExecuteSemanticMetricForMatch ? { canExecute: canExecuteSemanticMetricForMatch } : {}),
1546
- }).catch(() => null);
1801
+ let semanticMetricMatch = forcedExploratoryTier
1802
+ ? null
1803
+ : preferredSemanticMetric
1804
+ ? { metric: preferredSemanticMetric, score: 1, basis: 'name' }
1805
+ : input.resolvedAnalyticalPlan?.mode === 'authoritative'
1806
+ ? null
1807
+ : await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
1808
+ measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
1809
+ ...(canExecuteSemanticMetricForMatch ? { canExecute: canExecuteSemanticMetricForMatch } : {}),
1810
+ ...(snapshotVectorMetricShortlist(input.contextPack).length
1811
+ ? { vectorMetricShortlist: snapshotVectorMetricShortlist(input.contextPack) }
1812
+ : {}),
1813
+ ...semanticMetricEmbeddingOptions(input.embeddingProvider),
1814
+ }).catch(() => null);
1547
1815
  // Stage 1: certified artifact match. Blocks can be executed; dashboards,
1548
1816
  // Apps, and notebooks are returned as governed citations/navigation targets.
1549
1817
  const drilldownCertifiedHit = input.followUp?.kind === 'drilldown'
@@ -1563,8 +1831,10 @@ async function runAnswerLoop(input) {
1563
1831
  && objectNameInQuestion(question, node)
1564
1832
  && /\b(run|use|open|show|execute|certified|saved|block)\b/i.test(question));
1565
1833
  });
1566
- const shouldTryCertifiedRoute = shouldUseCertifiedRoute(catalogRoute, intent)
1567
- || explicitlyRequestedCertifiedBlock;
1834
+ const shouldTryCertifiedRoute = !forcedExploratoryTier
1835
+ && (shouldUseCertifiedRoute(catalogRoute, intent)
1836
+ || questionPlan.mode === 'definition'
1837
+ || explicitlyRequestedCertifiedBlock);
1568
1838
  const catalogCertifiedHit = shouldTryCertifiedRoute
1569
1839
  ? certifiedHitFromContextPack(input.contextPack, kg)
1570
1840
  : null;
@@ -1574,7 +1844,9 @@ async function runAnswerLoop(input) {
1574
1844
  // metric, so a high-scoring `top_beverage_customers` catalog hit could answer a
1575
1845
  // product-type → product-name flow request with customer rows.
1576
1846
  const unsafeCatalogCertifiedHit = catalogCertifiedHit?.node.kind === 'block'
1577
- && !hasCertifiedNodeFit(question, questionPlan, catalogCertifiedHit.node)
1847
+ && !hasCertifiedNodeFit(question, questionPlan, catalogCertifiedHit.node, {
1848
+ uniqueExactExampleContract: hasUniqueExactCatalogExample(question, input.contextPack, catalogCertifiedHit.node),
1849
+ })
1578
1850
  ? null
1579
1851
  : catalogCertifiedHit;
1580
1852
  const fallbackCertifiedHit = shouldTryCertifiedRoute ? pickCertifiedArtifact({
@@ -1591,13 +1863,85 @@ async function runAnswerLoop(input) {
1591
1863
  && authoritativePlanBinding.kind === 'certified'
1592
1864
  ? authoritativePlanBinding
1593
1865
  : undefined;
1866
+ const namedDefinitionArtifact = input.resolvedAnalyticalPlan?.mode === 'authoritative'
1867
+ ? null
1868
+ : uniqueNamedCertifiedDefinitionArtifact(question, kg, authorizedDomains);
1594
1869
  let artifactHit = authoritativeCertifiedBinding
1595
1870
  ? { node: authoritativeCertifiedBinding.node, score: 1 }
1596
1871
  : input.resolvedAnalyticalPlan?.mode === 'authoritative'
1597
1872
  ? null
1598
- : drilldownCertifiedHit ?? unsafeCatalogCertifiedHit
1873
+ : namedDefinitionArtifact ?? drilldownCertifiedHit ?? unsafeCatalogCertifiedHit
1599
1874
  ?? (catalogCertifiedHit ? null : fallbackCertifiedHit);
1600
1875
  let certifiedExecutionFallback;
1876
+ // A named certified artifact-definition request is documentation about that
1877
+ // artifact, not a request to execute its SQL. The planner strips the
1878
+ // artifact identifier from its output contract, and this deterministic path
1879
+ // returns only authored metadata. It is deliberately narrower than a bare
1880
+ // "what is revenue" question: the user must name the certified block and use
1881
+ // an explicit definition form before execution is bypassed.
1882
+ if (artifactHit?.node.kind === 'block'
1883
+ && artifactHit.node.status === 'certified'
1884
+ && isUniqueNamedCertifiedDefinitionQuestion(question, artifactHit.node, kg, authorizedDomains)) {
1885
+ const artifact = artifactHit.node;
1886
+ const outputs = [...new Set([
1887
+ ...(artifact.outputContract ?? []).map((output) => output.name),
1888
+ ...(artifact.declaredOutputs ?? []),
1889
+ ...(artifact.outputs ?? []).map((output) => output.name),
1890
+ ].map((output) => output.trim()).filter(Boolean))].slice(0, 16);
1891
+ const metadataLines = [
1892
+ artifact.description ?? artifact.llmContext ?? 'No authored description is available for this certified artifact.',
1893
+ outputs.length > 0 ? `Declared outputs: ${outputs.join(', ')}.` : undefined,
1894
+ artifact.grain ? `Grain: ${artifact.grain}.` : undefined,
1895
+ artifact.owner ? `Owner: ${artifact.owner}.` : undefined,
1896
+ 'This is certified artifact metadata, not a query result.',
1897
+ ].filter((line) => Boolean(line));
1898
+ const citations = [{
1899
+ nodeId: artifact.nodeId,
1900
+ kind: artifact.kind,
1901
+ name: artifact.name,
1902
+ gitSha: artifact.gitSha,
1903
+ sourceTier: 'certified_artifact',
1904
+ provenance: artifact.provenance,
1905
+ }];
1906
+ const analysisPlan = buildAnalysisPlan({
1907
+ question,
1908
+ intent: 'definition_lookup',
1909
+ routeReason: 'The question explicitly requested the meaning of one named certified artifact; no SQL execution was requested.',
1910
+ selectedNodes: [artifact],
1911
+ schemaContext,
1912
+ });
1913
+ const text = `Certified artifact **${artifact.name}**\n\n${metadataLines.join('\n\n')}`;
1914
+ return {
1915
+ kind: 'certified',
1916
+ sourceTier: 'certified_artifact',
1917
+ certification: 'certified',
1918
+ reviewStatus: 'certified',
1919
+ confidence: 0.99,
1920
+ text,
1921
+ answer: text,
1922
+ block: artifact,
1923
+ sourceCertifiedBlock: artifact.name,
1924
+ trustLabel: 'certified',
1925
+ citations,
1926
+ memoryContext: input.memoryContext,
1927
+ analysisPlan,
1928
+ evidence: buildCertifiedEvidence({
1929
+ question,
1930
+ artifact,
1931
+ businessHits,
1932
+ semanticHits,
1933
+ manifestHits,
1934
+ considered,
1935
+ executorWasAvailable: false,
1936
+ citations,
1937
+ memoryContext: input.memoryContext ?? [],
1938
+ analysisPlan,
1939
+ }),
1940
+ contextPack: input.contextPack,
1941
+ considered,
1942
+ providerUsed: provider.name,
1943
+ };
1944
+ }
1601
1945
  // Certified remains first when it actually covers the question. If the
1602
1946
  // retrieved block does not fit but a governed semantic metric does, never
1603
1947
  // let the broad catalog match pre-empt Lane 2.
@@ -1706,7 +2050,16 @@ async function runAnswerLoop(input) {
1706
2050
  providerUsed: provider.name,
1707
2051
  };
1708
2052
  }
1709
- const resultShapeWarnings = result ? validateAnswerResultShape(questionPlan, result).warnings : [];
2053
+ const resultShapeWarnings = result
2054
+ ? validateAnswerResultShape(questionPlan, result, {
2055
+ outputBindings: authoritativeCertifiedBinding
2056
+ ? frozenCertifiedResultShapeBindings(input.resolvedAnalyticalPlan, artifactHit.node, questionPlan, {
2057
+ uniqueExactExampleContract: hasUniqueExactCatalogExample(question, input.contextPack, artifactHit.node),
2058
+ })
2059
+ : [],
2060
+ requireBoundMeasures: Boolean(authoritativeCertifiedBinding),
2061
+ }).warnings
2062
+ : [];
1710
2063
  // When a certified block's execution was ATTEMPTED and FAILED, the answer
1711
2064
  // cannot wear the certified badge — a failed run has no data to stand behind.
1712
2065
  // Downgrade to analyst_review_required (the error is surfaced in the text).
@@ -1745,6 +2098,10 @@ async function runAnswerLoop(input) {
1745
2098
  ? undefined
1746
2099
  : questionPlan.requestedShape.topN?.n);
1747
2100
  const authoritativeCertifiedFailure = Boolean(authoritativeCertifiedBinding && executionError);
2101
+ // A selected certified block which returns an incomplete tuple is a
2102
+ // same-tier terminal failure. It is not a reason to reinterpret meaning,
2103
+ // label the partial rows generated, or retry another route after freeze.
2104
+ const authoritativeCertifiedShapeFailure = Boolean(authoritativeCertifiedBinding && !executionError && resultShapeWarnings.length > 0);
1748
2105
  const recoverableCertifiedFailure = artifactHit.node.kind === 'block'
1749
2106
  && executionError !== undefined
1750
2107
  && !authoritativeCertifiedFailure
@@ -1759,18 +2116,38 @@ async function runAnswerLoop(input) {
1759
2116
  artifactHit = null;
1760
2117
  }
1761
2118
  else {
2119
+ const frozenCertifiedRoute = authoritativeCertifiedBinding
2120
+ ? {
2121
+ tier: 'certified_block',
2122
+ label: certifiedShapePassed
2123
+ ? `Answered from certified block ${artifactHit.node.name}`
2124
+ : `The selected certified block ${artifactHit.node.name} could not complete its frozen output contract.`,
2125
+ ref: artifactHit.node.name,
2126
+ }
2127
+ : undefined;
1762
2128
  return {
1763
- kind: authoritativeCertifiedFailure ? 'no_answer' : certifiedShapePassed ? 'certified' : 'uncertified',
1764
- sourceTier: authoritativeCertifiedFailure ? 'no_answer' : sourceTier,
2129
+ kind: authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
2130
+ ? 'no_answer'
2131
+ : certifiedShapePassed ? 'certified' : 'uncertified',
2132
+ // Preserve the selected certified authority even when its own
2133
+ // execution/result contract fails. The failure is terminal and has
2134
+ // review-required trust, but it is not generated SQL.
2135
+ sourceTier,
1765
2136
  certification: certifiedShapePassed ? 'certified' : 'analyst_review_required',
1766
- reviewStatus: authoritativeCertifiedFailure ? 'none' : certifiedShapePassed ? 'certified' : 'analyst_review_required',
2137
+ reviewStatus: authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
2138
+ ? 'none'
2139
+ : certifiedShapePassed ? 'certified' : 'analyst_review_required',
1767
2140
  confidence: certifiedShapePassed ? 0.95 : 0.45,
1768
2141
  text,
1769
2142
  answer: text,
1770
2143
  block: artifactHit.node.kind === 'block' ? artifactHit.node : undefined,
1771
2144
  result,
1772
2145
  executionError,
1773
- ...(authoritativeCertifiedFailure ? { refusalCode: 'grounding_gap' } : {}),
2146
+ ...(authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
2147
+ ? { refusalCode: 'grounding_gap' }
2148
+ : {}),
2149
+ ...(authoritativeCertifiedShapeFailure ? { certifiedResultShapeFailure: true } : {}),
2150
+ ...(frozenCertifiedRoute ? { route: frozenCertifiedRoute } : {}),
1774
2151
  sql: result?.sql,
1775
2152
  dqlArtifact,
1776
2153
  trustLabel: certifiedShapePassed ? input.contextPack?.trustLabel ?? 'certified' : 'mixed',
@@ -1808,7 +2185,7 @@ async function runAnswerLoop(input) {
1808
2185
  // a governed metric can answer. Match by name + synonyms + measure family + hybrid
1809
2186
  // rank over the FTS semantic hits, then ALL metric KG nodes (revenue ⇄
1810
2187
  // cumulative_revenue). Certified-first is still preserved (checked above).
1811
- const clarifyBeforeGeneration = shouldClarifyBeforeGeneration({
2188
+ const clarifyBeforeGeneration = !forcedExploratoryTier && shouldClarifyBeforeGeneration({
1812
2189
  intent,
1813
2190
  routeDecision: catalogRoute,
1814
2191
  hasSemanticMetricMatch: Boolean(semanticMetricMatch),
@@ -1866,12 +2243,14 @@ async function runAnswerLoop(input) {
1866
2243
  // wins over raw dbt manifest context; memory is appended last as advisory.
1867
2244
  // A confident metric match forces the semantic tier even when FTS returned no
1868
2245
  // semantic hits, so the governed metric (not refusal) answers the question.
1869
- const activeTier = sourceTierFromContextPack(input.contextPack)
1870
- ?? (semanticHits.length > 0 || semanticMetricMatch
1871
- ? 'semantic_layer'
1872
- : manifestHits.length > 0
1873
- ? 'dbt_manifest'
1874
- : 'dbt_manifest');
2246
+ const activeTier = forcedExploratoryTier
2247
+ ? 'dbt_manifest'
2248
+ : sourceTierFromContextPack(input.contextPack)
2249
+ ?? (semanticHits.length > 0 || semanticMetricMatch
2250
+ ? 'semantic_layer'
2251
+ : manifestHits.length > 0
2252
+ ? 'dbt_manifest'
2253
+ : 'dbt_manifest');
1875
2254
  const reviewRequiredArtifactHits = artifactHits
1876
2255
  .filter((hit) => hit.score >= CERTIFIED_HIT_THRESHOLD && !isCertifiedHit(hit, kg))
1877
2256
  .slice(0, 4);
@@ -1904,7 +2283,13 @@ async function runAnswerLoop(input) {
1904
2283
  { hits: businessHits.slice(0, 4), reserve: 3 },
1905
2284
  { hits: manifestHits, reserve: 3 },
1906
2285
  ], 14);
1907
- 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));
2286
+ // A frozen exploratory selection gets its relation/column/proof context only
2287
+ // from `executionContextPack`. KG hit prose is useful diagnostics, but it
2288
+ // is not execution authority and must not tell a provider about another
2289
+ // same-snapshot relation it could then attempt to query.
2290
+ const contextNodes = forcedExploratoryTier
2291
+ ? []
2292
+ : 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));
1908
2293
  const kgJoinPathHints = buildKgJoinPathHints(kg, contextNodes, questionPlan);
1909
2294
  const contextBlocks = contextNodes.filter((node) => {
1910
2295
  if (node.kind !== 'block')
@@ -1924,10 +2309,10 @@ async function runAnswerLoop(input) {
1924
2309
  const messages = [
1925
2310
  { role: 'system', content: SYSTEM_PROMPT },
1926
2311
  ];
1927
- const skillsPrompt = buildSkillsPrompt(selectedSkills, userId ?? null);
2312
+ const skillsPrompt = forcedExploratoryTier ? '' : buildSkillsPrompt(selectedSkills, userId ?? null);
1928
2313
  if (skillsPrompt)
1929
2314
  messages.push({ role: 'system', content: skillsPrompt });
1930
- const analyticalPlan = input.manifest
2315
+ const analyticalPlan = !forcedExploratoryTier && input.manifest
1931
2316
  ? planAnalyticalPath(input.manifest, {
1932
2317
  entityIds: inferAnalyticalEntityIds(question, contextNodes, input.manifest),
1933
2318
  ownerDomain: input.domainContext?.activeDomain ?? input.domain,
@@ -2279,7 +2664,7 @@ async function runAnswerLoop(input) {
2279
2664
  // (pre-validated, since the wrapper only restricts an already-certified result)
2280
2665
  // and is labeled BELOW certified. Falls through to generation on any miss.
2281
2666
  let certifiedAdaptation;
2282
- if (!semanticBridgeAnswer && !metricFirst && input.executeGeneratedSql) {
2667
+ if (!forcedExploratoryTier && !semanticBridgeAnswer && !metricFirst && input.executeGeneratedSql) {
2283
2668
  const fit = input.contextPack?.routeDecision?.blockFit;
2284
2669
  const sourceBlock = input.contextPack?.allowedSqlContext?.sourceBlockSql?.[0];
2285
2670
  if (fit && sourceBlock?.sql) {
@@ -2317,7 +2702,18 @@ async function runAnswerLoop(input) {
2317
2702
  onCompiled: (record) => compiledSemanticRecords.push(record),
2318
2703
  }),
2319
2704
  ];
2320
- if (semanticBridgeAnswer) {
2705
+ if (input.forcedGeneratedProposal) {
2706
+ // The analyst loop already did its provider/tool work. Re-entering ordinary
2707
+ // generation here would create a second proposal whose SQL is not covered
2708
+ // by the request-scoped execution capability.
2709
+ parsed = {
2710
+ sql: input.forcedGeneratedProposal.sql,
2711
+ text: input.forcedGeneratedProposal.summary
2712
+ ?? 'Prepared from identifiers observed during this bounded analyst run. Review-required until an analyst promotes it.',
2713
+ ...(input.forcedGeneratedProposal.suggestedViz ? { viz: input.forcedGeneratedProposal.suggestedViz } : {}),
2714
+ };
2715
+ }
2716
+ else if (semanticBridgeAnswer) {
2321
2717
  governedMetricAnswer = true;
2322
2718
  parsed = {
2323
2719
  sql: semanticBridgeAnswer.sql,
@@ -2503,7 +2899,7 @@ async function runAnswerLoop(input) {
2503
2899
  let deepCandidateResult;
2504
2900
  let deepCandidateExecutionError;
2505
2901
  let deepCandidateNotes = [];
2506
- if (!governedMetricAnswer && input.analysisDepth === 'deep' && parsed.sql) {
2902
+ if (!input.forcedGeneratedProposal && !governedMetricAnswer && input.analysisDepth === 'deep' && parsed.sql) {
2507
2903
  const selection = await selectDeepGeneratedProposalCandidate({
2508
2904
  provider,
2509
2905
  messages,
@@ -2705,7 +3101,12 @@ async function runAnswerLoop(input) {
2705
3101
  };
2706
3102
  }
2707
3103
  }
2708
- const tightenedFlow = tightenSourceTargetFlowProjection(parsed.sql, question, questionPlan);
3104
+ // The analyst capability binds the exact composed bytes. Any projection
3105
+ // tightening is a new SQL proposal and therefore must wait for a fresh
3106
+ // analyst pass/capability rather than silently rewriting this one.
3107
+ const tightenedFlow = input.forcedGeneratedProposal
3108
+ ? undefined
3109
+ : tightenSourceTargetFlowProjection(parsed.sql, question, questionPlan);
2709
3110
  if (tightenedFlow && tightenedFlow.sql !== parsed.sql) {
2710
3111
  parsed.sql = tightenedFlow.sql;
2711
3112
  parsed.outputs = tightenedFlow.outputs;
@@ -2723,7 +3124,7 @@ async function runAnswerLoop(input) {
2723
3124
  // model emitted to its real warehouse relation from the runtime schema BEFORE
2724
3125
  // governance validation. Same resolver the build path uses — one grounding,
2725
3126
  // no weak path. `allowedSqlContext` relations are already qualified.
2726
- if (parsed.sql) {
3127
+ if (parsed.sql && !input.forcedGeneratedProposal) {
2727
3128
  parsed.sql = contextLedger.qualifySql(parsed.sql).sql;
2728
3129
  }
2729
3130
  // Validation gate. Governed metric SQL synthesized from the semantic layer is
@@ -2840,7 +3241,8 @@ async function runAnswerLoop(input) {
2840
3241
  // that predicate entirely, inject it deterministically before spending the
2841
3242
  // bounded AI validation repair. Misbound or ambiguous predicates are never
2842
3243
  // rewritten here; they continue through the guarded correction/refusal path.
2843
- if (!contextValidation.ok
3244
+ if (!input.forcedGeneratedProposal
3245
+ && !contextValidation.ok
2844
3246
  && contextValidation.code === 'misbound_filter'
2845
3247
  && !contextValidation.offending?.relation
2846
3248
  && !contextValidation.offending?.column
@@ -2892,7 +3294,8 @@ async function runAnswerLoop(input) {
2892
3294
  // stands instead of guessing a measure.
2893
3295
  // 2) Otherwise fall back to a CLEAN governed-metric definition (direct or exact
2894
3296
  // leaf-measure; no fuzzy family guess that could answer the wrong measure).
2895
- if (!contextValidation.ok && contextValidation.code !== 'unsafe_aggregation' && semanticMetricRoute && semanticMetricMatch && !rankedGrainGap) {
3297
+ if (!input.forcedGeneratedProposal
3298
+ && !contextValidation.ok && contextValidation.code !== 'unsafe_aggregation' && semanticMetricRoute && semanticMetricMatch && !rankedGrainGap) {
2896
3299
  const grounded = contextLedger.validateRuntimeGrounding(parsed.sql);
2897
3300
  // Runtime grounding proves that relations and columns exist; it cannot prove
2898
3301
  // that a resolved business member was bound to the right dimension. Never
@@ -2922,7 +3325,8 @@ async function runAnswerLoop(input) {
2922
3325
  // nothing, or found the table and something else rejected the SQL — which are
2923
3326
  // three different fixes.
2924
3327
  let regroundDiagnostic;
2925
- if (!contextValidation.ok && contextValidation.code !== 'unsafe_aggregation' && !governedMetricAnswer && input.expandGroundingContext && canUseLaneRepair(repairBudgetState, 'reground')) {
3328
+ if (!input.forcedGeneratedProposal
3329
+ && !contextValidation.ok && contextValidation.code !== 'unsafe_aggregation' && !governedMetricAnswer && input.expandGroundingContext && canUseLaneRepair(repairBudgetState, 'reground')) {
2926
3330
  recordLaneRepair(repairBudgetState, 'reground');
2927
3331
  try {
2928
3332
  const expansion = await input.expandGroundingContext({
@@ -3019,6 +3423,7 @@ async function runAnswerLoop(input) {
3019
3423
  && input.resolvedAnalyticalPlan?.mode !== 'authoritative'
3020
3424
  && !semanticAggregationAuthorityBlocked
3021
3425
  && !governedMetricAnswer
3426
+ && !input.forcedGeneratedProposal
3022
3427
  && canUseLaneRepair(repairBudgetState, 'validation')) {
3023
3428
  recordLaneRepair(repairBudgetState, 'validation');
3024
3429
  try {
@@ -3094,7 +3499,7 @@ async function runAnswerLoop(input) {
3094
3499
  // Business-language chat text; the validator's machine message (with
3095
3500
  // relation ids and tool guidance for the repair prompt) stays in
3096
3501
  // refusalDetails + validationWarnings for the Inspect surface.
3097
- const text = renderContextValidationRefusalForUser(contextValidation.code, contextValidation.error, input.followUp?.memberBindings);
3502
+ const text = renderContextValidationRefusalForUser(contextValidation.code, contextValidation.error, input.followUp?.memberBindings, contextValidation.aggregationSafetyProof?.issueCodes);
3098
3503
  const analysisPlan = buildAnalysisPlan({
3099
3504
  question,
3100
3505
  intent,
@@ -3258,7 +3663,13 @@ async function runAnswerLoop(input) {
3258
3663
  let result;
3259
3664
  let executionError;
3260
3665
  let warehouseFailure;
3666
+ let exploratoryClosureDenied = false;
3261
3667
  let repairAttempts = 0;
3668
+ // A forced proposal may originate from the bounded analyst loop, but it
3669
+ // cannot bypass a router-owned exploratory decision. In that case the host
3670
+ // still prepares and freezes the exact validated SQL before execution.
3671
+ const exploratoryExecutionSelected = input.selectedCascadeTier === 'exploratory_sql';
3672
+ let preparedExploratoryExecution;
3262
3673
  // Repair candidates that were generated but rejected before execution. These
3263
3674
  // are diagnostics ABOUT the recovery attempt, not the reason the run failed,
3264
3675
  // so they are reported alongside the original error rather than replacing it.
@@ -3273,7 +3684,7 @@ async function runAnswerLoop(input) {
3273
3684
  executionError = deepCandidateExecutionError;
3274
3685
  warehouseFailure = normalizeWarehouseSqlFailure(deepCandidateExecutionError, input.semanticDriver);
3275
3686
  }
3276
- const executeCurrentSql = () => {
3687
+ const executeCurrentSql = async () => {
3277
3688
  const requestedLimit = questionPlan.requestedShape.topN?.scope === 'per_group'
3278
3689
  ? 200
3279
3690
  : questionPlan.requestedShape.topN?.n ?? 200;
@@ -3283,8 +3694,6 @@ async function runAnswerLoop(input) {
3283
3694
  && semanticBridgeAnswer?.sql.trim() === parsed.sql?.trim()) {
3284
3695
  return input.executeDqlArtifact({ ...semanticArtifact, limit: requestedLimit });
3285
3696
  }
3286
- if (!input.executeGeneratedSql)
3287
- throw new Error('No generated SQL executor is configured.');
3288
3697
  const artifact = buildGeneratedSqlDqlArtifact({
3289
3698
  question,
3290
3699
  sql: parsed.sql,
@@ -3300,9 +3709,35 @@ async function runAnswerLoop(input) {
3300
3709
  validationWarnings: [...contextValidation.warnings, ...deepCandidateNotes],
3301
3710
  outputs: parsed.outputs,
3302
3711
  });
3303
- return input.executeGeneratedSql(parsed.sql, artifact ? { ...artifact, limit: requestedLimit } : undefined);
3712
+ const boundedArtifact = artifact ? { ...artifact, limit: requestedLimit } : undefined;
3713
+ if (input.forcedGeneratedProposal && !exploratoryExecutionSelected) {
3714
+ if (!input.agenticSqlExecutionCapability || !input.executeAgenticGeneratedSql) {
3715
+ throw Object.assign(new Error('The analyst-approved SQL has no live execution capability, so DQL did not execute it.'), {
3716
+ code: 'AGENTIC_EXECUTION_CAPABILITY_REQUIRED',
3717
+ });
3718
+ }
3719
+ return input.executeAgenticGeneratedSql(input.agenticSqlExecutionCapability, parsed.sql, boundedArtifact);
3720
+ }
3721
+ if (exploratoryExecutionSelected) {
3722
+ if (!input.prepareExploratorySqlExecution || !input.executeAgenticGeneratedSql) {
3723
+ throw Object.assign(new Error('The selected exploratory SQL has no host-authorized execution capability, so DQL did not execute it.'), {
3724
+ code: 'EXPLORATORY_EXECUTION_CAPABILITY_REQUIRED',
3725
+ });
3726
+ }
3727
+ // This is the only point at which a router-selected exploratory proposal
3728
+ // crosses from validated text into executable authority. The callback
3729
+ // rechecks the snapshot, physical target, and qualified runtime columns,
3730
+ // then returns a one-shot capability bound to these exact SQL bytes.
3731
+ preparedExploratoryExecution ??= await input.prepareExploratorySqlExecution(parsed.sql, boundedArtifact);
3732
+ return input.executeAgenticGeneratedSql(preparedExploratoryExecution.capability, parsed.sql, boundedArtifact);
3733
+ }
3734
+ if (!input.executeGeneratedSql)
3735
+ throw new Error('No generated SQL executor is configured.');
3736
+ return input.executeGeneratedSql(parsed.sql, boundedArtifact);
3304
3737
  };
3305
- if (input.executeGeneratedSql && !result) {
3738
+ if ((input.executeGeneratedSql
3739
+ || (input.forcedGeneratedProposal && input.executeAgenticGeneratedSql)
3740
+ || exploratoryExecutionSelected) && !result) {
3306
3741
  // Fanout gate for native semantic direct-joins: a duplicate join-key row on
3307
3742
  // the joined side multiplies fact rows BEFORE aggregation, so every summed
3308
3743
  // value inflates silently (seen in the field as trillions-scale "governed"
@@ -3311,6 +3746,9 @@ async function runAnswerLoop(input) {
3311
3746
  // repair, which would regenerate the same multiplying join.
3312
3747
  let fanoutContradiction = false;
3313
3748
  if (!executionError
3749
+ && !input.forcedGeneratedProposal
3750
+ && !exploratoryExecutionSelected
3751
+ && input.executeGeneratedSql
3314
3752
  && semanticBridgeAnswer?.composeResult?.fanoutProbeSql
3315
3753
  && semanticBridgeAnswer.sql.trim() === parsed.sql?.trim()) {
3316
3754
  const inflationError = await probeSemanticJoinFanout(semanticBridgeAnswer.composeResult.fanoutProbeSql, semanticBridgeAnswer.composeResult.tables, input.executeGeneratedSql);
@@ -3324,10 +3762,24 @@ async function runAnswerLoop(input) {
3324
3762
  result = await executeCurrentSql();
3325
3763
  }
3326
3764
  catch (err) {
3765
+ const executionCode = err && typeof err === 'object'
3766
+ && typeof err.code === 'string'
3767
+ ? err.code
3768
+ : undefined;
3769
+ // A router-selected exploratory proposal that leaves its candidate
3770
+ // closure is not an executable draft. It is a pre-capability safety
3771
+ // denial: do not present its SQL/artifact as review-required work after
3772
+ // the host rejected it.
3773
+ exploratoryClosureDenied = exploratoryExecutionSelected
3774
+ && executionCode === 'UNAUTHORIZED_SQL';
3327
3775
  warehouseFailure = normalizeWarehouseSqlFailure(err, input.semanticDriver);
3328
3776
  executionError = warehouseFailure.redactedMessage;
3329
3777
  }
3330
- if (executionError && !fanoutContradiction && !authoritativeSemanticBinding) {
3778
+ if (executionError
3779
+ && !input.forcedGeneratedProposal
3780
+ && !exploratoryExecutionSelected
3781
+ && !fanoutContradiction
3782
+ && !authoritativeSemanticBinding) {
3331
3783
  warehouseFailure ??= normalizeWarehouseSqlFailure(executionError, input.semanticDriver);
3332
3784
  if (isRetryableGeneratedSqlError(warehouseFailure)) {
3333
3785
  const localRepairSql = repairGeneratedSqlLocally(parsed.sql, executionError, schemaContext);
@@ -3438,10 +3890,15 @@ async function runAnswerLoop(input) {
3438
3890
  : 'The question asks for a custom analysis, ranking, breakdown, comparison, or grain that should not be answered by a loose certified block match.'),
3439
3891
  selectedNodes: contextNodes,
3440
3892
  schemaContext,
3441
- sql: parsed.sql,
3893
+ // A router-selected physical-closure denial happens before a capability
3894
+ // exists. Its rejected bytes are diagnostics for the host, never a SQL
3895
+ // preview that a user can review, copy, or turn into a draft.
3896
+ sql: exploratoryClosureDenied ? undefined : parsed.sql,
3442
3897
  suggestedViz: parsed.viz ?? 'table',
3443
3898
  assumptions: [
3444
- 'The SQL preview is uncertified until an analyst reviews and promotes the DQL artifact.',
3899
+ ...(exploratoryClosureDenied
3900
+ ? ['The router-selected physical closure rejected the generated SQL before capability minting.']
3901
+ : ['The SQL preview is uncertified until an analyst reviews and promotes the DQL artifact.']),
3445
3902
  ...(certifiedExecutionFallback
3446
3903
  ? [`Certified block ${certifiedExecutionFallback.node.name} failed execution and was bypassed: ${certifiedExecutionFallback.error}`]
3447
3904
  : []),
@@ -3493,21 +3950,23 @@ async function runAnswerLoop(input) {
3493
3950
  const generatedOutputs = parsed.outputs?.length ? parsed.outputs : resultColumnNames(result);
3494
3951
  const generatedRequestedFilters = mergeProposalStringLists(input.followUp?.filters, parsed.requestedFilters);
3495
3952
  const generatedRequestedDimensions = mergeProposalStringLists(input.followUp?.dimensions, parsed.requestedDimensions);
3496
- const baseDqlArtifact = result?.dqlArtifact ?? semanticBridgeAnswer?.dqlArtifact ?? buildGeneratedSqlDqlArtifact({
3497
- question,
3498
- sql: parsed.sql,
3499
- intent,
3500
- domain,
3501
- followUp: input.followUp,
3502
- contextPack: input.contextPack,
3503
- sourceBlock: followUpSourceBlock ?? undefined,
3504
- sourceDqlArtifact: input.followUp?.priorDqlArtifact,
3505
- proposedEntity: parsed.proposedEntity,
3506
- requestedFilters: generatedRequestedFilters,
3507
- requestedDimensions: generatedRequestedDimensions,
3508
- validationWarnings,
3509
- outputs: generatedOutputs,
3510
- });
3953
+ const baseDqlArtifact = exploratoryClosureDenied
3954
+ ? undefined
3955
+ : result?.dqlArtifact ?? semanticBridgeAnswer?.dqlArtifact ?? buildGeneratedSqlDqlArtifact({
3956
+ question,
3957
+ sql: parsed.sql,
3958
+ intent,
3959
+ domain,
3960
+ followUp: input.followUp,
3961
+ contextPack: input.contextPack,
3962
+ sourceBlock: followUpSourceBlock ?? undefined,
3963
+ sourceDqlArtifact: input.followUp?.priorDqlArtifact,
3964
+ proposedEntity: parsed.proposedEntity,
3965
+ requestedFilters: generatedRequestedFilters,
3966
+ requestedDimensions: generatedRequestedDimensions,
3967
+ validationWarnings,
3968
+ outputs: generatedOutputs,
3969
+ });
3511
3970
  const requestedTopN = questionPlan.requestedShape.topN?.scope === 'per_group'
3512
3971
  ? undefined
3513
3972
  : questionPlan.requestedShape.topN?.n;
@@ -3520,7 +3979,7 @@ async function runAnswerLoop(input) {
3520
3979
  : undefined;
3521
3980
  let draftBlock;
3522
3981
  let draftCaptureError;
3523
- if (input.captureGeneratedDraft && parsed.sql) {
3982
+ if (!exploratoryClosureDenied && input.captureGeneratedDraft && parsed.sql) {
3524
3983
  try {
3525
3984
  draftBlock = await input.captureGeneratedDraft({
3526
3985
  question,
@@ -3567,32 +4026,38 @@ async function runAnswerLoop(input) {
3567
4026
  : undefined;
3568
4027
  const certifiedMetricAnswer = semanticMetricCertification === 'certified' || semanticMetricCertification === 'reviewed';
3569
4028
  const governedMetricExecutionFailure = governedMetricAnswer && Boolean(executionError);
4029
+ const terminalExecutionFailure = governedMetricExecutionFailure || exploratoryClosureDenied;
3570
4030
  const finalSemanticExecutionTrace = semanticTraceAfterExecution(semanticExecutionTrace, {
3571
4031
  executed: Boolean(result),
3572
4032
  ...(executionError ? { error: executionError } : {}),
3573
4033
  ...(result ? { result } : {}),
3574
4034
  });
3575
4035
  return {
3576
- kind: governedMetricExecutionFailure ? 'no_answer' : 'uncertified',
3577
- sourceTier: governedMetricExecutionFailure ? 'no_answer' : governedMetricAnswer ? 'semantic_layer' : activeTier,
3578
- certification: governedMetricExecutionFailure ? 'analyst_review_required' : governedMetricAnswer ? 'governed' : 'ai_generated',
3579
- reviewStatus: governedMetricExecutionFailure ? 'none' : governedMetricAnswer ? 'governed' : 'draft_ready',
4036
+ kind: terminalExecutionFailure ? 'no_answer' : 'uncertified',
4037
+ sourceTier: terminalExecutionFailure ? 'no_answer' : governedMetricAnswer ? 'semantic_layer' : activeTier,
4038
+ certification: terminalExecutionFailure ? 'analyst_review_required' : governedMetricAnswer ? 'governed' : 'ai_generated',
4039
+ reviewStatus: terminalExecutionFailure ? 'none' : governedMetricAnswer ? 'governed' : 'draft_ready',
3580
4040
  semanticMetricCertification,
3581
- confidence: governedMetricExecutionFailure ? 0 : certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
3582
- text: generatedText,
3583
- answer: generatedText,
3584
- proposedSql: parsed.sql,
3585
- sql: parsed.sql,
4041
+ confidence: terminalExecutionFailure ? 0 : certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
4042
+ text: exploratoryClosureDenied
4043
+ ? 'The generated SQL referenced a relation outside the router-selected physical closure, so DQL did not execute it.'
4044
+ : generatedText,
4045
+ answer: exploratoryClosureDenied
4046
+ ? 'The generated SQL referenced a relation outside the router-selected physical closure, so DQL did not execute it.'
4047
+ : generatedText,
4048
+ ...(exploratoryClosureDenied ? {} : { proposedSql: parsed.sql, sql: parsed.sql }),
3586
4049
  result,
3587
4050
  executionError,
3588
4051
  ...(warehouseFailure ? { warehouseFailure } : {}),
3589
4052
  ...(finalSemanticExecutionTrace ? { semanticExecutionTrace: finalSemanticExecutionTrace } : {}),
3590
- ...(governedMetricExecutionFailure ? { refusalCode: 'grounding_gap' } : {}),
4053
+ ...(terminalExecutionFailure ? { refusalCode: 'grounding_gap' } : {}),
3591
4054
  suggestedViz: parsed.viz ?? 'table',
3592
- dqlArtifact: answerDqlArtifact,
3593
- draftBlock,
3594
- draftBlockId: draftBlock?.path,
3595
- promoteCommand: draftBlock ? `dql certify --from-draft ${draftBlock.path}` : undefined,
4055
+ ...(exploratoryClosureDenied ? {} : { dqlArtifact: answerDqlArtifact }),
4056
+ ...(exploratoryClosureDenied ? {} : {
4057
+ draftBlock,
4058
+ draftBlockId: draftBlock?.path,
4059
+ promoteCommand: draftBlock ? `dql certify --from-draft ${draftBlock.path}` : undefined,
4060
+ }),
3596
4061
  trustLabel: input.contextPack?.trustLabel,
3597
4062
  sourceCertifiedBlock,
3598
4063
  contextPackId: input.contextPack?.id,
@@ -3630,6 +4095,7 @@ async function runAnswerLoop(input) {
3630
4095
  considered,
3631
4096
  providerUsed: provider.name,
3632
4097
  aggregationSafetyProof,
4098
+ ...(preparedExploratoryExecution ? { exploratoryExecutionFreeze: preparedExploratoryExecution.freeze } : {}),
3633
4099
  // Carry the governed metric match so the exit point can name a
3634
4100
  // `semantic_metric` route (spec 17, part C).
3635
4101
  _semanticMetricMatch: governedMetricAnswer ? semanticMetricMatch ?? undefined : undefined,
@@ -4718,6 +5184,147 @@ function scopeContextPackToQuestionDomains(contextPack, domains, manifest) {
4718
5184
  },
4719
5185
  };
4720
5186
  }
5187
+ /**
5188
+ * Make the router-selected exploratory physical closure the only SQL authority
5189
+ * visible to generation and execution. The broad context pack remains on the
5190
+ * run as diagnostics, but must not make a same-snapshot neighbouring relation
5191
+ * executable merely because it ranked well for the surrounding question.
5192
+ *
5193
+ * Matching is canonical-ID only. In particular this deliberately does not
5194
+ * fall back to relation/model leaf names: duplicate `orders` relations across
5195
+ * schemas/domains must not inherit each other's proof.
5196
+ */
5197
+ export function scopeContextPackToExploratoryCandidateClosure(contextPack, candidateIds) {
5198
+ if (!contextPack || !candidateIds?.length)
5199
+ return undefined;
5200
+ const requestedIds = new Set(candidateIds.map(normalizeExploratoryCandidateIdentity).filter(Boolean));
5201
+ if (requestedIds.size === 0)
5202
+ return undefined;
5203
+ const intersects = (values) => {
5204
+ for (const value of values) {
5205
+ const normalized = normalizeExploratoryCandidateIdentity(value ?? '');
5206
+ if (normalized && requestedIds.has(normalized))
5207
+ return true;
5208
+ }
5209
+ return false;
5210
+ };
5211
+ const objectIdentities = (object) => {
5212
+ const payload = object.payload ?? {};
5213
+ return [
5214
+ object.objectKey,
5215
+ object.fullName,
5216
+ payload.qualifiedId,
5217
+ payload.uniqueId,
5218
+ payload.id,
5219
+ payload.localId,
5220
+ payload.relation,
5221
+ payload.table,
5222
+ payload.model,
5223
+ ...metadataStringValues(payload.sourceObjects),
5224
+ ...metadataStringValues(payload.sourceRelations),
5225
+ ...metadataStringValues(payload.tableDependencies),
5226
+ ].filter((value) => typeof value === 'string');
5227
+ };
5228
+ const objectRelationReferences = (object) => {
5229
+ const payload = object.payload ?? {};
5230
+ return [
5231
+ payload.relation,
5232
+ payload.table,
5233
+ payload.model,
5234
+ ...metadataStringValues(payload.sourceObjects),
5235
+ ...metadataStringValues(payload.sourceRelations),
5236
+ ...metadataStringValues(payload.tableDependencies),
5237
+ ].filter((value) => typeof value === 'string');
5238
+ };
5239
+ const directObjects = contextPack.objects.filter((object) => intersects(objectIdentities(object)));
5240
+ const requestedRelations = new Set();
5241
+ for (const object of directObjects) {
5242
+ for (const relation of objectRelationReferences(object)) {
5243
+ const normalized = normalizeExploratoryCandidateIdentity(relation);
5244
+ if (normalized)
5245
+ requestedRelations.add(normalized);
5246
+ }
5247
+ }
5248
+ for (const relation of contextPack.allowedSqlContext.relations) {
5249
+ if (intersects([relation.objectKey, relation.relation])) {
5250
+ requestedRelations.add(normalizeExploratoryCandidateIdentity(relation.relation));
5251
+ }
5252
+ }
5253
+ const relations = contextPack.allowedSqlContext.relations.filter((relation) => {
5254
+ const normalized = normalizeExploratoryCandidateIdentity(relation.relation);
5255
+ return requestedIds.has(normalized)
5256
+ || requestedRelations.has(normalized)
5257
+ || Boolean(relation.objectKey && directObjects.some((object) => object.objectKey === relation.objectKey));
5258
+ });
5259
+ // A router-selected exploratory attempt without a physical relation is not
5260
+ // executable. Returning undefined is intentionally fail-closed; callers may
5261
+ // preserve the full pack for receipts but may not use it as SQL authority.
5262
+ if (relations.length === 0)
5263
+ return undefined;
5264
+ const relationIds = new Set(relations.map((relation) => normalizeExploratoryCandidateIdentity(relation.relation)));
5265
+ const relationshipEndpointIds = new Set();
5266
+ for (const object of directObjects.filter((object) => object.objectType === 'relationship')) {
5267
+ const payload = object.payload ?? {};
5268
+ for (const endpoint of [payload.from, payload.to]) {
5269
+ if (typeof endpoint === 'string') {
5270
+ const normalized = normalizeExploratoryCandidateIdentity(endpoint);
5271
+ if (normalized)
5272
+ relationshipEndpointIds.add(normalized);
5273
+ }
5274
+ }
5275
+ }
5276
+ const objects = contextPack.objects.filter((object) => {
5277
+ if (directObjects.includes(object))
5278
+ return true;
5279
+ const identities = objectIdentities(object).map(normalizeExploratoryCandidateIdentity);
5280
+ if (identities.some((identity) => relationIds.has(identity) || relationshipEndpointIds.has(identity)))
5281
+ return true;
5282
+ return objectRelationReferences(object)
5283
+ .map(normalizeExploratoryCandidateIdentity)
5284
+ .some((identity) => relationIds.has(identity));
5285
+ });
5286
+ const objectKeys = new Set(objects.map((object) => object.objectKey));
5287
+ const relationKeys = new Set(relations.map((relation) => normalizeRelationKey(relation.relation)));
5288
+ const selectedRelations = contextPack.retrievalDiagnostics.selectedRelations?.filter((relation) => relationKeys.has(normalizeRelationKey(relation.relation)));
5289
+ const selectedJoinPaths = contextPack.retrievalDiagnostics.selectedJoinPaths?.filter((path) => relationKeys.has(normalizeRelationKey(path.leftRelation))
5290
+ && relationKeys.has(normalizeRelationKey(path.rightRelation)));
5291
+ return {
5292
+ ...contextPack,
5293
+ focusObjectKey: contextPack.focusObjectKey && objectKeys.has(contextPack.focusObjectKey)
5294
+ ? contextPack.focusObjectKey
5295
+ : objects[0]?.objectKey ?? null,
5296
+ objects,
5297
+ skills: [],
5298
+ edges: contextPack.edges.filter((edge) => objectKeys.has(edge.fromKey) && objectKeys.has(edge.toKey)),
5299
+ citations: contextPack.citations.filter((citation) => objectKeys.has(citation.objectKey)),
5300
+ evidenceSummaries: contextPack.evidenceSummaries.filter((summary) => !summary.objectKey || objectKeys.has(summary.objectKey)),
5301
+ evidenceRoles: contextPack.evidenceRoles.filter((role) => objectKeys.has(role.objectKey)),
5302
+ allowedSqlContext: {
5303
+ relations,
5304
+ // Authored block SQL is not a physical exploratory authority. The
5305
+ // selected dbt/runtime relation closure above is the whole prompt/SQL
5306
+ // boundary for this tier.
5307
+ sourceBlockSql: [],
5308
+ },
5309
+ retrievalDiagnostics: {
5310
+ ...contextPack.retrievalDiagnostics,
5311
+ selectedObjects: objects.length,
5312
+ selectedEvidence: contextPack.retrievalDiagnostics.selectedEvidence.filter((evidence) => objectKeys.has(evidence.objectKey)),
5313
+ selectedRelations,
5314
+ selectedJoinPaths,
5315
+ schemaShapeCandidates: contextPack.retrievalDiagnostics.schemaShapeCandidates?.filter((candidate) => objectKeys.has(candidate.objectKey)),
5316
+ certifiedCandidateFits: contextPack.retrievalDiagnostics.certifiedCandidateFits.filter((candidate) => objectKeys.has(candidate.objectKey)),
5317
+ },
5318
+ };
5319
+ }
5320
+ function metadataStringValues(value) {
5321
+ return Array.isArray(value)
5322
+ ? value.filter((item) => typeof item === 'string')
5323
+ : [];
5324
+ }
5325
+ function normalizeExploratoryCandidateIdentity(value) {
5326
+ return value.trim().replace(/["`\[\]]/g, '').replace(/\s*\.\s*/g, '.').toLowerCase();
5327
+ }
4721
5328
  /**
4722
5329
  * Build the prompt-facing evidence pack after enforcing the certified output
4723
5330
  * contract. Incompatible blocks remain in the returned answer's `considered`
@@ -4733,7 +5340,13 @@ function contextPackForRequestedShape(contextPack, question, questionPlan, kg) {
4733
5340
  const blockName = object.name || object.objectKey.replace(/^dql:block:/, '');
4734
5341
  const node = kg.getNode(`block:${blockName}`);
4735
5342
  if (node?.kind === 'block') {
4736
- const fit = evaluateCertifiedBlockFit({ question, plan: questionPlan, block: node });
5343
+ const fit = evaluateCertifiedBlockFit({
5344
+ question,
5345
+ plan: questionPlan,
5346
+ block: node,
5347
+ exactExampleMatch: hasUniqueExactCatalogExample(question, contextPack, node),
5348
+ uniqueExactExampleContract: hasUniqueExactCatalogExample(question, contextPack, node),
5349
+ });
4737
5350
  if (fit.kind === 'context_only' || fit.kind === 'not_applicable') {
4738
5351
  incompatibleBlockKeys.add(object.objectKey);
4739
5352
  }
@@ -5251,15 +5864,26 @@ function renderContextPackForPrompt(contextPack, budget) {
5251
5864
  const warnings = contextPack.warnings.length
5252
5865
  ? `Warnings:\n${contextPack.warnings.slice(0, budget.warningLimit).map((warning) => `- ${warning}`).join('\n')}\n`
5253
5866
  : '';
5254
- const objects = contextPack.objects.slice(0, budget.contextObjectLimit).map((object) => {
5255
- const detail = [
5256
- object.objectType,
5257
- object.domain ? `domain: ${object.domain}` : '',
5258
- object.status ? `status: ${object.status}` : '',
5259
- object.description ? `description: ${object.description}` : '',
5260
- ].filter(Boolean).join('; ');
5261
- return `- ${object.objectKey} (${detail})`;
5262
- }).join('\n');
5867
+ // Pack by value per token instead of truncating at a fixed position. The
5868
+ // objects arrive already ranked, so position IS the value — but cost is not
5869
+ // uniform: one object with a long description can crowd out several cheap
5870
+ // ones that rank just below it, and the old `slice` cut by rank alone. This
5871
+ // keeps the same ordering pressure while letting an inexpensive object that
5872
+ // ranked 19th still fit. The budget is derived from the existing item limit
5873
+ // so the window stays the size the caller already tuned for.
5874
+ const objects = packContext(contextPack.objects.map((object, index) => ({
5875
+ id: object.objectKey,
5876
+ score: contextPack.objects.length - index,
5877
+ render: () => {
5878
+ const detail = [
5879
+ object.objectType,
5880
+ object.domain ? `domain: ${object.domain}` : '',
5881
+ object.status ? `status: ${object.status}` : '',
5882
+ object.description ? `description: ${object.description}` : '',
5883
+ ].filter(Boolean).join('; ');
5884
+ return `- ${object.objectKey} (${detail})`;
5885
+ },
5886
+ })), Math.max(1, budget.contextObjectLimit) * 45).text;
5263
5887
  const conflicts = contextPack.retrievalDiagnostics.candidateConflicts.length
5264
5888
  ? `\nCandidate conflicts:\n${contextPack.retrievalDiagnostics.candidateConflicts.slice(0, 4).map((conflict) => `- ${conflict.reason} ${conflict.prompt}`).join('\n')}`
5265
5889
  : '';
@@ -5275,7 +5899,6 @@ function renderContextPackForPrompt(contextPack, budget) {
5275
5899
  : '';
5276
5900
  const sourceSql = renderSourceBlockSqlContext(contextPack, budget);
5277
5901
  return [
5278
- `context_pack_id: ${contextPack.id}`,
5279
5902
  `trust_label: ${contextPack.trustLabel}`,
5280
5903
  contextPack.trustLabelInfo ? `trust_label_canonical: ${contextPack.trustLabelInfo.display}` : '',
5281
5904
  renderDomainBriefingForPrompt(contextPack).trim(),
@@ -5569,6 +6192,72 @@ function certifiedHitFromContextPack(contextPack, kg) {
5569
6192
  const node = nodeId ? kg.getNode(nodeId) : null;
5570
6193
  return node ? { node, score: 1, snippet: object.snippet } : null;
5571
6194
  }
6195
+ /**
6196
+ * Preserve the catalog's unique exact-example proof when the answer boundary
6197
+ * rechecks the frozen block. The context pack is the same retrieval snapshot
6198
+ * that produced `exactObjectKey`; broad KG search is intentionally not used to
6199
+ * manufacture example uniqueness.
6200
+ */
6201
+ function hasUniqueExactCatalogExample(question, contextPack, node) {
6202
+ const normalizedQuestion = normalizeQuestion(question);
6203
+ if (!normalizedQuestion || !contextPack)
6204
+ return false;
6205
+ const matches = contextPack.objects.filter((object) => {
6206
+ if (object.objectType !== 'dql_block')
6207
+ return false;
6208
+ if (object.status !== 'certified' && object.status !== 'approved' && object.payload?.certification !== 'certified')
6209
+ return false;
6210
+ const examples = Array.isArray(object.payload?.examples) ? object.payload.examples : [];
6211
+ return examples.some((example) => example
6212
+ && typeof example === 'object'
6213
+ && normalizeQuestion(String(example.question ?? '')) === normalizedQuestion);
6214
+ });
6215
+ return matches.length === 1 && normalizeQuestion(matches[0].name) === normalizeQuestion(node.name);
6216
+ }
6217
+ /**
6218
+ * A metadata-only definition answer is safe only for one certified artifact
6219
+ * named exactly by the user. Do not let FTS ranking decide this: a generic
6220
+ * definition phrase must never select a neighbouring block. Payload aliases
6221
+ * are authored block aliases; descriptions and tags are deliberately excluded.
6222
+ */
6223
+ function uniqueNamedCertifiedDefinitionArtifact(question, kg, authorizedDomains) {
6224
+ const subject = namedArtifactDefinitionSubject(question);
6225
+ if (!subject)
6226
+ return null;
6227
+ const matches = kg.getNodesByKind('block', 1000).filter((node) => {
6228
+ if (node.status !== 'certified')
6229
+ return false;
6230
+ if (authorizedDomains.length > 0 && node.domain && !authorizedDomains.includes(node.domain))
6231
+ return false;
6232
+ return certifiedArtifactNames(node).some((name) => normalizeQuestion(name) === subject);
6233
+ });
6234
+ return matches.length === 1 ? { node: matches[0], score: 1, snippet: undefined } : null;
6235
+ }
6236
+ function isUniqueNamedCertifiedDefinitionQuestion(question, node, kg, authorizedDomains) {
6237
+ const selected = uniqueNamedCertifiedDefinitionArtifact(question, kg, authorizedDomains);
6238
+ return selected?.node.nodeId === node.nodeId;
6239
+ }
6240
+ function namedArtifactDefinitionSubject(question) {
6241
+ const match = /^\s*what\s+does\s+(?:the\s+)?(.+?)\s+(?:measure|mean|define|represent)\s*[?.!]*\s*$/i.exec(question);
6242
+ if (!match?.[1])
6243
+ return undefined;
6244
+ // Remove an optional explicit artifact noun while preserving the exact
6245
+ // authored identifier. `top_customers block` and `top_customers` therefore
6246
+ // resolve identically, while arbitrary definition questions have no subject
6247
+ // match and remain on the normal route.
6248
+ const subject = match[1]
6249
+ .replace(/\b(?:certified\s+)?(?:block|artifact|model|metric)\b/gi, ' ')
6250
+ .replace(/\s+/g, ' ')
6251
+ .trim();
6252
+ const normalized = normalizeQuestion(subject);
6253
+ return normalized || undefined;
6254
+ }
6255
+ function certifiedArtifactNames(node) {
6256
+ const aliases = Array.isArray(node.payload?.aliases)
6257
+ ? node.payload.aliases.filter((alias) => typeof alias === 'string')
6258
+ : [];
6259
+ return [node.name, ...aliases];
6260
+ }
5572
6261
  /**
5573
6262
  * Reduce a possibly-composed prompt to the user's actual question for display.
5574
6263
  * Callers (e.g. the Build-SQL-draft modal) sometimes prepend a multi-line instruction
@@ -5785,6 +6474,28 @@ function flowFieldCovers(field, term) {
5785
6474
  function normalizeFlowField(value) {
5786
6475
  return value.toLowerCase().replace(/[_-]+/g, ' ').replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
5787
6476
  }
6477
+ /**
6478
+ * Remove the model's QUERY PLAN scaffolding from user-facing prose.
6479
+ *
6480
+ * Rule 4 of the SQL prompt tells the model to state its grain, measures, join
6481
+ * path, and join keys BEFORE writing SQL. That instruction is load-bearing —
6482
+ * it is what prevents wrong-grain answers and fan-out joins — but it is
6483
+ * reasoning, not an answer, and it was reaching the reader verbatim:
6484
+ *
6485
+ * "QUERY PLAN: grain = one row per customer, filtered to the named customer
6486
+ * (Matthew Meyer, honorific stripped) ... FROM dev.customers c, filtered on
6487
+ * c.customer_name ILIKE '%Matthew Meyer%'."
6488
+ *
6489
+ * Someone who asked why a customer tops a list should not be handed a join
6490
+ * plan. The plan stays in the artifact and the trace; it leaves the prose.
6491
+ */
6492
+ export function stripQueryPlanScaffolding(text) {
6493
+ return text
6494
+ // A QUERY PLAN section runs to the next blank line or the end of the prose.
6495
+ .replace(/(?:^|\n)\s*(?:\*\*)?QUERY PLAN(?:\*\*)?\s*[::][\s\S]*?(?=\n\s*\n|$)/gi, '')
6496
+ .replace(/\n{3,}/g, '\n\n')
6497
+ .trim();
6498
+ }
5788
6499
  /**
5789
6500
  * Public for tests. Prefer the structured W2.7 JSON proposal contract, then
5790
6501
  * fall back to the legacy prose + ```sql block + Viz line format.
@@ -5798,11 +6509,11 @@ export function parseProposal(raw) {
5798
6509
  const vizMatch = raw.match(/^Viz:\s*([a-z_]+)/im);
5799
6510
  const viz = vizMatch ? vizMatch[1].trim().toLowerCase() : undefined;
5800
6511
  // Strip the SQL block + Viz line from the prose to keep the summary clean.
5801
- const text = raw
6512
+ const text = stripQueryPlanScaffolding(raw
5802
6513
  .replace(/```json[\s\S]*?```/gi, '')
5803
6514
  .replace(/```sql[\s\S]*?```/gi, '')
5804
6515
  .replace(/^Viz:.*$/gim, '')
5805
- .trim();
6516
+ .trim());
5806
6517
  return { text, sql, viz };
5807
6518
  }
5808
6519
  function parseStructuredProposal(raw) {
@@ -5830,7 +6541,7 @@ function parsedProposalFromJson(value) {
5830
6541
  return undefined;
5831
6542
  const record = value;
5832
6543
  const sql = firstJsonString(record.sql, record.query)?.trim();
5833
- const text = firstJsonString(record.summary, record.text, record.answer, record.description)?.trim() ?? '';
6544
+ const text = stripQueryPlanScaffolding(firstJsonString(record.summary, record.text, record.answer, record.description)?.trim() ?? '');
5834
6545
  const viz = firstJsonString(record.viz, record.visualization, record.chartType)
5835
6546
  ?.trim()
5836
6547
  .toLowerCase()
@@ -6634,7 +7345,12 @@ function isBusinessDefinitionQuestion(question) {
6634
7345
  }
6635
7346
  /** Definition that may terminate with documentation instead of executing data. */
6636
7347
  function isPureBusinessDefinitionQuestion(question) {
6637
- return questionTypeFromText(question) === 'definition';
7348
+ return questionTypeFromText(question) === 'definition'
7349
+ // `questionTypeFromText` intentionally recognizes ranking before generic
7350
+ // definitions, so a named artifact such as `top_customers` would otherwise
7351
+ // look like a ranking request even in the explicit "what does … measure?"
7352
+ // form. Object-name matching remains mandatory at the terminal site.
7353
+ || /^\s*what\s+does\s+(?:the\s+)?[^?.!]{1,160}\s+(?:measure|mean|define|represent)\s*[?.!]*\s*$/i.test(question);
6638
7354
  }
6639
7355
  function isBreakdownOrDrilldownQuestion(question) {
6640
7356
  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);
@@ -6697,12 +7413,21 @@ function hasCertifiedNodeFit(question, plan, node, options = {}) {
6697
7413
  plan,
6698
7414
  block: node,
6699
7415
  exactExampleMatch: exactExampleMatch || exactObjectRequest,
7416
+ uniqueExactExampleContract: options.uniqueExactExampleContract === true,
6700
7417
  definitionLookup,
6701
7418
  });
6702
7419
  // Delegate the termination decision to THE single authority instead of a
6703
7420
  // loop-local re-derivation (the seams between such copies produced certified
6704
7421
  // answers that ignored member-scoped follow-ups).
6705
- return certifiedTerminationVerdict({ fit, allowInferredContract: options.allowInferredContract }).allow;
7422
+ return certifiedTerminationVerdict({
7423
+ fit,
7424
+ // A named `what does <artifact> measure?` request is an artifact metadata
7425
+ // lookup, not a query-result claim. The termination authority still
7426
+ // rejects missing declared measures and unsupported filters before this
7427
+ // narrow bypass is considered.
7428
+ ...(definitionLookup ? { bypass: 'definition_lookup' } : {}),
7429
+ allowInferredContract: options.allowInferredContract,
7430
+ }).allow;
6706
7431
  }
6707
7432
  function objectNameInQuestion(question, node) {
6708
7433
  const questionText = normalizeQuestion(question);