@duckcodeailabs/dql-agent 1.13.4 → 1.14.0

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 (137) hide show
  1. package/dist/agent-run-engine.d.ts +87 -7
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +559 -40
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-store.d.ts.map +1 -1
  6. package/dist/agent-run-store.js +42 -25
  7. package/dist/agent-run-store.js.map +1 -1
  8. package/dist/agentic/tool-loop.d.ts.map +1 -1
  9. package/dist/agentic/tool-loop.js +100 -32
  10. package/dist/agentic/tool-loop.js.map +1 -1
  11. package/dist/aggregation-safety-proof.d.ts +41 -0
  12. package/dist/aggregation-safety-proof.d.ts.map +1 -0
  13. package/dist/aggregation-safety-proof.js +511 -0
  14. package/dist/aggregation-safety-proof.js.map +1 -0
  15. package/dist/analytical-compatibility.d.ts.map +1 -1
  16. package/dist/analytical-compatibility.js +42 -25
  17. package/dist/analytical-compatibility.js.map +1 -1
  18. package/dist/analytical-execution-graph.d.ts +4 -21
  19. package/dist/analytical-execution-graph.d.ts.map +1 -1
  20. package/dist/analytical-execution-graph.js +27 -0
  21. package/dist/analytical-execution-graph.js.map +1 -1
  22. package/dist/analytical-frame.d.ts +12 -1
  23. package/dist/analytical-frame.d.ts.map +1 -1
  24. package/dist/analytical-frame.js +102 -2
  25. package/dist/analytical-frame.js.map +1 -1
  26. package/dist/analytical-orchestration.d.ts +256 -0
  27. package/dist/analytical-orchestration.d.ts.map +1 -0
  28. package/dist/analytical-orchestration.js +419 -0
  29. package/dist/analytical-orchestration.js.map +1 -0
  30. package/dist/analytical-result-facts.d.ts +54 -1
  31. package/dist/analytical-result-facts.d.ts.map +1 -1
  32. package/dist/analytical-result-facts.js +183 -0
  33. package/dist/analytical-result-facts.js.map +1 -1
  34. package/dist/answer-loop.d.ts +44 -3
  35. package/dist/answer-loop.d.ts.map +1 -1
  36. package/dist/answer-loop.js +771 -38
  37. package/dist/answer-loop.js.map +1 -1
  38. package/dist/cascade/budgets.js +1 -1
  39. package/dist/cascade/route-policy.d.ts +1 -1
  40. package/dist/cascade/route-policy.d.ts.map +1 -1
  41. package/dist/cascade/route-policy.js +2 -0
  42. package/dist/cascade/route-policy.js.map +1 -1
  43. package/dist/conversation/snapshot.d.ts.map +1 -1
  44. package/dist/conversation/snapshot.js +3 -0
  45. package/dist/conversation/snapshot.js.map +1 -1
  46. package/dist/conversation/turn-trust.d.ts.map +1 -1
  47. package/dist/conversation/turn-trust.js +6 -0
  48. package/dist/conversation/turn-trust.js.map +1 -1
  49. package/dist/generated-analytical-proposal.d.ts +37 -0
  50. package/dist/generated-analytical-proposal.d.ts.map +1 -0
  51. package/dist/generated-analytical-proposal.js +219 -0
  52. package/dist/generated-analytical-proposal.js.map +1 -0
  53. package/dist/index.d.ts +11 -5
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +7 -3
  56. package/dist/index.js.map +1 -1
  57. package/dist/intent-controller.d.ts +8 -1
  58. package/dist/intent-controller.d.ts.map +1 -1
  59. package/dist/intent-controller.js.map +1 -1
  60. package/dist/kg/build.js +102 -6
  61. package/dist/kg/build.js.map +1 -1
  62. package/dist/meaning-resolution.d.ts +30 -0
  63. package/dist/meaning-resolution.d.ts.map +1 -1
  64. package/dist/meaning-resolution.js +61 -8
  65. package/dist/meaning-resolution.js.map +1 -1
  66. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  67. package/dist/metadata/analysis-planner.js +46 -2
  68. package/dist/metadata/analysis-planner.js.map +1 -1
  69. package/dist/metadata/block-fit.d.ts.map +1 -1
  70. package/dist/metadata/block-fit.js +55 -2
  71. package/dist/metadata/block-fit.js.map +1 -1
  72. package/dist/metadata/catalog.d.ts +24 -0
  73. package/dist/metadata/catalog.d.ts.map +1 -1
  74. package/dist/metadata/catalog.js +227 -42
  75. package/dist/metadata/catalog.js.map +1 -1
  76. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  77. package/dist/metadata/meaning-evidence.js +408 -36
  78. package/dist/metadata/meaning-evidence.js.map +1 -1
  79. package/dist/metadata/sql-context-validation.d.ts +3 -0
  80. package/dist/metadata/sql-context-validation.d.ts.map +1 -1
  81. package/dist/metadata/sql-context-validation.js +16 -5
  82. package/dist/metadata/sql-context-validation.js.map +1 -1
  83. package/dist/plan-execution-adapter.d.ts +9 -1
  84. package/dist/plan-execution-adapter.d.ts.map +1 -1
  85. package/dist/plan-execution-adapter.js +81 -32
  86. package/dist/plan-execution-adapter.js.map +1 -1
  87. package/dist/provider-egress.d.ts +122 -0
  88. package/dist/provider-egress.d.ts.map +1 -0
  89. package/dist/provider-egress.js +504 -0
  90. package/dist/provider-egress.js.map +1 -0
  91. package/dist/providers/claude.d.ts.map +1 -1
  92. package/dist/providers/claude.js +76 -33
  93. package/dist/providers/claude.js.map +1 -1
  94. package/dist/providers/dispatch.d.ts +15 -0
  95. package/dist/providers/dispatch.d.ts.map +1 -0
  96. package/dist/providers/dispatch.js +25 -0
  97. package/dist/providers/dispatch.js.map +1 -0
  98. package/dist/providers/gemini.d.ts.map +1 -1
  99. package/dist/providers/gemini.js +16 -8
  100. package/dist/providers/gemini.js.map +1 -1
  101. package/dist/providers/index.d.ts +2 -1
  102. package/dist/providers/index.d.ts.map +1 -1
  103. package/dist/providers/index.js +1 -0
  104. package/dist/providers/index.js.map +1 -1
  105. package/dist/providers/ollama.d.ts.map +1 -1
  106. package/dist/providers/ollama.js +29 -10
  107. package/dist/providers/ollama.js.map +1 -1
  108. package/dist/providers/openai.d.ts.map +1 -1
  109. package/dist/providers/openai.js +92 -36
  110. package/dist/providers/openai.js.map +1 -1
  111. package/dist/providers/types.d.ts +43 -0
  112. package/dist/providers/types.d.ts.map +1 -1
  113. package/dist/providers/types.js.map +1 -1
  114. package/dist/relationship-proof.d.ts +19 -0
  115. package/dist/relationship-proof.d.ts.map +1 -0
  116. package/dist/relationship-proof.js +75 -0
  117. package/dist/relationship-proof.js.map +1 -0
  118. package/dist/research-loop.d.ts.map +1 -1
  119. package/dist/research-loop.js +17 -11
  120. package/dist/research-loop.js.map +1 -1
  121. package/dist/resolved-analytical-plan.d.ts +25 -3
  122. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  123. package/dist/resolved-analytical-plan.js +460 -31
  124. package/dist/resolved-analytical-plan.js.map +1 -1
  125. package/dist/router.d.ts +38 -1
  126. package/dist/router.d.ts.map +1 -1
  127. package/dist/router.js +1060 -53
  128. package/dist/router.js.map +1 -1
  129. package/dist/semantic-identities.d.ts +18 -0
  130. package/dist/semantic-identities.d.ts.map +1 -0
  131. package/dist/semantic-identities.js +60 -0
  132. package/dist/semantic-identities.js.map +1 -0
  133. package/dist/semantic-proof-authority.d.ts +29 -0
  134. package/dist/semantic-proof-authority.d.ts.map +1 -0
  135. package/dist/semantic-proof-authority.js +34 -0
  136. package/dist/semantic-proof-authority.js.map +1 -0
  137. package/package.json +4 -4
@@ -15,7 +15,7 @@
15
15
  * full pipeline.
16
16
  */
17
17
  import { createHash } from 'node:crypto';
18
- import { describeDialectForPrompt, normalizeMetricCapabilityContract, } from '@duckcodeailabs/dql-core';
18
+ import { describeDialectForPrompt, buildSqlOutputExpressionSignature, normalizeMetricCapabilityContract, semanticExecutionFingerprint, } from '@duckcodeailabs/dql-core';
19
19
  import { buildSkillBlockHints, buildSkillMetricHints, buildSkillsPrompt, expandQuestionWithSkillVocabulary, selectRelevantSkills } from './skills/loader.js';
20
20
  import { conversationTurnContextState, } from './conversation/snapshot.js';
21
21
  import { isTrustedConversationTurn } from './conversation/turn-trust.js';
@@ -32,6 +32,9 @@ import { createContextLedger } from './grounding/context-ledger.js';
32
32
  import { validateAnswerResultShape } from './answer-shape.js';
33
33
  import { analyticalErrorDetail, } from './analytical-error.js';
34
34
  import { fanoutWarningsForSql } from './metadata/grain-ledger.js';
35
+ import { buildAggregationSafetyProof, buildSemanticCompilationAggregationSafetyProof, } from './aggregation-safety-proof.js';
36
+ import { buildSemanticProofAuthorityV1 } from './semantic-proof-authority.js';
37
+ import { GENERATED_ANALYTICAL_TUPLE_DRIFT_MESSAGE, validateGeneratedAnalyticalProposal, } from './generated-analytical-proposal.js';
35
38
  import { evaluateDbtFirstGeneratedSql } from './metadata/dbt-first-safety.js';
36
39
  import { planAnalyticalPath, humanizeAnalyticalEntityId, analyticalPolicyUserFacingReason, } from './metadata/analytical-policy.js';
37
40
  import { planCertifiedAdaptation } from './metadata/block-adapt.js';
@@ -47,14 +50,14 @@ import { questionTypeFromText } from './meaning-resolution.js';
47
50
  import { cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, } from './cascade/cascade.js';
48
51
  import { shouldClarifyBeforeGeneration } from './cascade/triage.js';
49
52
  import { stampTrustLabel } from './trust/stamp.js';
50
- import { deriveResolvedAnalyticalPlan } from './resolved-analytical-plan.js';
53
+ import { buildResolvedAnalyticalPlan, deriveResolvedAnalyticalPlan } from './resolved-analytical-plan.js';
51
54
  import { adaptAnalyticalFreshnessRequest, adaptAnalyticalSemanticGraph, adaptResolvedAnalyticalPlan, buildPlanExecutionRegistry, } from './plan-execution-adapter.js';
52
55
  import { buildGovernedRelationalRegistry, compileGovernedRelationalExecutionGraph, compileGovernedRelationalPlan, finalizeGovernedCompilationReceipt, renderGovernedRelationalDqlArtifact, } from './governed-relational-compiler.js';
53
56
  import { buildAnalyticalExecutionGraph, executeAnalyticalExecutionGraph, } from './analytical-execution-graph.js';
54
57
  import { buildAnalyticalResultFacts, renderDeterministicAnalyticalNarrative, validateAnalyticalNarrativeClaims, } from './analytical-result-facts.js';
55
58
  import { resolveAnalyticalPeriods, } from './analytical-period-resolution.js';
56
59
  import { classifyAnalyticalFailure, createAnalyticalFailure, } from './analytical-failure-repair.js';
57
- import { QUICK_PROMPT_CONTEXT_BUDGET, canUseLaneRepair, cascadeBudgetTrace, createCascadeBudgetState, deepAlternativeCountForQuestion, promptContextBudgetForQuestion, proposalToolBudgetForQuestion, recordLaneRepair, } from './cascade/budgets.js';
60
+ import { QUICK_PROMPT_CONTEXT_BUDGET, canUseLaneRepair, cascadeBudgetTrace, createCascadeBudgetState, promptContextBudgetForQuestion, proposalToolBudgetForQuestion, recordLaneRepair, } from './cascade/budgets.js';
58
61
  function semanticCompilerFailure(error) {
59
62
  const record = error && typeof error === 'object' ? error : undefined;
60
63
  const message = error instanceof Error ? error.message : String(error);
@@ -675,6 +678,141 @@ export function materializeKnowledgeLensSkills(contextPack, available) {
675
678
  };
676
679
  });
677
680
  }
681
+ /**
682
+ * Older direct AnswerLoop callers supplied the semantic catalog but no frozen
683
+ * route contract. Migrate that one bounded retrieval result exactly once at
684
+ * ingress: the chosen KG identity and its normalized capability are copied into
685
+ * an authoritative RAP before compiler/proof code runs. No downstream stage is
686
+ * permitted to repeat this name/family selection.
687
+ */
688
+ async function freezeLegacySemanticSelection(input) {
689
+ const metricNodes = input.kg.getNodesByKind('metric', 100_000);
690
+ if (metricNodes.length === 0)
691
+ return undefined;
692
+ const questionPlan = input.contextPack?.questionPlan?.requestedShape
693
+ ? input.contextPack.questionPlan
694
+ : buildAnalysisQuestionPlan(input.question, input.followUp);
695
+ const semanticLayer = input.semanticLayer;
696
+ const match = await matchSemanticMetric(input.question, metricNodes, {
697
+ measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
698
+ ...(semanticLayer ? { canExecute: (name) => semanticLayer.canComposeMetric(name) } : {}),
699
+ }).catch(() => null);
700
+ if (!match)
701
+ return undefined;
702
+ const normalizedCapability = normalizeMetricCapabilityContract(match.metric.payload?.analyticalCapability);
703
+ if (!normalizedCapability)
704
+ return undefined;
705
+ const metricDefinition = semanticLayer?.listMetrics().find((metric) => metric.name === match.metric.name
706
+ || metric.name.endsWith(`.${match.metric.name}`)
707
+ || match.metric.name.endsWith(`.${metric.name}`));
708
+ const composed = semanticLayer ? composeSemanticQueryForQuestion({
709
+ semanticLayer,
710
+ question: input.question,
711
+ questionPlan,
712
+ matchedMetric: match.metric,
713
+ filterValueColumns: (value) => resolveFilterValueColumns(value, input.schemaContext ?? []),
714
+ filterValueBindings: (value) => resolveAgentFilterValueBindings(value, input.schemaContext ?? []),
715
+ ...(input.semanticDriver ? { driver: input.semanticDriver } : {}),
716
+ ...(input.semanticTableMapping ? { tableMapping: input.semanticTableMapping } : {}),
717
+ }) : undefined;
718
+ // A semantic runtime may still resolve the shape later (Research/member-tool
719
+ // lane). Do not freeze a partial tuple here.
720
+ if (semanticLayer && !composed)
721
+ return undefined;
722
+ const capability = {
723
+ ...normalizedCapability,
724
+ dimensions: normalizedCapability.dimensions.map((dimension) => {
725
+ const definition = semanticLayer?.resolveGroupBy(dimension.dimensionId);
726
+ const compilerSelected = composed?.dimensions.includes(dimension.dimensionId)
727
+ || composed?.timeDimension?.name === dimension.dimensionId;
728
+ return definition
729
+ ? {
730
+ ...dimension,
731
+ nativeGroupingReference: definition.qualifiedName ?? definition.name,
732
+ ...(compilerSelected && questionPlan.requestedShape.rankingDirection
733
+ ? { supportedRoles: [...new Set([...dimension.supportedRoles, 'rank_entity'])] }
734
+ : {}),
735
+ }
736
+ : dimension;
737
+ }),
738
+ };
739
+ const identity = match.metric.nodeId;
740
+ const selectedMetricNames = composed?.metrics ?? [match.metric.name];
741
+ const selectedNodes = selectedMetricNames.flatMap((name) => {
742
+ const matches = metricNodes.filter((node) => node.name === name || node.name.endsWith(`.${name}`) || name.endsWith(`.${node.name}`));
743
+ return matches.length === 1 ? matches : [];
744
+ });
745
+ if (selectedNodes.length !== selectedMetricNames.length)
746
+ return undefined;
747
+ const candidates = selectedNodes.flatMap((node) => {
748
+ const nodeCapability = normalizeMetricCapabilityContract(node.payload?.analyticalCapability);
749
+ if (!nodeCapability)
750
+ return [];
751
+ const isPrimary = node.nodeId === match.metric.nodeId;
752
+ return [{
753
+ id: node.nodeId,
754
+ kind: 'semantic_metric',
755
+ semanticObjectType: 'metric',
756
+ trustTier: 'semantic',
757
+ name: node.name,
758
+ aliases: node.tags,
759
+ definition: node.description,
760
+ domain: node.domain,
761
+ primaryEntity: nodeCapability.primaryEntityId,
762
+ dimensions: nodeCapability.dimensions.map((dimension) => dimension.dimensionId),
763
+ sourceObjects: nodeCapability.semanticModelId ? [nodeCapability.semanticModelId] : [],
764
+ relevanceScore: isPrimary ? match.score : 1,
765
+ matchReasons: [`bounded_${isPrimary ? match.basis ?? 'semantic' : 'compiler'}_retrieval`],
766
+ compatibility: 'compatible',
767
+ compatibilityFacts: ['The selected KG identity carries the exact normalized semantic capability embedded in this plan.'],
768
+ analyticalCapability: isPrimary ? capability : nodeCapability,
769
+ eligible: true,
770
+ exactMatch: true,
771
+ }];
772
+ });
773
+ if (candidates.length !== selectedNodes.length)
774
+ return undefined;
775
+ return buildResolvedAnalyticalPlan({
776
+ question: input.question,
777
+ resolution: {
778
+ interpretedQuestion: input.question,
779
+ questionType: questionTypeFromText(input.question),
780
+ selectedConceptIds: candidates.map((item) => item.id),
781
+ recommendedExecutionId: identity,
782
+ queryIntent: {
783
+ measures: composed?.metrics ?? [match.metric.name],
784
+ dimensions: composed ? [
785
+ ...composed.dimensions,
786
+ ...(composed.timeDimension ? [composed.timeDimension.name] : []),
787
+ ] : [
788
+ ...questionPlan.requestedShape.dimensions,
789
+ ...(questionPlan.timeTerms.length > 0 && (metricDefinition?.aggTimeDimension ?? capability.timeDimensions[0]?.dimensionId)
790
+ ? [metricDefinition?.aggTimeDimension ?? capability.timeDimensions[0].dimensionId]
791
+ : []),
792
+ ],
793
+ filters: composed?.filters.map((filter) => ({ field: filter.dimension, value: String(filter.values[0] ?? '') })) ?? [],
794
+ ...(composed?.timeDimension?.granularity
795
+ ? { timeGrain: composed.timeDimension.granularity }
796
+ : questionPlan.timeTerms.some((term) => /month/i.test(term)) ? { timeGrain: 'month' } : {}),
797
+ ...(questionPlan.requestedShape.rankingDirection
798
+ ? { order: questionPlan.requestedShape.rankingDirection === 'top' ? 'desc' : 'asc' }
799
+ : {}),
800
+ ...(questionPlan.requestedShape.topN ? { limit: questionPlan.requestedShape.topN.n } : {}),
801
+ },
802
+ rejectedCandidates: [],
803
+ confidence: match.score >= 0.7 ? 'high' : 'medium',
804
+ missingInformation: [],
805
+ recommendedRoute: 'semantic',
806
+ },
807
+ evidence: {
808
+ snapshotId: input.contextPack?.knowledgeLens?.snapshotId ?? input.contextPack?.id ?? 'legacy-answer-loop-snapshot',
809
+ sourceFingerprint: capability.sourceFingerprint,
810
+ candidates,
811
+ },
812
+ candidates,
813
+ mode: 'authoritative',
814
+ });
815
+ }
678
816
  export async function answer(input) {
679
817
  const inheritedPlan = !input.resolvedAnalyticalPlan
680
818
  && input.followUp?.priorResolvedAnalyticalPlan
@@ -694,6 +832,11 @@ export async function answer(input) {
694
832
  resolvedAnalyticalPlan: normalizedInput.resolvedAnalyticalPlan,
695
833
  };
696
834
  }
835
+ if (!normalizedInput.resolvedAnalyticalPlan) {
836
+ const migratedPlan = await freezeLegacySemanticSelection(normalizedInput);
837
+ if (migratedPlan)
838
+ normalizedInput = { ...normalizedInput, resolvedAnalyticalPlan: migratedPlan };
839
+ }
697
840
  const executionRegistry = buildPlanExecutionRegistry({
698
841
  nodes: [
699
842
  ...normalizedInput.kg.getNodesByKind('block', 100_000),
@@ -731,7 +874,7 @@ export async function answer(input) {
731
874
  request: freshnessRequest,
732
875
  registry: executionRegistry,
733
876
  semanticLayer: normalizedInput.semanticLayer,
734
- expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens.snapshotId,
877
+ expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens?.snapshotId,
735
878
  })
736
879
  : undefined;
737
880
  if (adapterBinding?.status === 'blocked') {
@@ -788,7 +931,7 @@ export async function answer(input) {
788
931
  plan: normalizedInput.resolvedAnalyticalPlan,
789
932
  registry: executionRegistry,
790
933
  semanticLayer: normalizedInput.semanticLayer,
791
- expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens.snapshotId,
934
+ expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens?.snapshotId,
792
935
  })
793
936
  : undefined;
794
937
  const analyticalCapability = normalizedInput.resolvedAnalyticalPlan
@@ -801,9 +944,11 @@ export async function answer(input) {
801
944
  ? analyticalCapability.executionCapabilities.find((candidate) => candidate.route === analyticalRoute)?.adapterId
802
945
  : undefined;
803
946
  const multiMetricPlan = (normalizedInput.resolvedAnalyticalPlan?.analyticalFrame?.metricConceptIds.length ?? 0) > 1;
947
+ const generatedProposalPlan = normalizedInput.resolvedAnalyticalPlan?.capability === 'bounded_exploration';
804
948
  const analyticalGraphBuild = normalizedInput.resolvedAnalyticalPlan?.schemaVersion === 2
805
949
  && normalizedInput.resolvedAnalyticalPlan.analyticalFrame
806
950
  && !multiMetricPlan
951
+ && !generatedProposalPlan
807
952
  && !normalizedInput.analyticalPeriodResolutionFailure
808
953
  && analyticalCapability
809
954
  && analyticalRoute
@@ -819,6 +964,7 @@ export async function answer(input) {
819
964
  : undefined;
820
965
  const analyticalExecutionGraphFailure = normalizedInput.resolvedAnalyticalPlan?.schemaVersion === 2
821
966
  && !multiMetricPlan
967
+ && !generatedProposalPlan
822
968
  ? analyticalGraphBuild?.status === 'blocked'
823
969
  ? {
824
970
  code: analyticalGraphBuild.code,
@@ -839,7 +985,7 @@ export async function answer(input) {
839
985
  plan: normalizedInput.resolvedAnalyticalPlan,
840
986
  registry: executionRegistry,
841
987
  semanticLayer: normalizedInput.semanticLayer,
842
- expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens.snapshotId,
988
+ expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens?.snapshotId,
843
989
  })
844
990
  : undefined;
845
991
  const planBoundInput = resolvedPlanExecutionBinding || analyticalExecutionGraph || analyticalExecutionGraphFailure
@@ -1017,17 +1163,27 @@ function deriveAiRoute(result, metricMatch) {
1017
1163
  };
1018
1164
  }
1019
1165
  // Uncertified: a governed metric matched → semantic_metric; else generated preview.
1020
- if (result.sourceTier === 'semantic_layer' && metricMatch) {
1166
+ if (result.sourceTier === 'semantic_layer' && (metricMatch || result.analyticalExecutionGraph?.route === 'semantic')) {
1167
+ const metricName = metricMatch?.metric.name
1168
+ ?? result.dqlArtifact?.metrics?.join(', ')
1169
+ ?? result.analyticalExecutionGraph?.metricId;
1021
1170
  return {
1022
1171
  tier: 'semantic_metric',
1023
- label: `Answered from metric ${metricMatch.metric.name}`,
1024
- ref: metricMatch.metric.name,
1172
+ label: metricName ? `Answered from metric ${metricName}` : 'Answered from the snapshot-bound semantic plan',
1173
+ ...(metricName ? { ref: metricName } : {}),
1025
1174
  };
1026
1175
  }
1027
1176
  return result.dqlArtifact
1028
1177
  ? { tier: 'generated_sql', label: 'Prepared review-required DQL artifact with SQL preview.' }
1029
1178
  : { tier: 'generated_sql', label: 'Prepared review-required SQL preview.' };
1030
1179
  }
1180
+ /** "customers_customer_name" → "customers" for a question the user has to read. */
1181
+ function humanizeDeicticDimension(dimension) {
1182
+ const leaf = dimension.split(/[.:]/).pop() ?? dimension;
1183
+ const words = leaf.replace(/[_-]+/g, ' ').trim().toLowerCase();
1184
+ const last = words.split(' ').filter(Boolean).pop() ?? words;
1185
+ return last.endsWith('s') ? last : `${last}s`;
1186
+ }
1031
1187
  async function runAnswerLoop(input) {
1032
1188
  const { question, userId, domain, provider, kg, skills = [], blockHints = [] } = input;
1033
1189
  // AGT-004: with no explicit domain selection, let direct question evidence
@@ -1198,6 +1354,7 @@ async function runAnswerLoop(input) {
1198
1354
  input,
1199
1355
  binding: input.semanticGraphExecutionBinding,
1200
1356
  graph: input.analyticalExecutionGraph,
1357
+ capability: normalizeMetricCapabilityContract(input.semanticGraphExecutionBinding.metricNode.payload?.analyticalCapability),
1201
1358
  considered,
1202
1359
  providerName: provider.name,
1203
1360
  });
@@ -1313,7 +1470,22 @@ async function runAnswerLoop(input) {
1313
1470
  };
1314
1471
  }
1315
1472
  if (authoritativePlanBinding?.status === 'blocked') {
1316
- const text = `The resolved analytical plan cannot execute safely: ${authoritativePlanBinding.reason}`;
1473
+ // An ambiguous singular reference is answerable the moment the user says
1474
+ // which one they meant, so offer the candidates rather than dead-ending.
1475
+ // The old refusal shipped the binder's internal sentence as the answer
1476
+ // text, which reached the user as a generic "could not be completed" card.
1477
+ const choices = input.followUp?.deicticChoices;
1478
+ const clarificationOptions = choices
1479
+ ? choices.values.slice(0, 8).map((value) => ({
1480
+ id: `member:${choices.dimension}:${value}`,
1481
+ label: value,
1482
+ kind: 'member',
1483
+ question: `${question} (${value})`,
1484
+ }))
1485
+ : undefined;
1486
+ const text = clarificationOptions?.length
1487
+ ? `The previous answer listed ${choices.values.length} ${humanizeDeicticDimension(choices.dimension)}. Which one did you mean?`
1488
+ : `The resolved analytical plan cannot execute safely: ${authoritativePlanBinding.reason}`;
1317
1489
  return {
1318
1490
  kind: 'no_answer',
1319
1491
  sourceTier: 'no_answer',
@@ -1322,8 +1494,12 @@ async function runAnswerLoop(input) {
1322
1494
  confidence: 0,
1323
1495
  text,
1324
1496
  answer: text,
1325
- refusalCode: authoritativePlanBinding.code === 'PLAN_BLOCKED' ? 'ambiguous' : 'grounding_gap',
1497
+ // Offering real choices makes this a clarify turn, not a terminal block.
1498
+ refusalCode: clarificationOptions?.length || authoritativePlanBinding.code === 'PLAN_BLOCKED'
1499
+ ? 'ambiguous'
1500
+ : 'grounding_gap',
1326
1501
  refusalDetails: { code: authoritativePlanBinding.code, message: authoritativePlanBinding.reason },
1502
+ ...(clarificationOptions?.length ? { clarificationOptions } : {}),
1327
1503
  citations: contextPackCitations(input.contextPack, 8),
1328
1504
  considered,
1329
1505
  contextPack: input.contextPack,
@@ -1766,6 +1942,13 @@ async function runAnswerLoop(input) {
1766
1942
  ?? renderSingleEntityModelingPrompt(analyticalPlan, input.manifest);
1767
1943
  if (analyticalPlanPrompt)
1768
1944
  messages.push({ role: 'system', content: analyticalPlanPrompt });
1945
+ const attributeLookup = isMetricIndependentAttributeLookup(question, questionPlan, input.followUp);
1946
+ if (attributeLookup) {
1947
+ messages.push({
1948
+ role: 'system',
1949
+ content: renderAttributeLookupInstruction(questionPlan, input.followUp),
1950
+ });
1951
+ }
1769
1952
  if (questionDomainScope.length > 0) {
1770
1953
  messages.push({
1771
1954
  role: 'system',
@@ -1795,6 +1978,7 @@ async function runAnswerLoop(input) {
1795
1978
  let semanticBridgeAnswer;
1796
1979
  let semanticRuntimeFailure;
1797
1980
  let semanticExecutionTrace;
1981
+ let aggregationCompilerReceipt;
1798
1982
  let semanticAttemptedArtifact;
1799
1983
  let semanticRuntimeCompiledAnswer = false;
1800
1984
  if (authoritativeSemanticBinding && input.semanticLayer) {
@@ -1902,7 +2086,7 @@ async function runAnswerLoop(input) {
1902
2086
  // AGT-005: a governed runtime failure is terminal for this selected metric. Do not
1903
2087
  // spend another model call re-selecting the same member or silently fall
1904
2088
  // into exploratory SQL; surface the compiler's actionable error instead.
1905
- if (!semanticBridgeAnswer && !semanticRuntimeFailure) {
2089
+ if (!semanticBridgeAnswer && !semanticRuntimeFailure && input.allowProviderSemanticMemberSelection === true) {
1906
2090
  const requestedGroupingDims = requestedGroupingDimensions(questionPlan);
1907
2091
  const wantedBreakdown = questionPlan.requestedShape.dimensions.length > 0
1908
2092
  || questionPlan.dimensionTerms.length > 0;
@@ -2219,6 +2403,7 @@ async function runAnswerLoop(input) {
2219
2403
  reasoningEffort: input.reasoningEffort,
2220
2404
  analysisDepth: input.analysisDepth,
2221
2405
  toolCalls: proposalToolCalls,
2406
+ providerPayloadGuard: input.providerPayloadGuard,
2222
2407
  });
2223
2408
  }
2224
2409
  catch (err) {
@@ -2230,7 +2415,28 @@ async function runAnswerLoop(input) {
2230
2415
  throw input.signal.reason ?? err;
2231
2416
  if (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError'))
2232
2417
  throw err;
2233
- const text = `Provider error: ${err.message}`;
2418
+ // DQL's own orchestration budget refusing a dispatch is NOT an upstream
2419
+ // outage. Reporting it as one ("Claude subscription failed: the 25-second
2420
+ // soft target elapsed") blames the user's AI provider for DQL's timer and
2421
+ // sends them to debug the wrong system.
2422
+ const budgetCode = err && typeof err === 'object' && 'code' in err
2423
+ ? String(err.code)
2424
+ : '';
2425
+ // Admission control is NOT an exhausted clock. `RUN_DEADLINE_INSUFFICIENT`
2426
+ // means DQL declined to START a call it predicted would not fit in the
2427
+ // remaining budget — it can fire seconds into a run, and a compound turn's
2428
+ // second task hit it after 6.4s of a 45s budget. Sharing the "stopped at
2429
+ // its own time budget" wording with the genuine soft-target codes told the
2430
+ // reader the run ran out of time when most of it remained, sending them to
2431
+ // debug latency instead of the estimate.
2432
+ const admissionDeclined = budgetCode === 'RUN_DEADLINE_INSUFFICIENT';
2433
+ const orchestrationBudget = budgetCode === 'RUN_SOFT_TARGET_EXCEEDED'
2434
+ || budgetCode === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED';
2435
+ const text = admissionDeclined
2436
+ ? `DQL did not start another AI call because it estimated the call would not finish inside this run's remaining budget, so the run stopped early rather than mid-flight. The AI provider did not fail and the time limit was not necessarily reached. ${err.message} Retry, or use Research for a longer budget.`
2437
+ : orchestrationBudget
2438
+ ? 'DQL stopped this run at its own time budget before the query could be settled. The AI provider did not fail. Retry, or use Research for a longer budget.'
2439
+ : `Provider error: ${err.message}`;
2234
2440
  return {
2235
2441
  kind: 'no_answer',
2236
2442
  sourceTier: 'no_answer',
@@ -2240,7 +2446,10 @@ async function runAnswerLoop(input) {
2240
2446
  text,
2241
2447
  answer: text,
2242
2448
  refusalCode: 'provider_error',
2243
- refusalDetails: { code: 'provider_error', message: text },
2449
+ refusalDetails: {
2450
+ code: orchestrationBudget ? 'orchestration_budget_exhausted' : 'provider_error',
2451
+ message: orchestrationBudget ? `${text} (${err.message})` : text,
2452
+ },
2244
2453
  citations: [],
2245
2454
  memoryContext: input.memoryContext,
2246
2455
  evidence: buildNoAnswerEvidence({
@@ -2307,7 +2516,10 @@ async function runAnswerLoop(input) {
2307
2516
  executeGeneratedSql: input.executeGeneratedSql,
2308
2517
  signal: input.signal,
2309
2518
  reasoningEffort: input.reasoningEffort,
2310
- maxAlternatives: deepAlternativeCountForQuestion(questionPlan, intent),
2519
+ // One initial generation plus one diverse candidate stays within the
2520
+ // ordinary two-round ceiling. Candidates are validated without preview
2521
+ // execution below; only the selected SQL reaches the gateway once.
2522
+ maxAlternatives: 1,
2311
2523
  });
2312
2524
  if (selection.selected) {
2313
2525
  proposed = selection.selected.raw;
@@ -2368,13 +2580,16 @@ async function runAnswerLoop(input) {
2368
2580
  proposed = await generateProposalWithOptionalTools({
2369
2581
  provider,
2370
2582
  messages: [...messages, { role: 'system', content: FORCE_JOIN_INSTRUCTION }],
2371
- tools: stageBTools,
2583
+ // One tool round per ordinary Ask. The second generation is a bounded
2584
+ // composition correction over already-retrieved evidence.
2585
+ tools: [],
2372
2586
  questionPlan,
2373
2587
  intent,
2374
2588
  signal: input.signal,
2375
2589
  reasoningEffort: input.reasoningEffort,
2376
2590
  analysisDepth: input.analysisDepth,
2377
2591
  toolCalls: proposalToolCalls,
2592
+ providerPayloadGuard: input.providerPayloadGuard,
2378
2593
  });
2379
2594
  parsed = parseProposal(proposed);
2380
2595
  }
@@ -2406,7 +2621,9 @@ async function runAnswerLoop(input) {
2406
2621
  ? analyticalPlan.userFacingReason
2407
2622
  ?? analyticalPolicyUserFacingReason(analyticalPlan.code ?? 'unsafe_relationship', analyticalPlan.entities)
2408
2623
  : declinedDespiteContext
2409
- ? 'I could not compose a governed query for this from the available tables and metrics. This usually needs a clearer join path or an explicit metric and grouping — name the specific measure and how to break it down, and I can generate a review-required draft.'
2624
+ ? attributeLookup
2625
+ ? 'I could not resolve the requested attribute from the available metadata. I preserved the conversation member, but the inspected models do not prove a relationship to that attribute. Tell me the exact field or table, or re-sync the dbt metadata, and I can prepare a review-required lookup.'
2626
+ : 'I could not compose a governed query for this from the available tables and metrics. This usually needs a clearer join path or an explicit metric and grouping — name the specific measure and how to break it down, and I can generate a review-required draft.'
2410
2627
  : parsed.text || 'No answer (the model declined to propose SQL).';
2411
2628
  return {
2412
2629
  kind: 'no_answer',
@@ -2446,6 +2663,48 @@ async function runAnswerLoop(input) {
2446
2663
  };
2447
2664
  }
2448
2665
  if (parsed.sql) {
2666
+ const frozenPlan = input.resolvedAnalyticalPlan;
2667
+ if (!governedMetricAnswer && frozenPlan?.mode === 'authoritative' && frozenPlan.schemaVersion === 2) {
2668
+ const targetFingerprint = input.generatedProposalTargetFingerprint ?? '';
2669
+ const validation = validateGeneratedAnalyticalProposal({
2670
+ plan: frozenPlan,
2671
+ expectedTargetFingerprint: targetFingerprint,
2672
+ contextPack: input.contextPack,
2673
+ dialect: input.semanticDriver,
2674
+ proposal: {
2675
+ version: 1,
2676
+ planId: frozenPlan.planId,
2677
+ planFingerprint: frozenPlan.fingerprint,
2678
+ snapshotId: frozenPlan.snapshotId,
2679
+ executionId: frozenPlan.executionId ?? '',
2680
+ capabilityFingerprint: frozenPlan.selectedCapabilityFingerprint ?? '',
2681
+ targetFingerprint,
2682
+ sql: parsed.sql,
2683
+ },
2684
+ });
2685
+ if (!validation.ok) {
2686
+ return {
2687
+ kind: 'no_answer',
2688
+ sourceTier: 'no_answer',
2689
+ certification: 'analyst_review_required',
2690
+ reviewStatus: 'none',
2691
+ confidence: 0,
2692
+ text: GENERATED_ANALYTICAL_TUPLE_DRIFT_MESSAGE,
2693
+ answer: GENERATED_ANALYTICAL_TUPLE_DRIFT_MESSAGE,
2694
+ refusalCode: 'policy_blocked',
2695
+ refusalDetails: {
2696
+ code: validation.code,
2697
+ message: `${validation.message}: ${validation.driftCodes.join(', ')}`,
2698
+ },
2699
+ validationWarnings: validation.driftCodes,
2700
+ citations: contextPackCitations(input.contextPack, 8),
2701
+ memoryContext: input.memoryContext,
2702
+ contextPack: input.contextPack,
2703
+ considered,
2704
+ providerUsed: provider.name,
2705
+ };
2706
+ }
2707
+ }
2449
2708
  const tightenedFlow = tightenSourceTargetFlowProjection(parsed.sql, question, questionPlan);
2450
2709
  if (tightenedFlow && tightenedFlow.sql !== parsed.sql) {
2451
2710
  parsed.sql = tightenedFlow.sql;
@@ -2479,8 +2738,52 @@ async function runAnswerLoop(input) {
2479
2738
  // semantic environment. A thin local retrieval pack cannot safely reject it
2480
2739
  // and replace it with a guessed leaf-measure SQL. Warehouse execution remains
2481
2740
  // the final binder/runtime check.
2741
+ const toolOwnedMetricNames = semanticBridgeAnswer?.metrics
2742
+ ?? compiledSemanticRecords.flatMap((record) => record.metrics);
2743
+ const semanticProofAuthority = input.resolvedAnalyticalPlan
2744
+ ? exactSemanticProofAuthority(input.resolvedAnalyticalPlan, semanticMetricNodes)
2745
+ : (input.allowProviderSemanticMemberSelection === true || compiledSemanticRecords.length > 0)
2746
+ ? exactToolSemanticProofAuthority(toolOwnedMetricNames, semanticMetricNodes)
2747
+ : { metricObjects: [] };
2748
+ const semanticProofContext = semanticRouteAggregationProofContext(input.contextPack, input.resolvedAnalyticalPlan, semanticProofAuthority.capability, semanticProofAuthority.metricObjects);
2749
+ const toolOwnedSemanticProofContext = !input.resolvedAnalyticalPlan
2750
+ && semanticProofAuthority.capability
2751
+ && (input.allowProviderSemanticMemberSelection === true || compiledSemanticRecords.length > 0)
2752
+ ? {
2753
+ ...(semanticProofContext ?? {}),
2754
+ questionPlan: {
2755
+ ...(semanticProofContext?.questionPlan ?? buildAnalysisQuestionPlan(question)),
2756
+ requestedShape: {
2757
+ ...(semanticProofContext?.questionPlan?.requestedShape ?? buildAnalysisQuestionPlan(question).requestedShape),
2758
+ grain: semanticBridgeAnswer?.dimensions[0] ?? semanticProofAuthority.capability.defaultResultGrainId,
2759
+ dimensions: semanticBridgeAnswer?.dimensions ?? [],
2760
+ },
2761
+ },
2762
+ }
2763
+ : semanticProofContext;
2764
+ const semanticAggregationProof = governedMetricAnswer && !certifiedAdaptation
2765
+ ? buildAggregationSafetyProof(parsed.sql, toolOwnedSemanticProofContext, input.semanticDriver, input.resolvedAnalyticalPlan?.fingerprint)
2766
+ : undefined;
2767
+ const semanticExecutionRequested = governedMetricAnswer
2768
+ && !certifiedAdaptation
2769
+ && Boolean(input.executeGeneratedSql || input.executeDqlArtifact);
2770
+ const semanticAggregationAuthorityBlocked = semanticExecutionRequested
2771
+ && semanticAggregationProof?.status !== 'safe';
2482
2772
  const initialValidation = semanticRuntimeCompiledAnswer
2483
- ? { ok: true, warnings: ['SQL compiled by the configured semantic runtime.'] }
2773
+ ? semanticAggregationProof?.status === 'safe' || !semanticExecutionRequested
2774
+ ? {
2775
+ ok: true,
2776
+ warnings: ['SQL compiled by the configured semantic runtime.'],
2777
+ aggregationSafetyProof: semanticAggregationProof,
2778
+ }
2779
+ : {
2780
+ ok: false,
2781
+ code: 'unsafe_aggregation',
2782
+ error: semanticAggregationSafetyFailure(semanticAggregationProof),
2783
+ warnings: ['Semantic runtime compilation did not establish complete aggregation authority.'],
2784
+ offending: undefined,
2785
+ aggregationSafetyProof: semanticAggregationProof,
2786
+ }
2484
2787
  : contextLedger.validateSql(parsed.sql, {
2485
2788
  question,
2486
2789
  intent,
@@ -2489,6 +2792,9 @@ async function runAnswerLoop(input) {
2489
2792
  memberBindings: input.followUp?.memberBindings,
2490
2793
  enforceAggregationIntegrity: !governedMetricAnswer,
2491
2794
  });
2795
+ let aggregationSafetyProof = 'aggregationSafetyProof' in initialValidation
2796
+ ? initialValidation.aggregationSafetyProof
2797
+ : undefined;
2492
2798
  const rankedGrainGap = missingRankedGrainOutput(questionPlan, parsed.sql, semanticBridgeAnswer?.dimensions);
2493
2799
  contextValidation = initialValidation.ok && rankedGrainGap
2494
2800
  ? {
@@ -2498,14 +2804,37 @@ async function runAnswerLoop(input) {
2498
2804
  warnings: initialValidation.warnings,
2499
2805
  }
2500
2806
  : initialValidation.ok
2501
- ? { ok: true, warnings: initialValidation.warnings }
2807
+ ? { ok: true, warnings: initialValidation.warnings, aggregationSafetyProof }
2502
2808
  : {
2503
2809
  ok: false,
2504
2810
  code: initialValidation.code,
2505
2811
  error: initialValidation.error,
2506
2812
  warnings: initialValidation.warnings,
2507
2813
  offending: initialValidation.offending,
2814
+ aggregationSafetyProof,
2508
2815
  };
2816
+ // The route-specific semantic proof is authoritative for execution. Context
2817
+ // validation can prove SQL names without proving that the selected metric's
2818
+ // exact capability, grain, additivity and joins own this aggregate. Never let
2819
+ // a broad context pack or warehouse success elevate that blocked state.
2820
+ if (contextValidation.ok
2821
+ && semanticAggregationAuthorityBlocked) {
2822
+ aggregationSafetyProof = semanticAggregationProof;
2823
+ contextValidation = {
2824
+ ok: false,
2825
+ code: 'unsafe_aggregation',
2826
+ error: semanticAggregationSafetyFailure(semanticAggregationProof),
2827
+ warnings: [
2828
+ ...contextValidation.warnings,
2829
+ 'The selected semantic route lacks an exact positive aggregation proof.',
2830
+ ],
2831
+ aggregationSafetyProof: semanticAggregationProof,
2832
+ };
2833
+ }
2834
+ else if (semanticAggregationProof) {
2835
+ aggregationSafetyProof = semanticAggregationProof;
2836
+ contextValidation = { ...contextValidation, aggregationSafetyProof: semanticAggregationProof };
2837
+ }
2509
2838
  // A canonical member value that resolves to exactly one inspected column is
2510
2839
  // compiler-owned request state, not creative SQL. If the proposal omitted
2511
2840
  // that predicate entirely, inject it deterministically before spending the
@@ -2529,6 +2858,7 @@ async function runAnswerLoop(input) {
2529
2858
  const reboundGrainGap = rebound.ok
2530
2859
  ? missingRankedGrainOutput(questionPlan, parsed.sql, semanticBridgeAnswer?.dimensions)
2531
2860
  : undefined;
2861
+ aggregationSafetyProof = rebound.aggregationSafetyProof;
2532
2862
  contextValidation = rebound.ok && !reboundGrainGap
2533
2863
  ? { ok: true, warnings: [...rebound.warnings, ...injected.notes] }
2534
2864
  : rebound.ok
@@ -2676,7 +3006,20 @@ async function runAnswerLoop(input) {
2676
3006
  // provider get the same single chance; any provider failure keeps the honest
2677
3007
  // refusal below. This mirrors the engine-level repair loop, applied to the
2678
3008
  // context-validation gate that previously refused on first failure.
2679
- if (!contextValidation.ok && !governedMetricAnswer && canUseLaneRepair(repairBudgetState, 'validation')) {
3009
+ // Semantic aggregation authority cannot be created by a provider rewrite. In
3010
+ // particular, clearing governedMetricAnswer after a blocked semantic proof
3011
+ // must not turn that refusal into the generic SQL-repair lane: the provider
3012
+ // could return grounded raw-row SQL and silently downgrade the attempt to an
3013
+ // uncertified execution. Keep this semantic proof failure terminal while
3014
+ // retaining only the separate typed warehouse SQL-only repair capability.
3015
+ // Unsafe aggregation/rounding is never rewritten automatically until the SQL
3016
+ // parser supplies exact nested-expression AST spans.
3017
+ if (!contextValidation.ok
3018
+ && contextValidation.code !== 'unsafe_aggregation'
3019
+ && input.resolvedAnalyticalPlan?.mode !== 'authoritative'
3020
+ && !semanticAggregationAuthorityBlocked
3021
+ && !governedMetricAnswer
3022
+ && canUseLaneRepair(repairBudgetState, 'validation')) {
2680
3023
  recordLaneRepair(repairBudgetState, 'validation');
2681
3024
  try {
2682
3025
  const failedSql = parsed.sql ?? '';
@@ -2782,7 +3125,9 @@ async function runAnswerLoop(input) {
2782
3125
  answer: text,
2783
3126
  proposedSql: parsed.sql,
2784
3127
  sql: parsed.sql,
2785
- trustLabel: input.contextPack?.trustLabel,
3128
+ trustLabel: contextValidation.code === 'unsafe_aggregation'
3129
+ ? 'blocked'
3130
+ : input.contextPack?.trustLabel,
2786
3131
  sourceCertifiedBlock: followUpSourceBlock?.name ?? input.followUp?.sourceBlockName,
2787
3132
  contextPackId: input.contextPack?.id,
2788
3133
  validationWarnings: [
@@ -2808,6 +3153,7 @@ async function runAnswerLoop(input) {
2808
3153
  contextPack: input.contextPack,
2809
3154
  considered,
2810
3155
  providerUsed: provider.name,
3156
+ aggregationSafetyProof,
2811
3157
  };
2812
3158
  }
2813
3159
  // The dbt-first join guard adjudicates GENERATED SQL — joins an LLM invented,
@@ -3050,6 +3396,7 @@ async function runAnswerLoop(input) {
3050
3396
  memberBindings: input.followUp?.memberBindings,
3051
3397
  });
3052
3398
  if (repairedValidation.ok) {
3399
+ aggregationSafetyProof = repairedValidation.aggregationSafetyProof;
3053
3400
  repairAttempts += 1;
3054
3401
  // Adopt the corrected SQL, but do NOT let the repair prose become the answer.
3055
3402
  repairNarrative = repaired.text?.trim() || undefined;
@@ -3282,6 +3629,7 @@ async function runAnswerLoop(input) {
3282
3629
  contextPack: input.contextPack,
3283
3630
  considered,
3284
3631
  providerUsed: provider.name,
3632
+ aggregationSafetyProof,
3285
3633
  // Carry the governed metric match so the exit point can name a
3286
3634
  // `semantic_metric` route (spec 17, part C).
3287
3635
  _semanticMetricMatch: governedMetricAnswer ? semanticMetricMatch ?? undefined : undefined,
@@ -3322,6 +3670,23 @@ function resultColumnNames(result) {
3322
3670
  return columns?.length ? columns : undefined;
3323
3671
  }
3324
3672
  function analyticalCapabilityForPlan(plan, contextPack, registry = []) {
3673
+ const selected = normalizeMetricCapabilityContract(plan.selectedCapability);
3674
+ if (selected) {
3675
+ const selectedMetricId = plan.analyticalFrame?.metricConceptIds[0]
3676
+ ?? plan.query.measures[0]?.qualifiedId;
3677
+ const exactRegistryAuthority = registry.some((entry) => Boolean(plan.executionId)
3678
+ && entry.identities.includes(plan.executionId)
3679
+ && normalizeMetricCapabilityContract(entry.node.payload?.analyticalCapability)?.sourceFingerprint
3680
+ === selected.sourceFingerprint);
3681
+ if (plan.selectedCapabilityFingerprint !== selected.sourceFingerprint
3682
+ || (selectedMetricId && selected.metricId !== selectedMetricId && !exactRegistryAuthority))
3683
+ return undefined;
3684
+ return selected;
3685
+ }
3686
+ // New authoritative v2 plans always carry the exact normalized capability.
3687
+ // A missing authority is terminal rather than permission to rematch by name.
3688
+ if (plan.mode === 'authoritative' && plan.schemaVersion === 2)
3689
+ return undefined;
3325
3690
  const candidates = contextPack?.retrievalDiagnostics.meaningEvidence?.candidates ?? [];
3326
3691
  const exactExecution = candidates.filter((candidate) => candidate.objectKey === plan.executionId || candidate.qualifiedId === plan.executionId);
3327
3692
  const metricId = plan.analyticalFrame?.metricConceptIds[0];
@@ -3343,6 +3708,142 @@ function analyticalCapabilityForPlan(plan, contextPack, registry = []) {
3343
3708
  const byFingerprint = new Map(normalized.map((capability) => [capability.sourceFingerprint, capability]));
3344
3709
  return byFingerprint.size === 1 ? [...byFingerprint.values()][0] : undefined;
3345
3710
  }
3711
+ function semanticRouteAggregationProofContext(contextPack, plan, capability, exactMetricObjects = []) {
3712
+ if (!plan || !capability) {
3713
+ return exactMetricObjects.length > 0
3714
+ ? { ...(contextPack ?? {}), objects: exactMetricObjects }
3715
+ : contextPack;
3716
+ }
3717
+ const baseQuestionPlan = buildAnalysisQuestionPlan(plan.question);
3718
+ if ((plan.selectedCapabilityFingerprint !== undefined
3719
+ && plan.selectedCapabilityFingerprint !== capability.sourceFingerprint)
3720
+ || (plan.analyticalFrame !== undefined
3721
+ && plan.analyticalFrame.metricConceptIds[0] !== capability.metricId))
3722
+ return {
3723
+ ...(contextPack ?? {}),
3724
+ objects: [],
3725
+ questionPlan: {
3726
+ ...baseQuestionPlan,
3727
+ requestedShape: { ...baseQuestionPlan.requestedShape, dimensions: [] },
3728
+ },
3729
+ };
3730
+ const contextMetricObjects = (contextPack?.objects ?? []).filter((object) => {
3731
+ const objectCapability = normalizeMetricCapabilityContract(object.payload?.analyticalCapability);
3732
+ if (!objectCapability || objectCapability.sourceFingerprint !== capability.sourceFingerprint)
3733
+ return false;
3734
+ return [object.objectKey, object.fullName, object.name, object.payload?.qualifiedId]
3735
+ .filter((value) => Boolean(value))
3736
+ .includes(plan.executionId ?? capability.metricId);
3737
+ });
3738
+ const relationshipIds = new Set(plan.relationshipPathIds);
3739
+ const relationshipObjects = (contextPack?.objects ?? []).filter((object) => object.objectType.includes('relationship')
3740
+ && [object.objectKey, object.fullName, object.name, object.payload?.qualifiedId]
3741
+ .filter((value) => Boolean(value))
3742
+ .some((value) => relationshipIds.has(value)));
3743
+ const frozenDimensions = plan.analyticalFrame?.dimensions
3744
+ .filter((dimension) => dimension.role === 'group_by' || dimension.role === 'rank_entity')
3745
+ .map((dimension) => dimension.dimensionId);
3746
+ const legacyCapabilityDimensions = !plan.analyticalFrame && plan.query.dimensions.length > 0
3747
+ ? capability.dimensions.filter((dimension) => dimension.supportedRoles.includes('group_by'))
3748
+ : [];
3749
+ const dimensions = frozenDimensions
3750
+ ?? (legacyCapabilityDimensions.length === plan.query.dimensions.length
3751
+ ? legacyCapabilityDimensions.map((dimension) => dimension.dimensionId)
3752
+ : plan.query.dimensions.flatMap((dimension) => dimension.qualifiedId ? [dimension.qualifiedId] : []));
3753
+ const legacyGrains = legacyCapabilityDimensions.map((dimension) => dimension.entityId)
3754
+ .filter((entityId) => capability.resultGrainIds.includes(entityId));
3755
+ const grain = plan.analyticalFrame?.entityGrainIds[0]
3756
+ ?? plan.entityGrain
3757
+ ?? (legacyGrains.length === 1 ? legacyGrains[0] : undefined)
3758
+ ?? (dimensions.length === 0 ? capability.defaultResultGrainId : undefined);
3759
+ const exactObjects = new Map();
3760
+ for (const object of [...contextMetricObjects, ...exactMetricObjects, ...relationshipObjects]) {
3761
+ const authorityKey = `${object.objectKey}:${normalizeMetricCapabilityContract(object.payload?.analyticalCapability)?.sourceFingerprint ?? ''}`;
3762
+ if (!exactObjects.has(authorityKey))
3763
+ exactObjects.set(authorityKey, object);
3764
+ }
3765
+ return {
3766
+ ...(contextPack ?? {}),
3767
+ objects: [...exactObjects.values()],
3768
+ questionPlan: {
3769
+ ...baseQuestionPlan,
3770
+ requestedShape: {
3771
+ ...baseQuestionPlan.requestedShape,
3772
+ dimensions,
3773
+ ...(grain ? { grain } : {}),
3774
+ },
3775
+ },
3776
+ };
3777
+ }
3778
+ function exactSemanticProofAuthority(plan, metricNodes) {
3779
+ if (!plan)
3780
+ return { metricObjects: [] };
3781
+ const embedded = normalizeMetricCapabilityContract(plan.selectedCapability);
3782
+ const planMetricIds = new Set([
3783
+ plan.executionId,
3784
+ ...plan.query.measures.flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : []),
3785
+ ].filter((value) => Boolean(value)));
3786
+ const exactNodes = metricNodes.filter((metric) => {
3787
+ const identities = [
3788
+ metric.nodeId,
3789
+ metric.payload?.qualifiedId,
3790
+ metric.payload?.localId,
3791
+ ].filter((value) => typeof value === 'string' && Boolean(value));
3792
+ return identities.some((identity) => planMetricIds.has(identity));
3793
+ });
3794
+ const capabilities = exactNodes.flatMap((metric) => {
3795
+ const capability = normalizeMetricCapabilityContract(metric.payload?.analyticalCapability);
3796
+ return capability ? [capability] : [];
3797
+ });
3798
+ const selected = embedded ?? (new Map(capabilities.map((capability) => [capability.sourceFingerprint, capability])).size === 1
3799
+ ? capabilities[0]
3800
+ : undefined);
3801
+ if (!selected || (plan.selectedCapabilityFingerprint && plan.selectedCapabilityFingerprint !== selected.sourceFingerprint)) {
3802
+ return { metricObjects: [] };
3803
+ }
3804
+ return {
3805
+ capability: selected,
3806
+ metricObjects: exactNodes.flatMap((metric) => metric.payload ? [{
3807
+ objectKey: metric.nodeId,
3808
+ objectType: 'semantic_metric',
3809
+ name: metric.name,
3810
+ ...(metric.domain ? { domain: metric.domain } : {}),
3811
+ ...(metric.status ? { status: metric.status } : {}),
3812
+ payload: metric.payload,
3813
+ }] : []),
3814
+ };
3815
+ }
3816
+ function exactToolSemanticProofAuthority(metricNames, metricNodes) {
3817
+ const exactNodes = [...new Set(metricNames)].flatMap((name) => {
3818
+ const matches = metricNodes.filter((node) => node.name === name || node.name.endsWith(`.${name}`) || name.endsWith(`.${node.name}`));
3819
+ return matches.length === 1 ? matches : [];
3820
+ });
3821
+ if (exactNodes.length !== new Set(metricNames).size)
3822
+ return { metricObjects: [] };
3823
+ const capabilities = exactNodes.flatMap((node) => {
3824
+ const capability = normalizeMetricCapabilityContract(node.payload?.analyticalCapability);
3825
+ return capability ? [capability] : [];
3826
+ });
3827
+ if (capabilities.length !== exactNodes.length)
3828
+ return { metricObjects: [] };
3829
+ return {
3830
+ capability: capabilities[0],
3831
+ metricObjects: exactNodes.map((metric) => ({
3832
+ objectKey: metric.nodeId,
3833
+ objectType: 'semantic_metric',
3834
+ name: metric.name,
3835
+ ...(metric.domain ? { domain: metric.domain } : {}),
3836
+ ...(metric.status ? { status: metric.status } : {}),
3837
+ payload: metric.payload,
3838
+ })),
3839
+ };
3840
+ }
3841
+ function semanticAggregationSafetyFailure(proof) {
3842
+ const issues = proof?.issueCodes.length
3843
+ ? proof.issueCodes.join(', ')
3844
+ : 'AGGREGATION_PROOF_MISSING';
3845
+ return `The selected semantic route did not establish an exact aggregation safety proof (${issues}). No SQL was executed.`;
3846
+ }
3346
3847
  function analyticalRouteForPlan(plan) {
3347
3848
  if (plan.capability === 'certified_execution' && plan.recommendedRoute === 'certified')
3348
3849
  return 'certified';
@@ -3440,6 +3941,8 @@ async function executeSemanticAnalyticalGraph(input) {
3440
3941
  const compiledSql = [];
3441
3942
  const artifacts = [];
3442
3943
  let semanticExecutionTrace;
3944
+ let aggregationCompilerReceipt;
3945
+ let aggregationSafetyProof;
3443
3946
  for (const invocation of input.binding.invocations) {
3444
3947
  const attemptedArtifact = semanticAttemptArtifact(layer, input.input.question, invocation.selection);
3445
3948
  let composed = composeSemanticQueryFromMembers({
@@ -3453,6 +3956,7 @@ async function executeSemanticAnalyticalGraph(input) {
3453
3956
  try {
3454
3957
  const compiled = await input.input.semanticQueryCompiler(invocation.selection);
3455
3958
  semanticExecutionTrace = compiled.trace;
3959
+ aggregationCompilerReceipt = compiled.aggregationCompilerReceipt;
3456
3960
  composed = composeSemanticQueryFromCompiledMembers({
3457
3961
  semanticLayer: layer,
3458
3962
  question: input.input.question,
@@ -3503,6 +4007,42 @@ async function executeSemanticAnalyticalGraph(input) {
3503
4007
  failedBindings: [{ role: 'source_invocation', reasonCode: 'SEMANTIC_COMPILE_FAILED' }],
3504
4008
  });
3505
4009
  }
4010
+ const compilerProof = semanticRatioCompilerProofBinding(layer, invocation.selection.metrics, input.capability, input.input.resolvedAnalyticalPlan, semanticTraceTargetFingerprint(semanticExecutionTrace), composed.sql, aggregationCompilerReceipt);
4011
+ const proof = buildSemanticCompilationAggregationSafetyProof({
4012
+ sql: composed.sql,
4013
+ capability: input.capability,
4014
+ semanticAuthority: input.input.resolvedAnalyticalPlan
4015
+ ? buildSemanticProofAuthorityV1({
4016
+ plan: input.input.resolvedAnalyticalPlan,
4017
+ capability: input.capability,
4018
+ contextPack: input.input.contextPack,
4019
+ compilerReceipt: aggregationCompilerReceipt,
4020
+ })
4021
+ : undefined,
4022
+ relationshipProofs: input.input.resolvedAnalyticalPlan?.relationshipProofs,
4023
+ executionId: input.input.resolvedAnalyticalPlan?.executionId,
4024
+ snapshotId: input.input.resolvedAnalyticalPlan?.snapshotId,
4025
+ capabilityFingerprint: input.input.resolvedAnalyticalPlan?.selectedCapabilityFingerprint,
4026
+ route: 'semantic',
4027
+ adapterId: input.graph.adapterId,
4028
+ ...compilerProof.binding,
4029
+ ...(compilerProof.issueCodes.length
4030
+ ? { compilerAuthorityIssueCodes: compilerProof.issueCodes }
4031
+ : {}),
4032
+ contextPack: semanticRouteAggregationProofContext(input.input.contextPack, input.input.resolvedAnalyticalPlan, input.capability),
4033
+ dialect: input.input.semanticDriver,
4034
+ planFingerprint: input.binding.planFingerprint,
4035
+ });
4036
+ aggregationSafetyProof = proof;
4037
+ if (proof.status !== 'safe') {
4038
+ return analyticalGraphFailureAnswer(input, 'EXECUTION_GRAPH_MISMATCH', semanticAggregationSafetyFailure(proof), {
4039
+ phase: 'validation',
4040
+ dqlArtifact: composed.dqlArtifact,
4041
+ compiledSql: composed.sql,
4042
+ semanticExecutionTrace,
4043
+ aggregationSafetyProof: proof,
4044
+ });
4045
+ }
3506
4046
  compiledSql.push(composed.sql);
3507
4047
  artifacts.push(composed.dqlArtifact);
3508
4048
  const executor = input.input.executeDqlArtifact
@@ -3562,6 +4102,7 @@ async function executeSemanticAnalyticalGraph(input) {
3562
4102
  sql,
3563
4103
  ...(artifacts.length === 1 ? { dqlArtifact: artifacts[0] } : {}),
3564
4104
  analyticalExecutionGraph: input.graph,
4105
+ aggregationSafetyProof,
3565
4106
  ...(semanticExecutionTrace
3566
4107
  ? { semanticExecutionTrace: semanticTraceAfterExecution(semanticExecutionTrace, { executed: false }) }
3567
4108
  : {}),
@@ -3606,6 +4147,7 @@ async function executeSemanticAnalyticalGraph(input) {
3606
4147
  ...(artifacts.length === 1 ? { dqlArtifact: artifacts[0] } : {}),
3607
4148
  result,
3608
4149
  analyticalExecutionGraph: input.graph,
4150
+ aggregationSafetyProof,
3609
4151
  analyticalExecutionReceipt: executed.receipt,
3610
4152
  analyticalFacts: story.factSet,
3611
4153
  analyticalNarrative: story.narrative,
@@ -3618,6 +4160,137 @@ async function executeSemanticAnalyticalGraph(input) {
3618
4160
  providerUsed: input.providerName,
3619
4161
  };
3620
4162
  }
4163
+ function semanticRatioCompilerProofBinding(layer, selectedMetricNames, capability, plan, targetFingerprint, compiledSql, receipt) {
4164
+ if (capability.aggregation.trim().toLowerCase() !== 'ratio')
4165
+ return { issueCodes: [] };
4166
+ if (receipt) {
4167
+ const outputAlias = capability.declaredOutputIds?.length === 1
4168
+ ? capability.declaredOutputIds[0]
4169
+ : capability.metricId.split(/[:.]/).at(-1);
4170
+ const { receiptFingerprint, ...receiptBody } = receipt;
4171
+ const compiledExpression = outputAlias
4172
+ ? buildSqlOutputExpressionSignature(compiledSql, outputAlias)
4173
+ : undefined;
4174
+ const dimensionIds = [...new Set(plan.analyticalFrame?.dimensions.map((dimension) => dimension.dimensionId) ?? [])].sort();
4175
+ const relationshipPathIds = [...plan.relationshipPathIds].sort();
4176
+ const relationshipProofFingerprints = [...new Set(plan.relationshipProofs?.map((proof) => proof.authorityFingerprint) ?? [])].sort();
4177
+ const expectedOrderBy = semanticCompilerReceiptOrder(plan, capability);
4178
+ if (receipt.version !== 1
4179
+ || receipt.executionId !== plan.executionId
4180
+ || receipt.capabilityFingerprint !== plan.selectedCapabilityFingerprint
4181
+ || receipt.metricId !== capability.metricId
4182
+ || receipt.operator !== 'ratio'
4183
+ || receipt.planFingerprint !== plan.fingerprint
4184
+ || receipt.snapshotId !== plan.snapshotId
4185
+ || !targetFingerprint
4186
+ || receipt.targetFingerprint !== targetFingerprint
4187
+ || receipt.compiledSqlFingerprint !== semanticExecutionFingerprint(compiledSql)
4188
+ || !compiledExpression
4189
+ || receipt.compiledExpressionFingerprint !== semanticExecutionFingerprint(compiledExpression.canonicalExpression)
4190
+ || receipt.orderedMeasureIds.join('|') !== capability.measureIds.join('|')
4191
+ || [...receipt.dimensionIds].sort().join('|') !== dimensionIds.join('|')
4192
+ || [...receipt.relationshipPathIds].sort().join('|') !== relationshipPathIds.join('|')
4193
+ || [...receipt.relationshipProofFingerprints].sort().join('|') !== relationshipProofFingerprints.join('|')
4194
+ || receipt.orderBy.length !== expectedOrderBy.length
4195
+ || receipt.orderBy.some((order, index) => {
4196
+ const expected = expectedOrderBy[index];
4197
+ return !expected
4198
+ || normalizeSemanticProofId(order.expression) !== normalizeSemanticProofId(expected.expression)
4199
+ || order.direction !== expected.direction;
4200
+ })
4201
+ || receipt.outputAlias !== outputAlias
4202
+ || receiptFingerprint !== semanticExecutionFingerprint(receiptBody)) {
4203
+ return { issueCodes: ['SEMANTIC_COMPILER_RECEIPT_AUTHORITY_MISMATCH'] };
4204
+ }
4205
+ return {
4206
+ binding: {
4207
+ compilerMetricExpressionSql: receipt.metricExpressionSql,
4208
+ compilerMetricId: receipt.metricId,
4209
+ compilerMeasureIds: receipt.orderedMeasureIds,
4210
+ compilerRelation: receipt.physicalRelation,
4211
+ compilerRelationAliases: receipt.relationAliases,
4212
+ },
4213
+ issueCodes: [],
4214
+ };
4215
+ }
4216
+ const selected = new Set(selectedMetricNames.map(normalizeSemanticProofId));
4217
+ const capabilityMetric = normalizeSemanticProofId(capability.metricId);
4218
+ const metrics = layer.listMetrics(undefined, { includeMeasures: false }).filter((metric) => {
4219
+ const names = [metric.name, metric.cube ? `${metric.cube}.${metric.name}` : undefined]
4220
+ .filter((value) => Boolean(value))
4221
+ .map(normalizeSemanticProofId);
4222
+ return names.includes(capabilityMetric)
4223
+ && names.some((name) => selected.has(name));
4224
+ });
4225
+ if (metrics.length !== 1 || !metrics[0].sql.trim())
4226
+ return { issueCodes: ['SEMANTIC_COMPILER_RECEIPT_AUTHORITY_MISSING'] };
4227
+ const metric = metrics[0];
4228
+ const measures = capability.measureIds.map((measureId) => {
4229
+ const normalizedId = normalizeSemanticProofId(measureId);
4230
+ const matches = layer.listMeasures().filter((measure) => {
4231
+ const names = [measure.name, measure.cube ? `${measure.cube}.${measure.name}` : undefined]
4232
+ .filter((value) => Boolean(value))
4233
+ .map(normalizeSemanticProofId);
4234
+ return names.includes(normalizedId)
4235
+ && (!metric.cube || !measure.cube || measure.cube === metric.cube);
4236
+ });
4237
+ return matches.length === 1 ? matches[0] : undefined;
4238
+ });
4239
+ if (measures.some((measure) => !measure))
4240
+ return { issueCodes: ['SEMANTIC_COMPILER_RECEIPT_AUTHORITY_MISSING'] };
4241
+ const relations = Array.from(new Set(measures.map((measure) => measure.table.trim()).filter(Boolean)));
4242
+ if (relations.length !== 1)
4243
+ return { issueCodes: ['SEMANTIC_COMPILER_RECEIPT_AUTHORITY_MISSING'] };
4244
+ return {
4245
+ binding: {
4246
+ compilerMetricExpressionSql: metric.sql,
4247
+ compilerMetricId: capability.metricId,
4248
+ compilerMeasureIds: [...capability.measureIds],
4249
+ compilerRelation: relations[0],
4250
+ ...(metric.cube ? { compilerRelationAliases: [metric.cube] } : {}),
4251
+ },
4252
+ issueCodes: [],
4253
+ };
4254
+ }
4255
+ function semanticCompilerReceiptOrder(plan, capability) {
4256
+ const ranking = plan.analyticalFrame?.ranking;
4257
+ if (!ranking || ranking.byMetricId !== capability.metricId)
4258
+ return [];
4259
+ const metricOutput = plan.analyticalFrame?.requestedOutputs.find((output) => output.kind === 'metric_value'
4260
+ && output.metricId === ranking.byMetricId
4261
+ && (!ranking.byPeriodId || output.periodId === ranking.byPeriodId));
4262
+ if (!metricOutput)
4263
+ return [];
4264
+ const order = [{ expression: metricOutput.id, direction: ranking.direction }];
4265
+ if (ranking.tiePolicy !== 'stable_secondary_key')
4266
+ return order;
4267
+ const dimensionOutputs = plan.analyticalFrame.requestedOutputs
4268
+ .filter((output) => output.kind === 'dimension');
4269
+ return [
4270
+ ...order,
4271
+ ...dimensionOutputs.map((output) => ({ expression: output.id, direction: 'asc' })),
4272
+ ];
4273
+ }
4274
+ function semanticTraceTargetFingerprint(trace) {
4275
+ const targetBinding = trace?.targetBinding;
4276
+ if (!targetBinding || typeof targetBinding !== 'object' || Array.isArray(targetBinding))
4277
+ return undefined;
4278
+ const executionTarget = targetBinding.executionTarget;
4279
+ if (!executionTarget || typeof executionTarget !== 'object' || Array.isArray(executionTarget))
4280
+ return undefined;
4281
+ const identityFingerprint = executionTarget.identityFingerprint;
4282
+ return typeof identityFingerprint === 'string' && identityFingerprint.trim()
4283
+ ? identityFingerprint
4284
+ : undefined;
4285
+ }
4286
+ function normalizeSemanticProofId(value) {
4287
+ return value
4288
+ .trim()
4289
+ .toLowerCase()
4290
+ .replace(/^semantic(?:::|:)/, '')
4291
+ .replace(/^(?:metric|measure)[.:]/, '')
4292
+ .replaceAll(':', '.');
4293
+ }
3621
4294
  async function executeGovernedRelationalAnalyticalGraph(input) {
3622
4295
  const sourceResults = {};
3623
4296
  const artifacts = [];
@@ -3822,6 +4495,7 @@ function analyticalGraphFailureAnswer(input, code, reason, artifacts = {}) {
3822
4495
  ...(artifacts.compiledSql ? { proposedSql: artifacts.compiledSql, sql: artifacts.compiledSql } : {}),
3823
4496
  ...(artifacts.dqlArtifact ? { dqlArtifact: artifacts.dqlArtifact } : {}),
3824
4497
  ...(artifacts.semanticExecutionTrace ? { semanticExecutionTrace: artifacts.semanticExecutionTrace } : {}),
4498
+ ...(artifacts.aggregationSafetyProof ? { aggregationSafetyProof: artifacts.aggregationSafetyProof } : {}),
3825
4499
  citations: contextPackCitations(input.input.contextPack, 8),
3826
4500
  considered: input.considered,
3827
4501
  contextPack: input.input.contextPack,
@@ -3834,7 +4508,7 @@ function analyticalFailureForInput(input, diagnostic) {
3834
4508
  error: diagnostic.error,
3835
4509
  code: diagnostic.code,
3836
4510
  phase: diagnostic.phase,
3837
- snapshotId: plan?.snapshotId ?? input.contextPack?.knowledgeLens.snapshotId ?? 'snapshot-unavailable',
4511
+ snapshotId: plan?.snapshotId ?? input.contextPack?.knowledgeLens?.snapshotId ?? 'snapshot-unavailable',
3838
4512
  runId: diagnostic.runId ?? plan?.planId,
3839
4513
  planFingerprint: plan?.fingerprint,
3840
4514
  dqlSource: diagnostic.dqlArtifact?.source,
@@ -4377,6 +5051,77 @@ function renderContextPrompt(blocks, businessContext, others, activeTier, memory
4377
5051
  : '';
4378
5052
  return `${intentSection}${budgetSection}${dialectSection}\n\n${blockSection}${businessSection}${otherSection}${kgJoinSection}${schemaSection}${contextPackSection}${memorySection}${hintsSection}${conversationSection}${extraSection}${followUpSection}`;
4379
5053
  }
5054
+ const ATTRIBUTE_LOOKUP_CONCEPTS = new Set([
5055
+ 'account', 'category', 'channel', 'city', 'country', 'department', 'description',
5056
+ 'industry', 'label', 'location', 'market', 'name', 'owner', 'region', 'segment',
5057
+ 'state', 'status', 'team', 'tier', 'title', 'type', 'user',
5058
+ ]);
5059
+ /**
5060
+ * Attribute questions are valid analytical asks even when they have no
5061
+ * measure. A conversational reference such as "what region does he belong to?"
5062
+ * carries a typed member binding from the prior result; forcing a metric here
5063
+ * turns a straightforward dimensional lookup into the old generic governed
5064
+ * error. Keep this as a generation hint, not a deterministic semantic match:
5065
+ * the provider still has to choose a relation and the SQL guard still proves
5066
+ * the resulting query. AGT-031.
5067
+ */
5068
+ function isMetricIndependentAttributeLookup(question, questionPlan, followUp) {
5069
+ if (questionPlan.mode === 'ranking' || questionPlan.mode === 'comparison' || questionPlan.mode === 'definition')
5070
+ return false;
5071
+ const lower = question.toLowerCase();
5072
+ // Follow-up planning intentionally carries the previous measure so that a
5073
+ // refinement such as "and by region?" remains grounded. That carry must not
5074
+ // turn a new attribute request into a metric request. Only a measure named in
5075
+ // this question (or a non-carried custom term) disables this lane.
5076
+ const carriedTerms = new Set([
5077
+ ...(followUp?.priorMeasures ?? []),
5078
+ ...(followUp?.priorResultColumns ?? []),
5079
+ ...(followUp?.priorDqlArtifact?.metrics ?? []),
5080
+ ].map((term) => term.toLowerCase().replace(/[_-]+/g, ' ').trim()));
5081
+ const carriedOnly = (term) => carriedTerms.has(term.toLowerCase().replace(/[_-]+/g, ' ').trim());
5082
+ const directMeasures = questionPlan.requestedShape.measures.filter((term) => !carriedOnly(term));
5083
+ const directMetricTerms = questionPlan.metricTerms.filter((term) => !carriedOnly(term));
5084
+ if (directMeasures.length > 0 || directMetricTerms.length > 0)
5085
+ return false;
5086
+ if (/\b(?:revenue|sales?|amount|spend|cost|margin|profit|orders?|count|quantity|units?|value|rate|score|usage|growth|churn|conversion|tax)\b/i.test(lower))
5087
+ return false;
5088
+ const attributeTerms = new Set([
5089
+ ...questionPlan.dimensionTerms,
5090
+ ...questionPlan.requestedShape.dimensions,
5091
+ ...questionPlan.requestedShape.requiredOutputs,
5092
+ ].map((term) => term.toLowerCase().replace(/[_-]+/g, ' ').trim()));
5093
+ const hasAttribute = Array.from(attributeTerms).some((term) => {
5094
+ if (!term)
5095
+ return false;
5096
+ return ATTRIBUTE_LOOKUP_CONCEPTS.has(term) || Array.from(ATTRIBUTE_LOOKUP_CONCEPTS).some((concept) => new RegExp(`\\b${concept}\\b`, 'i').test(term));
5097
+ }) || /\b(?:region|segment|category|channel|location|country|state|city|status|type|name|label|title|description)\b/i.test(lower);
5098
+ if (!hasAttribute)
5099
+ return false;
5100
+ const asksForAttribute = /\b(?:what|which|where|who|show|give|tell|find|list)\b/i.test(lower)
5101
+ || /\b(?:belongs?|assigned|associated|located|based|part\s+of)\b/i.test(lower);
5102
+ if (!asksForAttribute)
5103
+ return false;
5104
+ const hasTarget = Boolean(followUp?.memberBindings?.length)
5105
+ || questionPlan.entities.length > 0
5106
+ || /\b(?:he|she|him|his|her|hers|this|that|these|those|same|above|previous|prior)\b/i.test(lower);
5107
+ return hasTarget;
5108
+ }
5109
+ function renderAttributeLookupInstruction(questionPlan, followUp) {
5110
+ const requested = Array.from(new Set([
5111
+ ...questionPlan.dimensionTerms,
5112
+ ...questionPlan.requestedShape.dimensions,
5113
+ ...questionPlan.requestedShape.requiredOutputs,
5114
+ ].filter((term) => ATTRIBUTE_LOOKUP_CONCEPTS.has(term.toLowerCase().replace(/[_-]+/g, ' ').trim()))));
5115
+ const bindings = followUp?.memberBindings?.map((binding) => `${binding.dimension} = ${binding.values.join(' | ')}`).join('; ');
5116
+ return [
5117
+ 'METRIC-INDEPENDENT ATTRIBUTE LOOKUP (authoritative for this turn):',
5118
+ 'This is a valid lookup of an entity attribute; it does not require a revenue, count, or other measure.',
5119
+ requested.length > 0 ? `Return the requested attribute column(s): ${requested.join(', ')}.` : 'Return the requested descriptive attribute column.',
5120
+ bindings ? `Preserve the typed conversation member binding exactly: ${bindings}.` : 'Preserve the explicitly named entity/member exactly.',
5121
+ 'Use the inspected metadata and documented join keys to find the owning relation, select the attribute, and apply the member predicate. Do not invent a metric, add an arbitrary aggregate, or broaden a singular member to all prior rows.',
5122
+ 'If the attribute or relationship is absent from inspected metadata, return no SQL and state the precise missing relation/column so the host can surface a typed gap.',
5123
+ ].join('\n');
5124
+ }
4380
5125
  function buildKgJoinPathHints(kg, nodes, questionPlan) {
4381
5126
  const candidates = nodes
4382
5127
  .filter((node) => ['block', 'business_view', 'metric', 'entity', 'semantic_model', 'dbt_model', 'dbt_source'].includes(node.kind))
@@ -6405,6 +7150,7 @@ async function generateProposalWithOptionalTools(input) {
6405
7150
  ...options,
6406
7151
  toolPolicy,
6407
7152
  maxToolCalls: toolBudget.maxToolCalls,
7153
+ providerPayloadGuard: input.providerPayloadGuard,
6408
7154
  onToolCall: (event) => {
6409
7155
  const sink = input.toolCalls;
6410
7156
  if (sink)
@@ -6502,22 +7248,9 @@ async function scoreDeepGeneratedProposalCandidate(input, candidate) {
6502
7248
  validationOk = validation.ok;
6503
7249
  if (validation.ok) {
6504
7250
  score += 100;
6505
- if (input.executeGeneratedSql) {
6506
- try {
6507
- result = await input.executeGeneratedSql(parsed.sql);
6508
- score += 40;
6509
- if (result.rowCount > 0)
6510
- score += 8;
6511
- const resultShape = validateAnswerResultShape(input.questionPlan, result);
6512
- score -= resultShape.warnings.length * 6;
6513
- if (generatedResultShapeIsPartial(resultShape))
6514
- score -= 120;
6515
- }
6516
- catch (error) {
6517
- executionError = error instanceof Error ? error.message : String(error);
6518
- score -= 20;
6519
- }
6520
- }
7251
+ // Budget authority: candidate comparison is parser/context validation
7252
+ // only. Executing each candidate would violate the one-SQL-call ordinary
7253
+ // Ask budget and make telemetry dependent on speculative work.
6521
7254
  }
6522
7255
  else {
6523
7256
  validationError = validation.error;