@duckcodeailabs/dql-agent 1.14.2 → 1.14.4

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 (196) hide show
  1. package/dist/agent-run-engine.d.ts +239 -4
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +2924 -85
  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 +51 -3
  7. package/dist/agent-run-gates.js.map +1 -1
  8. package/dist/agent-run-planner.d.ts +6 -0
  9. package/dist/agent-run-planner.d.ts.map +1 -1
  10. package/dist/agent-run-planner.js +2 -0
  11. package/dist/agent-run-planner.js.map +1 -1
  12. package/dist/agent-run-store.d.ts.map +1 -1
  13. package/dist/agent-run-store.js +591 -11
  14. package/dist/agent-run-store.js.map +1 -1
  15. package/dist/agentic/analyst-loop.d.ts +9 -1
  16. package/dist/agentic/analyst-loop.d.ts.map +1 -1
  17. package/dist/agentic/analyst-loop.js +204 -4
  18. package/dist/agentic/analyst-loop.js.map +1 -1
  19. package/dist/agentic/assumptions.d.ts +1 -1
  20. package/dist/agentic/assumptions.d.ts.map +1 -1
  21. package/dist/agentic/assumptions.js.map +1 -1
  22. package/dist/agentic/ledger-tools.d.ts.map +1 -1
  23. package/dist/agentic/ledger-tools.js +8 -1
  24. package/dist/agentic/ledger-tools.js.map +1 -1
  25. package/dist/agentic/orchestrator-policy.d.ts +3 -1
  26. package/dist/agentic/orchestrator-policy.d.ts.map +1 -1
  27. package/dist/agentic/orchestrator-policy.js +1 -1
  28. package/dist/agentic/orchestrator-policy.js.map +1 -1
  29. package/dist/agentic/research-agent.d.ts.map +1 -1
  30. package/dist/agentic/research-agent.js +10 -1
  31. package/dist/agentic/research-agent.js.map +1 -1
  32. package/dist/agentic/sql-authorization.d.ts +8 -0
  33. package/dist/agentic/sql-authorization.d.ts.map +1 -1
  34. package/dist/agentic/sql-authorization.js +3 -0
  35. package/dist/agentic/sql-authorization.js.map +1 -1
  36. package/dist/agentic/tool-loop.d.ts +13 -1
  37. package/dist/agentic/tool-loop.d.ts.map +1 -1
  38. package/dist/agentic/tool-loop.js +441 -32
  39. package/dist/agentic/tool-loop.js.map +1 -1
  40. package/dist/analytical-compatibility.d.ts.map +1 -1
  41. package/dist/analytical-compatibility.js +8 -1
  42. package/dist/analytical-compatibility.js.map +1 -1
  43. package/dist/analytical-failure-repair.d.ts.map +1 -1
  44. package/dist/analytical-failure-repair.js +6 -2
  45. package/dist/analytical-failure-repair.js.map +1 -1
  46. package/dist/analytical-frame.d.ts +42 -1
  47. package/dist/analytical-frame.d.ts.map +1 -1
  48. package/dist/analytical-frame.js +490 -26
  49. package/dist/analytical-frame.js.map +1 -1
  50. package/dist/analytical-orchestration.d.ts +1259 -9
  51. package/dist/analytical-orchestration.d.ts.map +1 -1
  52. package/dist/analytical-orchestration.js +970 -84
  53. package/dist/analytical-orchestration.js.map +1 -1
  54. package/dist/answer-loop.d.ts +230 -10
  55. package/dist/answer-loop.d.ts.map +1 -1
  56. package/dist/answer-loop.js +1182 -85
  57. package/dist/answer-loop.js.map +1 -1
  58. package/dist/answer-shape.d.ts +27 -0
  59. package/dist/answer-shape.d.ts.map +1 -1
  60. package/dist/answer-shape.js +55 -0
  61. package/dist/answer-shape.js.map +1 -1
  62. package/dist/ask-observability/index.d.ts +11 -0
  63. package/dist/ask-observability/index.d.ts.map +1 -0
  64. package/dist/ask-observability/index.js +7 -0
  65. package/dist/ask-observability/index.js.map +1 -0
  66. package/dist/ask-observability/instrumentation.d.ts +35 -0
  67. package/dist/ask-observability/instrumentation.d.ts.map +1 -0
  68. package/dist/ask-observability/instrumentation.js +523 -0
  69. package/dist/ask-observability/instrumentation.js.map +1 -0
  70. package/dist/ask-observability/observer.d.ts +62 -0
  71. package/dist/ask-observability/observer.d.ts.map +1 -0
  72. package/dist/ask-observability/observer.js +537 -0
  73. package/dist/ask-observability/observer.js.map +1 -0
  74. package/dist/ask-observability/portable.d.ts +98 -0
  75. package/dist/ask-observability/portable.d.ts.map +1 -0
  76. package/dist/ask-observability/portable.js +1089 -0
  77. package/dist/ask-observability/portable.js.map +1 -0
  78. package/dist/ask-observability/store.d.ts +130 -0
  79. package/dist/ask-observability/store.d.ts.map +1 -0
  80. package/dist/ask-observability/store.js +1114 -0
  81. package/dist/ask-observability/store.js.map +1 -0
  82. package/dist/ask-observability/types.d.ts +380 -0
  83. package/dist/ask-observability/types.d.ts.map +1 -0
  84. package/dist/ask-observability/types.js +2 -0
  85. package/dist/ask-observability/types.js.map +1 -0
  86. package/dist/ask-observability/utils.d.ts +21 -0
  87. package/dist/ask-observability/utils.d.ts.map +1 -0
  88. package/dist/ask-observability/utils.js +102 -0
  89. package/dist/ask-observability/utils.js.map +1 -0
  90. package/dist/ask-runtime/ask-agent-runtime-v2.d.ts +850 -0
  91. package/dist/ask-runtime/ask-agent-runtime-v2.d.ts.map +1 -0
  92. package/dist/ask-runtime/ask-agent-runtime-v2.js +1716 -0
  93. package/dist/ask-runtime/ask-agent-runtime-v2.js.map +1 -0
  94. package/dist/ask-runtime/ask-analyst-runtime.d.ts +95 -0
  95. package/dist/ask-runtime/ask-analyst-runtime.d.ts.map +1 -0
  96. package/dist/ask-runtime/ask-analyst-runtime.js +8280 -0
  97. package/dist/ask-runtime/ask-analyst-runtime.js.map +1 -0
  98. package/dist/conversation/session-store.d.ts +31 -0
  99. package/dist/conversation/session-store.d.ts.map +1 -1
  100. package/dist/conversation/session-store.js +38 -0
  101. package/dist/conversation/session-store.js.map +1 -1
  102. package/dist/conversation/snapshot.d.ts +4 -1
  103. package/dist/conversation/snapshot.d.ts.map +1 -1
  104. package/dist/conversation/snapshot.js +85 -3
  105. package/dist/conversation/snapshot.js.map +1 -1
  106. package/dist/conversation/turn-trust.d.ts.map +1 -1
  107. package/dist/conversation/turn-trust.js +2 -0
  108. package/dist/conversation/turn-trust.js.map +1 -1
  109. package/dist/embeddings/provider.d.ts +1 -0
  110. package/dist/embeddings/provider.d.ts.map +1 -1
  111. package/dist/embeddings/provider.js +18 -0
  112. package/dist/embeddings/provider.js.map +1 -1
  113. package/dist/generated-analytical-proposal.d.ts +37 -1
  114. package/dist/generated-analytical-proposal.d.ts.map +1 -1
  115. package/dist/generated-analytical-proposal.js +246 -3
  116. package/dist/generated-analytical-proposal.js.map +1 -1
  117. package/dist/governed-relational-compiler.d.ts +15 -0
  118. package/dist/governed-relational-compiler.d.ts.map +1 -1
  119. package/dist/governed-relational-compiler.js +99 -1
  120. package/dist/governed-relational-compiler.js.map +1 -1
  121. package/dist/index.d.ts +21 -12
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +14 -7
  124. package/dist/index.js.map +1 -1
  125. package/dist/intent-controller.d.ts +87 -1
  126. package/dist/intent-controller.d.ts.map +1 -1
  127. package/dist/intent-controller.js +2 -2
  128. package/dist/intent-controller.js.map +1 -1
  129. package/dist/kg/build.js +32 -2
  130. package/dist/kg/build.js.map +1 -1
  131. package/dist/meaning-resolution.d.ts +207 -6
  132. package/dist/meaning-resolution.d.ts.map +1 -1
  133. package/dist/meaning-resolution.js +439 -53
  134. package/dist/meaning-resolution.js.map +1 -1
  135. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  136. package/dist/metadata/analysis-planner.js +78 -7
  137. package/dist/metadata/analysis-planner.js.map +1 -1
  138. package/dist/metadata/catalog.d.ts +14 -0
  139. package/dist/metadata/catalog.d.ts.map +1 -1
  140. package/dist/metadata/catalog.js +29 -2
  141. package/dist/metadata/catalog.js.map +1 -1
  142. package/dist/metadata/meaning-evidence.d.ts +28 -2
  143. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  144. package/dist/metadata/meaning-evidence.js +373 -16
  145. package/dist/metadata/meaning-evidence.js.map +1 -1
  146. package/dist/plan-execution-adapter.d.ts +9 -1
  147. package/dist/plan-execution-adapter.d.ts.map +1 -1
  148. package/dist/plan-execution-adapter.js +93 -13
  149. package/dist/plan-execution-adapter.js.map +1 -1
  150. package/dist/provider-egress.d.ts +28 -11
  151. package/dist/provider-egress.d.ts.map +1 -1
  152. package/dist/provider-egress.js +72 -22
  153. package/dist/provider-egress.js.map +1 -1
  154. package/dist/providers/claude.d.ts +2 -2
  155. package/dist/providers/claude.d.ts.map +1 -1
  156. package/dist/providers/claude.js +201 -37
  157. package/dist/providers/claude.js.map +1 -1
  158. package/dist/providers/dispatch.d.ts +27 -1
  159. package/dist/providers/dispatch.d.ts.map +1 -1
  160. package/dist/providers/dispatch.js +63 -1
  161. package/dist/providers/dispatch.js.map +1 -1
  162. package/dist/providers/gemini.js +10 -10
  163. package/dist/providers/gemini.js.map +1 -1
  164. package/dist/providers/index.d.ts +2 -2
  165. package/dist/providers/index.d.ts.map +1 -1
  166. package/dist/providers/index.js +1 -1
  167. package/dist/providers/index.js.map +1 -1
  168. package/dist/providers/ollama.js +15 -15
  169. package/dist/providers/ollama.js.map +1 -1
  170. package/dist/providers/openai.d.ts +2 -2
  171. package/dist/providers/openai.d.ts.map +1 -1
  172. package/dist/providers/openai.js +213 -43
  173. package/dist/providers/openai.js.map +1 -1
  174. package/dist/providers/types.d.ts +131 -1
  175. package/dist/providers/types.d.ts.map +1 -1
  176. package/dist/providers/types.js.map +1 -1
  177. package/dist/relationship-proof.d.ts +81 -0
  178. package/dist/relationship-proof.d.ts.map +1 -1
  179. package/dist/relationship-proof.js +228 -4
  180. package/dist/relationship-proof.js.map +1 -1
  181. package/dist/requirement-clauses.d.ts +23 -0
  182. package/dist/requirement-clauses.d.ts.map +1 -0
  183. package/dist/requirement-clauses.js +70 -0
  184. package/dist/requirement-clauses.js.map +1 -0
  185. package/dist/resolved-analytical-plan.d.ts +18 -4
  186. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  187. package/dist/resolved-analytical-plan.js +953 -50
  188. package/dist/resolved-analytical-plan.js.map +1 -1
  189. package/dist/router.d.ts +118 -2
  190. package/dist/router.d.ts.map +1 -1
  191. package/dist/router.js +2481 -232
  192. package/dist/router.js.map +1 -1
  193. package/dist/semantic-bridge/compose.d.ts.map +1 -1
  194. package/dist/semantic-bridge/compose.js +23 -4
  195. package/dist/semantic-bridge/compose.js.map +1 -1
  196. package/package.json +4 -4
@@ -8,21 +8,69 @@
8
8
  */
9
9
  import { createHash } from "node:crypto";
10
10
  import { normalizeMetricCapabilityContract, } from "@duckcodeailabs/dql-core";
11
- import { certifiedCandidateDeclaredMeasureOutput, certifiedCandidateExplicitlyCoversMeasures, } from './meaning-resolution.js';
12
- import { buildResolvedRelationshipProofsV1 } from './relationship-proof.js';
11
+ import { certifiedCandidateDeclaredDimensionOutput, certifiedCandidateDeclaredMeasureOutput, certifiedCandidateExplicitlyCoversMeasures, certifiedCandidateGrainDimensionOutputs, } from './meaning-resolution.js';
12
+ import { currentQuestionGroundedParsedIntent } from './analytical-orchestration.js';
13
+ import { buildResolvedRelationshipProofsV1, buildGovernedRelationshipSafetyProofsV1, governedCapabilityDimensionHasFreshAutomaticRelationshipProofV1, semanticDimensionUsesExactAdapterGrouping, } from './relationship-proof.js';
14
+ import { proveSameSnapshotMetricflowRoleExtensionV1 } from './analytical-frame.js';
13
15
  export function buildResolvedAnalyticalPlan(input) {
14
- const byLegacyId = new Map(input.candidates.map((candidate) => [candidate.id, candidate]));
16
+ // A same-snapshot MetricFlow role extension deliberately retains the
17
+ // source card's legacy ID so older persisted selections remain readable,
18
+ // while carrying the capability-qualified dimension ID that the frozen
19
+ // plan must compile. Do not let a later, short-alias copy of that card win
20
+ // the legacy-ID map: doing so reopens a lexically similar but unproven
21
+ // dimension after the runtime already verified the exact grouping field.
22
+ // This is narrowly scoped to an extension whose qualified ID is present in
23
+ // the resolved immutable frame; ordinary duplicate retrieval cards retain
24
+ // the historical last-card behavior.
25
+ const frameDimensionIds = new Set(input.resolution.analyticalFrame?.dimensions.map((binding) => binding.dimensionId) ?? []);
26
+ const isFrameQualifiedSemanticExtension = (candidate) => Boolean(candidate?.kind === 'semantic_member'
27
+ && candidate.sameSnapshotRoleExtension
28
+ && candidate.qualifiedId
29
+ && frameDimensionIds.has(candidate.qualifiedId));
30
+ const byLegacyId = new Map();
31
+ for (const candidate of input.candidates) {
32
+ const existing = byLegacyId.get(candidate.id);
33
+ if (existing && isFrameQualifiedSemanticExtension(existing) && !isFrameQualifiedSemanticExtension(candidate)) {
34
+ continue;
35
+ }
36
+ byLegacyId.set(candidate.id, candidate);
37
+ }
15
38
  const selectedCandidates = input.resolution.selectedConceptIds
16
- .flatMap((id) => byLegacyId.get(id) ? [byLegacyId.get(id)] : []);
39
+ .flatMap((id) => {
40
+ // V3 programs retain a stable qualified field identity while the legacy
41
+ // carrier normally stores the local candidate ID. Resolve only an
42
+ // exact local identity first, then one exact qualified identity. This
43
+ // preserves a host-bound physical filter field such as
44
+ // `dbt:column:locations.location_name` / `locations.location_name`
45
+ // through the compiler bridge without treating an alias or leaf name as
46
+ // authority over a different relation.
47
+ const local = byLegacyId.get(id);
48
+ if (local)
49
+ return [local];
50
+ const qualified = input.candidates.filter((candidate) => candidate.qualifiedId === id);
51
+ return qualified.length === 1 ? qualified : [];
52
+ });
17
53
  const executionCandidate = input.resolution.recommendedExecutionId
18
- ? byLegacyId.get(input.resolution.recommendedExecutionId)
54
+ // V3 programs carry the canonical execution authority, while legacy
55
+ // selected concepts retain the local index ID for backwards-readable
56
+ // receipts. A certified block commonly has both (`dql:block:*` and
57
+ // `domain::block::*`). Resolve either exact identity here; otherwise the
58
+ // compiler carrier loses the selected execution authority and rebinds a
59
+ // complete certified tuple through unrelated semantic cards.
60
+ ? input.candidates.find((candidate) => candidate.id === input.resolution.recommendedExecutionId
61
+ || candidate.qualifiedId === input.resolution.recommendedExecutionId)
19
62
  : selectedCandidates[0];
20
63
  const bindingCandidates = input.resolution.recommendedRoute === 'certified' && executionCandidate
21
64
  // A deterministically compatible certified block has already proved the
22
65
  // complete tuple. Bind its declared inputs/outputs as one execution
23
66
  // authority instead of mixing in the semantic concept used to select it.
24
67
  ? [executionCandidate]
25
- : resolutionUsesRelationalEvidence(input.resolution)
68
+ // A governed-relational route may bind through its certified relationship
69
+ // closure. Exploratory is different: the router has already selected a
70
+ // minimal qualified physical closure before plan freeze. Re-opening the
71
+ // whole snapshot here lets unrelated columns re-enter an exact output
72
+ // contract and turns `order_id`/`product_id` into false ambiguities.
73
+ : input.resolution.recommendedRoute === 'governed_sql'
26
74
  ? input.candidates
27
75
  : selectedCandidates.length > 0
28
76
  ? selectedCandidates
@@ -30,7 +78,6 @@ export function buildResolvedAnalyticalPlan(input) {
30
78
  ? [executionCandidate]
31
79
  : input.candidates;
32
80
  const canonicalId = (candidate) => candidate.qualifiedId ?? candidate.id;
33
- const selectedConceptIds = selectedCandidates.map(canonicalId);
34
81
  const executionId = executionCandidate ? canonicalId(executionCandidate) : undefined;
35
82
  const measures = input.resolution.queryIntent.measures.length > 0
36
83
  ? input.resolution.queryIntent.measures.map((requested) => bindRequestedMember(requested, bindingCandidates, 'measure', input.question))
@@ -50,7 +97,173 @@ export function buildResolvedAnalyticalPlan(input) {
50
97
  ? input.candidates
51
98
  : bindingCandidates;
52
99
  const selectedCapability = normalizeMetricCapabilityContract(executionCandidate?.analyticalCapability);
100
+ const selectedCertifiedBlock = input.resolution.recommendedRoute === 'certified'
101
+ && executionCandidate?.kind === 'certified_block'
102
+ && executionCandidate.compatibility === 'compatible'
103
+ ? executionCandidate
104
+ : undefined;
105
+ // The direct router may canonicalize a literal request (for example,
106
+ // “food and drink”) to an exact block's declared output (`category`) after
107
+ // that exact block independently proved the tuple. Keep that narrow
108
+ // zero-provider projection distinct from the ordinary host requirement
109
+ // contract below; a model-selected block can never create it.
110
+ const exactCertifiedDimensionProjection = certifiedBlockCanUseExactCanonicalDimensionProjection(executionCandidate, input.resolution);
111
+ const hostEntityDisplayTerms = new Set(input.resolution.hostRequirementSeed?.requirements.entityDisplayTerms
112
+ .map(normalize)
113
+ .filter(Boolean) ?? []);
114
+ // A certified route is not a semantic capability route with a green badge.
115
+ // Every host-owned grouping/display/output term must be declared by the
116
+ // selected block itself before that block can freeze. The optional normalized
117
+ // capability can corroborate the selected block, but it may not substitute a
118
+ // contextual semantic field for a block output the block never promised.
119
+ const hostCertifiedProjectionTerms = new Set([
120
+ ...(input.resolution.hostRequirementSeed?.queryIntent.dimensions ?? []),
121
+ // The host requirement seed retains the business entity separately from
122
+ // its display label. For a block that has already passed the exact-tuple
123
+ // gate, both are projections of the block's own declared output (for
124
+ // example `customer` -> `customer_name`). Leaving the entity out here
125
+ // re-bound it through a generic semantic card during the compatibility
126
+ // carrier handoff, so a proven certified customer profile became blocked
127
+ // after its zero-call fast path had selected it.
128
+ ...(input.resolution.hostRequirementSeed?.requirements.entityTerms ?? []),
129
+ ...(input.resolution.hostRequirementSeed?.requirements.entityDisplayTerms ?? []),
130
+ ...(input.resolution.hostRequirementSeed?.requirements.outputTerms ?? []),
131
+ ].map(normalize).filter(Boolean));
132
+ const bindCertifiedDeclaredDimension = (requested) => {
133
+ const outputName = selectedCertifiedBlock
134
+ ? certifiedCandidateDeclaredDimensionOutput(selectedCertifiedBlock, requested)
135
+ : undefined;
136
+ if (!selectedCertifiedBlock || !outputName) {
137
+ return { requested, status: 'unresolved', candidateIds: [] };
138
+ }
139
+ const candidateId = canonicalId(selectedCertifiedBlock);
140
+ return {
141
+ requested,
142
+ qualifiedId: candidateId,
143
+ outputName,
144
+ status: 'resolved',
145
+ candidateIds: [candidateId],
146
+ };
147
+ };
148
+ const bindCertifiedDeclaredOutput = (requested) => {
149
+ const outputName = selectedCertifiedBlock
150
+ ? certifiedCandidateDeclaredDimensionOutput(selectedCertifiedBlock, requested)
151
+ ?? certifiedCandidateDeclaredMeasureOutput(selectedCertifiedBlock, requested)
152
+ : undefined;
153
+ if (!selectedCertifiedBlock || !outputName) {
154
+ return { requested, status: 'unresolved', candidateIds: [] };
155
+ }
156
+ const candidateId = canonicalId(selectedCertifiedBlock);
157
+ return {
158
+ requested,
159
+ qualifiedId: candidateId,
160
+ outputName,
161
+ status: 'resolved',
162
+ candidateIds: [candidateId],
163
+ };
164
+ };
53
165
  const bindDimension = (requested, roles) => {
166
+ const normalizedRequested = normalize(requested);
167
+ if (selectedCertifiedBlock && hostCertifiedProjectionTerms.has(normalizedRequested)) {
168
+ return bindCertifiedDeclaredDimension(requested);
169
+ }
170
+ // A V2 frame is already the verified, snapshot-bound result of the
171
+ // planner role bindings. Re-scoring its selected dimensions against
172
+ // every capability field can swap two valid roles when an entity label
173
+ // and a categorical grouping coexist (for example Customer Name plus
174
+ // Product Category). Prefer an exact qualified frame binding when the
175
+ // request names that field or a same-snapshot extension explicitly maps
176
+ // the business term to it. This is an identity-preserving projection,
177
+ // not a lexical fallback: the frame ID must be an eligible dimension of
178
+ // the selected capability and the extension must carry its original
179
+ // metric/dimension proof.
180
+ const frameOwnedDimensionIds = uniqueSorted((input.resolution.analyticalFrame?.dimensions ?? [])
181
+ .filter((binding) => capabilityEntailsFrameDimension(selectedCapability, binding))
182
+ .map((binding) => binding.dimensionId));
183
+ const exactFrameIds = frameOwnedDimensionIds.filter((dimensionId) => {
184
+ const dimension = selectedCapability?.dimensions.find((item) => item.dimensionId === dimensionId);
185
+ if (!dimension || !roles.every((role) => dimension.supportedRoles.includes(role)))
186
+ return false;
187
+ return frameDimensionMatchesRequestedBinding({
188
+ requested: normalizedRequested,
189
+ dimension,
190
+ candidates: input.candidates,
191
+ });
192
+ });
193
+ if (exactFrameIds.length === 1) {
194
+ return {
195
+ requested,
196
+ qualifiedId: exactFrameIds[0],
197
+ status: 'resolved',
198
+ candidateIds: exactFrameIds,
199
+ };
200
+ }
201
+ // The candidate-ID meaning protocol cannot author a frame, but the host
202
+ // has already bound a V2 frame from the immutable requirement seed. A
203
+ // seeded rank/display requirement may use the human phrase `customer
204
+ // name` while the selected MetricFlow capability truthfully names the
205
+ // native field simply `customer`. Re-running that field through generic
206
+ // lexical scoring loses the authoritative host binding and makes an
207
+ // executable legacy capability look unmodeled. Reuse only the exact host
208
+ // frame rank entity, and only after the selected capability proves every
209
+ // required role. This is not a name-based fallback and cannot select
210
+ // Customer Type, Customer Owner, or a cross-model same-leaf field.
211
+ const hostRankEntityDimensionId = hostEntityDisplayTerms.has(normalizedRequested)
212
+ ? input.resolution.analyticalFrame?.ranking?.entityDimensionId
213
+ : undefined;
214
+ const hostRankEntityDimension = hostRankEntityDimensionId
215
+ ? selectedCapability?.dimensions.find((dimension) => dimension.dimensionId === hostRankEntityDimensionId)
216
+ : undefined;
217
+ if (hostRankEntityDimension && roles.every((role) => hostRankEntityDimension.supportedRoles.includes(role))) {
218
+ return {
219
+ requested,
220
+ qualifiedId: hostRankEntityDimension.dimensionId,
221
+ status: 'resolved',
222
+ candidateIds: [hostRankEntityDimension.dimensionId],
223
+ };
224
+ }
225
+ // The host-built V2 frame may intentionally retain a genuine native
226
+ // display-key ambiguity (for example Billing Account versus Service
227
+ // Account). Keep that exact, metric-owned choice set when RAP binds the
228
+ // same seeded phrase. Re-running it through lexical candidate matching
229
+ // would discard the qualified alternatives and turn a useful
230
+ // clarification into a false “unresolved” gap. The frame is already
231
+ // snapshot-bound; still require the selected capability to prove every
232
+ // requested role before carrying a choice forward.
233
+ const sourceAmbiguity = input.resolution.analyticalFrame?.ambiguity.find((entry) => {
234
+ const [lane, ...parts] = entry.field.split('.');
235
+ return lane === 'dimensions' && normalize(parts.join('.')) === normalizedRequested;
236
+ });
237
+ const sourceChoices = uniqueSorted((sourceAmbiguity?.candidateIds ?? []).filter((candidateId) => eligibleCapabilityDimensions(selectedCapability, roles)
238
+ .some((dimension) => dimension.dimensionId === candidateId)));
239
+ if (sourceChoices.length === 1) {
240
+ return {
241
+ requested,
242
+ qualifiedId: sourceChoices[0],
243
+ status: 'resolved',
244
+ candidateIds: sourceChoices,
245
+ };
246
+ }
247
+ if (sourceChoices.length > 1) {
248
+ return {
249
+ requested,
250
+ status: 'ambiguous',
251
+ candidateIds: sourceChoices,
252
+ };
253
+ }
254
+ // An exact certified block is its own execution authority. Its declared
255
+ // output contract may use the entity identity (`customer`) where the
256
+ // host-owned ranking seed deliberately retains the display phrase
257
+ // (`customer name`). Do not let the optional normalized semantic
258
+ // capability re-run that proven block output through lexical capability
259
+ // matching: the capability is context for the block, while the block's
260
+ // declared output is the certified proof. This remains narrow to the
261
+ // selected compatible block and cannot bind an unrelated retrieved field.
262
+ const certifiedOutput = selectedCertifiedBlock
263
+ ? certifiedCandidateDeclaredDimensionOutput(selectedCertifiedBlock, requested)
264
+ : undefined;
265
+ if (certifiedOutput && selectedCertifiedBlock)
266
+ return bindCertifiedDeclaredDimension(requested);
54
267
  const capabilityBinding = bindSelectedCapabilityDimension(requested, selectedCapability, roles, input.candidates);
55
268
  // A provider frame may describe a selected capability, but it cannot pick
56
269
  // one of that capability's grouping dimensions for the host. Once a
@@ -66,10 +279,8 @@ export function buildResolvedAnalyticalPlan(input) {
66
279
  }
67
280
  if (capabilityBinding && capabilityBinding.status !== 'unresolved')
68
281
  return capabilityBinding;
69
- const normalizedRequested = normalize(requested);
70
282
  const frameIds = uniqueSorted((input.resolution.analyticalFrame?.dimensions ?? [])
71
283
  .filter((binding) => capabilityEntailsFrameDimension(selectedCapability, binding)
72
- && (!selectedCapability || binding.role === 'time_axis')
73
284
  && memberTermMatches(normalize(binding.dimensionId), normalizedRequested))
74
285
  .map((binding) => binding.dimensionId));
75
286
  if (frameIds.length === 1) {
@@ -87,8 +298,68 @@ export function buildResolvedAnalyticalPlan(input) {
87
298
  const rankingRequested = input.resolution.questionType === 'ranking'
88
299
  || input.resolution.queryIntent.order !== undefined
89
300
  || input.resolution.queryIntent.limit !== undefined;
90
- const dimensions = input.resolution.queryIntent.dimensions
91
- .map((requested) => bindDimension(requested, rankingRequested ? ['group_by', 'rank_entity'] : ['group_by']));
301
+ // A ranked report can group by several dimensions, but exactly one of them
302
+ // is the entity being ranked. Requiring `rank_entity` on every grouping
303
+ // field made a complete tuple such as "top customers by product category"
304
+ // clarify because `product_type` correctly supports grouping but not being
305
+ // the ranked customer entity.
306
+ const rankEntityDimensionId = input.resolution.analyticalFrame?.ranking?.entityDimensionId;
307
+ const requiredDimensionRoles = (requested) => {
308
+ if (!rankingRequested)
309
+ return ['group_by'];
310
+ const rankEntity = selectedCapability?.dimensions.find((dimension) => dimension.dimensionId === rankEntityDimensionId);
311
+ const matchesHostEntityDisplay = hostEntityDisplayTerms.has(normalize(requested))
312
+ && Boolean(rankEntity && rankEntity.dimensionId === rankEntityDimensionId);
313
+ const matchesRankEntity = matchesHostEntityDisplay || Boolean(rankEntity && selectedCapability
314
+ && capabilityDimensionMatchScore(requested, rankEntity, selectedCapability, input.candidates) > 0);
315
+ return matchesRankEntity ? ['group_by', 'rank_entity'] : ['group_by'];
316
+ };
317
+ // The requirement seed is the immutable, host-owned tuple for a bounded
318
+ // meaning call. Route reconciliation may still carry a legacy/broad
319
+ // `queryIntent` alongside that seed for compatibility, but it must never
320
+ // reintroduce an entity noun that the seed deliberately separated from its
321
+ // display/rank requirement. For example, `top customers by product
322
+ // category` binds the host terms `customer name` and `product category`;
323
+ // a legacy `customer` dimension would make Customer Type and Customer Order
324
+ // Number compete with the metric-native Customer Name key after the frame
325
+ // had already resolved it. Structured selections are merged into the
326
+ // seed before this point, so the seeded dimensions remain complete.
327
+ //
328
+ // An exact authored certified asset is a zero-provider host decision. Its
329
+ // direct router canonicalizes literal value wording (for example, “food and
330
+ // drink”) to the block's declared output (`category`) only after that exact
331
+ // block itself proved the requested measures and output contract. A model
332
+ // cannot reach this branch: candidate-ID meaning merge restores the host
333
+ // seed before RAP construction. Preserve this narrow canonical projection
334
+ // instead of turning a certified answer back into two invented dimensions.
335
+ // Unseeded V1/legacy callers retain their historical query-intent path.
336
+ const seededDimensionTerms = exactCertifiedDimensionProjection
337
+ ? input.resolution.queryIntent.dimensions
338
+ : input.resolution.hostRequirementSeed
339
+ ? input.resolution.hostRequirementSeed.queryIntent.dimensions
340
+ : input.resolution.queryIntent.dimensions;
341
+ // A server-issued clarification choice is a typed continuation binding. It
342
+ // may complete a display/grouping role that did not occur literally in the
343
+ // source question (for example “Show the top names by revenue” → Customer
344
+ // Name), but only after the router revalidated the stable choice against
345
+ // this snapshot. Candidate-ID model output cannot use this carrier.
346
+ const authoritativeDimensionTerms = [...new Set([
347
+ ...seededDimensionTerms,
348
+ ...(input.resolution.structuredDimensionIds ?? []),
349
+ ])];
350
+ const rawDimensions = authoritativeDimensionTerms
351
+ .map((requested) => bindDimension(requested, requiredDimensionRoles(requested)));
352
+ // The host-owned seed retains an entity display term (for example, "customer
353
+ // name") so a model cannot quietly turn "top customers" into an anonymous
354
+ // entity id. Some authored semantic/certified capabilities deliberately
355
+ // expose that display role under the generic entity identity ("customer").
356
+ // In that case the label is not a second requested grouping dimension. Keep
357
+ // it in the immutable seed/receipt, but remove only a redundant execution
358
+ // binding when the same resolved entity proves it, or an unresolved label
359
+ // when a resolved generic entity already covers the role. A real explicit
360
+ // `by customer name` dimension remains untouched because it appears in the
361
+ // seed's ordinary dimensions as well as its display terms.
362
+ const dimensions = collapseHostEntityDisplayBindings(rawDimensions, input.resolution.hostRequirementSeed);
92
363
  // A provider may name the field it is grouping by and forget the MEMBER the
93
364
  // reader asked about: "What customer type is Wesley Jenkins?" came back with
94
365
  // `filters: []`, so the run returned all 200 customers and the narration
@@ -100,18 +371,52 @@ export function buildResolvedAnalyticalPlan(input) {
100
371
  // still win whenever it supplies any.
101
372
  const requestedFilters = input.resolution.queryIntent.filters.length > 0
102
373
  ? input.resolution.queryIntent.filters
103
- : (input.evidence.parsedIntent?.filters ?? []).filter((filter) => filter.field && filter.value);
374
+ : (input.resolution.hostRequirementSeed?.queryIntent.filters
375
+ ?? currentQuestionGroundedParsedIntent(input.question, input.evidence.parsedIntent)?.filters
376
+ ?? []).filter((filter) => filter.field && filter.value);
104
377
  const filters = requestedFilters.map((filter) => ({
105
378
  ...filter,
106
379
  binding: bindDimension(filter.field, ['filter']),
107
380
  }));
108
- const capabilityDimensionProof = resolvedCapabilityDimensionProof(selectedCapability, dimensions, filters);
109
- const proofCandidates = [
110
- ...selectedCandidates,
111
- ...(executionCandidate && !selectedCandidates.some((candidate) => candidate.id === executionCandidate.id)
381
+ // Output terms are a distinct host-owned projection. A model may choose a
382
+ // qualified binding for one only from the admitted snapshot, but it cannot
383
+ // turn explicit `order ID` / `product ID` output terms into dimensions or
384
+ // omit them from an exploratory result contract.
385
+ //
386
+ // Host-owned output bindings have to come from the qualified meaning
387
+ // closure, not from the provisional route label. A meaning response may
388
+ // still carry the legacy `governed_sql` recommendation before the cascade
389
+ // proves whether the tuple is relational or exploratory. Re-opening the
390
+ // complete snapshot in that state turns unrelated `orders.order_id` /
391
+ // `raw_items.order_id` cards into false alternatives to the explicitly
392
+ // selected `order_items.order_id` output.
393
+ //
394
+ // If no meaning candidate was selected, preserve the legacy evidence path
395
+ // so a deterministic/certified binding can still explain a genuine gap.
396
+ // Once selection exists, however, only that snapshot-bound closure can
397
+ // satisfy a user-named output. The authorizer still proves target,
398
+ // read-only, relation, and exact-expression safety before SQL dispatch.
399
+ const outputBindingCandidates = selectedCandidates.length > 0
400
+ ? selectedCandidates
401
+ : executionCandidate
112
402
  ? [executionCandidate]
113
- : []),
114
- ];
403
+ : bindingCandidates;
404
+ const requiredOutputs = (input.resolution.hostRequirementSeed?.requirements.outputTerms ?? [])
405
+ .map((requested) => selectedCertifiedBlock
406
+ ? bindCertifiedDeclaredOutput(requested)
407
+ : bindRequestedMember(requested, outputBindingCandidates, 'output', input.question));
408
+ const outputAuthorityCandidates = requiredOutputs.flatMap((binding) => {
409
+ const ids = new Set([binding.qualifiedId, ...binding.candidateIds].filter((id) => Boolean(id)));
410
+ return input.candidates.filter((candidate) => ids.has(canonicalId(candidate)) || ids.has(candidate.id));
411
+ });
412
+ const frozenAuthorityCandidates = uniqueCandidatesByCanonicalId([
413
+ ...selectedCandidates,
414
+ ...(executionCandidate ? [executionCandidate] : []),
415
+ ...outputAuthorityCandidates,
416
+ ], canonicalId);
417
+ const selectedConceptIds = frozenAuthorityCandidates.map(canonicalId);
418
+ const capabilityDimensionProof = resolvedCapabilityDimensionProof(selectedCapability, dimensions, filters);
419
+ const proofCandidates = frozenAuthorityCandidates;
115
420
  const compatibilityProof = proofCandidates.map((candidate) => ({
116
421
  candidateId: canonicalId(candidate),
117
422
  compatibility: candidate.compatibility,
@@ -122,8 +427,8 @@ export function buildResolvedAnalyticalPlan(input) {
122
427
  : []),
123
428
  ]),
124
429
  }));
125
- const capability = resolveCapability(input.resolution, executionCandidate, measures, dimensions, filters);
126
- const bindingGaps = bindingMissingInformation(measures, dimensions, filters);
430
+ const capability = resolveCapability(input.resolution, executionCandidate, measures, dimensions, filters, requiredOutputs, selectedCapability, frozenAuthorityCandidates, input.candidates);
431
+ const bindingGaps = bindingMissingInformation(measures, dimensions, filters, requiredOutputs);
127
432
  // The reader named a member that no grounding lane could bind. Answering the
128
433
  // question WITHOUT it silently changes what was asked — "what customer type
129
434
  // is Wesley Jenkins" became "list every customer" — so the plan carries it as
@@ -159,6 +464,15 @@ export function buildResolvedAnalyticalPlan(input) {
159
464
  snapshotId,
160
465
  })
161
466
  : [];
467
+ const governedRelationshipSafetyProofs = selectedCapability
468
+ && selectedExecutionCapability?.route === 'governed_sql'
469
+ ? buildGovernedRelationshipSafetyProofsV1({
470
+ capability: selectedCapability,
471
+ dimensions: capabilityDimensionProof,
472
+ candidates: input.candidates,
473
+ snapshotId,
474
+ })
475
+ : [];
162
476
  const payload = {
163
477
  schemaVersion: input.resolution.analyticalFrame
164
478
  ? 2
@@ -194,15 +508,17 @@ export function buildResolvedAnalyticalPlan(input) {
194
508
  entityGrain: selectedCapability
195
509
  ? resolvedCapabilityEntityGrain(selectedCapability, dimensions)
196
510
  : input.resolution.analyticalFrame?.entityGrainIds[0] ?? executionCandidate?.primaryEntity,
197
- sourceRelationIds: uniqueSorted(selectedCandidates.flatMap((candidate) => candidate.sourceObjects ?? [])),
511
+ sourceRelationIds: uniqueSorted(frozenAuthorityCandidates.flatMap((candidate) => candidate.sourceObjects ?? [])),
198
512
  relationshipPathIds: selectedCapability
199
513
  ? uniqueSorted(relationshipProofs.flatMap((proof) => proof.kind === 'dql_relationship_path' ? proof.relationshipPathIds : []))
200
- : uniqueSorted(selectedCandidates.flatMap((candidate) => candidate.relationshipEvidence ?? [])),
514
+ : uniqueSorted(frozenAuthorityCandidates.flatMap((candidate) => candidate.relationshipEvidence ?? [])),
201
515
  relationshipProofs,
516
+ governedRelationshipSafetyProofs,
202
517
  compatibilityProof,
203
518
  outputContract: {
204
519
  measures: uniqueSorted(measures.flatMap((binding) => binding.outputName ? [binding.outputName] : binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
205
- dimensions: uniqueSorted(dimensions.flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
520
+ dimensions: uniqueSorted(dimensions.flatMap((binding) => binding.outputName ? [binding.outputName] : binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
521
+ requiredOutputs: requiredOutputs.map(cloneBinding),
206
522
  ...(input.resolution.queryIntent.timeGrain
207
523
  ? { timeGrain: input.resolution.queryIntent.timeGrain }
208
524
  : {}),
@@ -226,23 +542,23 @@ export function buildResolvedAnalyticalPlan(input) {
226
542
  outcome: input.resolution.compatibilityOutcome
227
543
  ?? (input.resolution.recommendedRoute === 'clarify' || input.resolution.confidence === 'low'
228
544
  ? 'clarify'
229
- : bindingCandidateIds(measures, dimensions, filters).length > 0
545
+ : bindingCandidateIds(measures, dimensions, filters, requiredOutputs).length > 0
230
546
  ? 'clarify'
231
547
  : 'modeling_gap'),
232
548
  codes: uniqueSorted([
233
549
  ...(input.resolution.compatibilityFailures ?? []).map((failure) => failure.code),
234
- ...bindingFailureCodes(measures, dimensions, filters),
550
+ ...bindingFailureCodes(measures, dimensions, filters, requiredOutputs),
235
551
  ]),
236
552
  candidateIds: uniqueSorted([
237
553
  ...(input.resolution.compatibilityFailures ?? []).flatMap((failure) => failure.candidateIds),
238
- ...bindingCandidateIds(measures, dimensions, filters),
554
+ ...bindingCandidateIds(measures, dimensions, filters, requiredOutputs),
239
555
  ]),
240
556
  ...(selectedCapability?.metricId
241
557
  ? { selectedCapabilityId: selectedCapability.metricId }
242
558
  : {}),
243
559
  ...(executionId ? { selectedExecutionId: executionId } : {}),
244
560
  finalCapability: capability,
245
- bindings: unresolvedBindingReceipts(measures, dimensions, filters),
561
+ bindings: unresolvedBindingReceipts(measures, dimensions, filters, requiredOutputs),
246
562
  },
247
563
  }
248
564
  : {}),
@@ -349,6 +665,41 @@ function capabilityDimensionMatchScore(requested, dimension, capability, candida
349
665
  + ((dimension.relationshipPathIds?.length ?? 0) > 0 ? 4 : 0);
350
666
  return identityScore + authorityScore;
351
667
  }
668
+ /**
669
+ * Match a frozen V2 frame dimension only through its own qualified identity,
670
+ * authored label/alias, or the host-authored same-snapshot extension that
671
+ * produced the frame. The latter is intentionally exact: a capability
672
+ * extension records both the original requested business term and the native
673
+ * MetricFlow dimension ID, so it can carry `product category` to
674
+ * `order_items.product_type` without turning arbitrary semantic aliases into
675
+ * compiler authority.
676
+ */
677
+ function frameDimensionMatchesRequestedBinding(input) {
678
+ if (!input.requested)
679
+ return false;
680
+ const namespaceTail = input.dimension.dimensionId.split(':').at(-1) ?? input.dimension.dimensionId;
681
+ const identities = [
682
+ input.dimension.dimensionId,
683
+ namespaceTail,
684
+ input.dimension.label,
685
+ ...(input.dimension.aliases ?? []),
686
+ ]
687
+ .filter((value) => Boolean(value))
688
+ .map(normalize);
689
+ if (identities.includes(input.requested))
690
+ return true;
691
+ return input.candidates.some((candidate) => {
692
+ const extension = candidate.sameSnapshotRoleExtension;
693
+ if (!extension
694
+ || extension.version !== 1
695
+ || extension.role !== 'categorical_dimension'
696
+ || normalize(extension.requestedTerm) !== input.requested)
697
+ return false;
698
+ const candidateId = candidate.qualifiedId ?? candidate.id;
699
+ return candidateId === input.dimension.dimensionId
700
+ && extension.dimensionId === input.dimension.dimensionId;
701
+ });
702
+ }
352
703
  function conventionalIdentitySpecificity(term, requestedTokens) {
353
704
  const termTokens = canonicalTokens(term);
354
705
  if (!requestedTokens.every((token) => termTokens.includes(token)))
@@ -410,13 +761,31 @@ function eligibleCapabilityDimensions(capability, requiredRoles) {
410
761
  if (!requiredRoles.every((role) => dimension.supportedRoles.includes(role)))
411
762
  return false;
412
763
  const sameEntity = dimension.entityId === capability.primaryEntityId;
413
- const declaredGrain = capability.resultGrainIds.includes(dimension.entityId);
414
- const relationshipProven = (dimension.relationshipPathIds?.length ?? 0) > 0;
415
- const nativeGroupingProven = Boolean(dimension.nativeGroupingReference)
416
- && capability.executionCapabilities.some((execution) => execution.route === 'semantic');
417
- return sameEntity || declaredGrain || relationshipProven || nativeGroupingProven;
764
+ // A semantic metric may only use a cross-model dimension when the
765
+ // capability carries the exact adapter-native grouping reference which
766
+ // the compiler can consume. Result-grain membership and a generic DQL
767
+ // relationship ID prove neither MetricFlow spelling nor semantic adapter
768
+ // reachability, so they must not admit a semantic freeze on their own.
769
+ // Non-semantic routes retain their separately governed relationship
770
+ // authority below.
771
+ const semanticNativeGroupingProven = semanticNativeGroupingIsExact(capability, dimension);
772
+ const nonSemanticRoute = capability.executionCapabilities.some((execution) => execution.route !== 'semantic');
773
+ const relationalProof = nonSemanticRoute
774
+ && (capability.resultGrainIds.includes(dimension.entityId)
775
+ || (dimension.relationshipPathIds?.length ?? 0) > 0);
776
+ return sameEntity || semanticNativeGroupingProven || relationalProof;
418
777
  });
419
778
  }
779
+ /**
780
+ * Verify a cross-model semantic grouping before it is allowed to bind the
781
+ * frozen plan. The capability is the only authority here: leaf-name equality,
782
+ * a broad result grain, and a physical/DQL relationship are intentionally not
783
+ * enough to claim MetricFlow can compose this tuple.
784
+ */
785
+ function semanticNativeGroupingIsExact(capability, dimension) {
786
+ return dimension.entityId === capability.primaryEntityId
787
+ || semanticDimensionUsesExactAdapterGrouping(capability, dimension);
788
+ }
420
789
  function capabilityEntailsFrameDimension(capability, binding) {
421
790
  if (!capability)
422
791
  return false;
@@ -461,11 +830,12 @@ function selectedCapabilityProofFacts(capability, dimensions) {
461
830
  ...capability.executionCapabilities.map((execution) => `capability:route:${execution.route}:${execution.adapterId ?? 'native'}`),
462
831
  ];
463
832
  }
464
- function unresolvedBindingReceipts(measures, dimensions, filters) {
833
+ function unresolvedBindingReceipts(measures, dimensions, filters, requiredOutputs = []) {
465
834
  return [
466
835
  ...measures.map((binding) => ({ kind: 'measure', binding })),
467
836
  ...dimensions.map((binding) => ({ kind: 'dimension', binding })),
468
837
  ...filters.map((filter) => ({ kind: 'filter', binding: filter.binding })),
838
+ ...requiredOutputs.map((binding) => ({ kind: 'output', binding })),
469
839
  ]
470
840
  .filter(({ binding }) => binding.status !== 'resolved')
471
841
  .map(({ kind, binding }) => ({
@@ -475,19 +845,20 @@ function unresolvedBindingReceipts(measures, dimensions, filters) {
475
845
  candidateIds: [...binding.candidateIds],
476
846
  }));
477
847
  }
478
- function bindingCandidateIds(measures, dimensions, filters) {
479
- return unresolvedBindingReceipts(measures, dimensions, filters)
848
+ function bindingCandidateIds(measures, dimensions, filters, requiredOutputs = []) {
849
+ return unresolvedBindingReceipts(measures, dimensions, filters, requiredOutputs)
480
850
  .flatMap((binding) => binding.candidateIds);
481
851
  }
482
- function bindingFailureCodes(measures, dimensions, filters) {
483
- return unresolvedBindingReceipts(measures, dimensions, filters)
852
+ function bindingFailureCodes(measures, dimensions, filters, requiredOutputs = []) {
853
+ return unresolvedBindingReceipts(measures, dimensions, filters, requiredOutputs)
484
854
  .map((binding) => `${binding.kind.toUpperCase()}_${binding.status.toUpperCase()}`);
485
855
  }
486
- function bindingMissingInformation(measures, dimensions, filters) {
856
+ function bindingMissingInformation(measures, dimensions, filters, requiredOutputs = []) {
487
857
  return [
488
858
  ...measures.map((binding) => ({ kind: 'measure', binding })),
489
859
  ...dimensions.map((binding) => ({ kind: 'dimension', binding })),
490
860
  ...filters.map((filter) => ({ kind: 'filter', binding: filter.binding })),
861
+ ...requiredOutputs.map((binding) => ({ kind: 'output', binding })),
491
862
  ].flatMap(({ kind, binding }) => {
492
863
  if (binding.status === 'resolved')
493
864
  return [];
@@ -523,6 +894,7 @@ export function deriveResolvedAnalyticalPlan(parent, delta) {
523
894
  ? (timeRange ? resolvePlanTimeRange(timeRange, delta.referenceTime ?? new Date()) : undefined)
524
895
  : parent.query.timeBounds;
525
896
  const unresolved = [...measures, ...dimensions, ...filters.map((filter) => filter.binding)]
897
+ .concat((parent.outputContract.requiredOutputs ?? []).map(cloneBinding))
526
898
  .filter((binding) => binding.status !== 'resolved');
527
899
  const payload = {
528
900
  ...parent,
@@ -544,7 +916,8 @@ export function deriveResolvedAnalyticalPlan(parent, delta) {
544
916
  },
545
917
  outputContract: {
546
918
  measures: uniqueSorted(measures.flatMap((binding) => binding.outputName ? [binding.outputName] : binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
547
- dimensions: uniqueSorted(dimensions.flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
919
+ dimensions: uniqueSorted(dimensions.flatMap((binding) => binding.outputName ? [binding.outputName] : binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
920
+ requiredOutputs: (parent.outputContract.requiredOutputs ?? []).map(cloneBinding),
548
921
  ...((delta.timeGrain ?? parent.query.timeGrain)
549
922
  ? { timeGrain: delta.timeGrain ?? parent.query.timeGrain }
550
923
  : {}),
@@ -622,13 +995,32 @@ function bindRequestedMember(requested, candidates, kind, question = '') {
622
995
  // a second binding identity into the frozen plan. Only candidates still
623
996
  // eligible for the requested tuple can contribute member authority.
624
997
  const eligibleCandidates = candidates.filter((candidate) => candidate.compatibility !== 'incompatible');
625
- const directCandidates = eligibleCandidates.filter((candidate) => {
998
+ const kindCandidates = eligibleCandidates.filter((candidate) => {
626
999
  const kindMatches = kind === 'measure'
627
1000
  ? candidate.kind === 'semantic_metric' || candidate.kind === 'sql_column'
628
- : candidate.kind === 'semantic_member' || candidate.kind === 'sql_column';
629
- return kindMatches
630
- && candidateTerms(candidate).some((term) => memberTermMatches(term, normalized));
1001
+ : kind === 'dimension'
1002
+ ? candidate.kind === 'semantic_member' || candidate.kind === 'sql_column'
1003
+ // Explicit projected fields are not grouping dimensions. A metric can
1004
+ // satisfy a requested price/revenue output only when it is selected as
1005
+ // the measure; the projection lane itself accepts qualified members or
1006
+ // physical columns.
1007
+ : candidate.kind === 'semantic_member' || candidate.kind === 'sql_column';
1008
+ return kindMatches;
631
1009
  });
1010
+ // Explicit outputs are a projection contract, not an approximate semantic
1011
+ // search. Prefer a physical qualified column whose leaf is exactly the
1012
+ // requested field. This keeps `product_id` from becoming an alternative for
1013
+ // `order id` through a noisy alias and collapses duplicate cards for the
1014
+ // same physical relation.column. Different exact physical columns remain
1015
+ // distinct, so genuine ambiguity is still surfaced for clarification.
1016
+ const exactOutputCandidates = kind === 'output'
1017
+ ? exactOutputBindingCandidates(kindCandidates, normalized)
1018
+ : [];
1019
+ const directCandidates = kind === 'output'
1020
+ ? exactOutputCandidates.length > 0 || isExplicitOutputIdentity(normalized)
1021
+ ? exactOutputCandidates
1022
+ : uniqueOutputBindingCandidates(kindCandidates.filter((candidate) => candidateTerms(candidate).some((term) => memberTermMatches(term, normalized))))
1023
+ : kindCandidates.filter((candidate) => candidateTerms(candidate).some((term) => memberTermMatches(term, normalized)));
632
1024
  const collectedIds = uniqueSorted([
633
1025
  ...directCandidates.map((candidate) => candidate.qualifiedId ?? candidate.id),
634
1026
  ...(kind === 'dimension' ? eligibleCandidates.flatMap((candidate) => (candidate.dimensions ?? []).filter((dimension) => {
@@ -667,12 +1059,29 @@ function bindRequestedMember(requested, candidates, kind, question = '') {
667
1059
  };
668
1060
  }
669
1061
  }
1062
+ if (ids.length === 0 && (kind === 'dimension' || kind === 'output') && eligibleCandidates.length === 1) {
1063
+ const certified = eligibleCandidates[0];
1064
+ const outputName = certifiedCandidateDeclaredDimensionOutput(certified, requested);
1065
+ if (certified.kind === 'certified_block'
1066
+ && certified.compatibility === 'compatible'
1067
+ && outputName) {
1068
+ const id = certified.qualifiedId ?? certified.id;
1069
+ return {
1070
+ requested,
1071
+ qualifiedId: id,
1072
+ outputName,
1073
+ status: 'resolved',
1074
+ candidateIds: [id],
1075
+ };
1076
+ }
1077
+ }
670
1078
  const namedWinner = memberNamedInQuestion(ids, eligibleCandidates, question);
671
1079
  if (namedWinner) {
672
1080
  const winnerCandidate = directCandidates.find((candidate) => (candidate.qualifiedId ?? candidate.id) === namedWinner);
673
1081
  return {
674
1082
  requested,
675
1083
  qualifiedId: namedWinner,
1084
+ ...(kind === 'output' ? { outputName: outputNameForCandidate(winnerCandidate, namedWinner) } : {}),
676
1085
  ...(winnerCandidate?.aggregation ? { aggregation: winnerCandidate.aggregation } : {}),
677
1086
  status: 'resolved',
678
1087
  candidateIds: [namedWinner],
@@ -684,11 +1093,85 @@ function bindRequestedMember(requested, candidates, kind, question = '') {
684
1093
  return {
685
1094
  requested,
686
1095
  ...(ids.length === 1 ? { qualifiedId: ids[0] } : {}),
1096
+ ...(kind === 'output' && ids.length === 1
1097
+ ? { outputName: outputNameForCandidate(direct, ids[0]) }
1098
+ : {}),
687
1099
  ...(direct?.aggregation ? { aggregation: direct.aggregation } : {}),
688
1100
  status: ids.length === 1 ? 'resolved' : ids.length > 1 ? 'ambiguous' : 'unresolved',
689
1101
  candidateIds: ids,
690
1102
  };
691
1103
  }
1104
+ /**
1105
+ * Return the exact host-authoritative output candidates for a requested
1106
+ * projection. Physical columns win over descriptive semantic aliases: when a
1107
+ * selected closure contains `order_items.order_id`, a semantic entity named
1108
+ * `order_id` is useful retrieval context but is not a competing SQL output.
1109
+ */
1110
+ function exactOutputBindingCandidates(candidates, requested) {
1111
+ const exactPhysical = candidates.filter((candidate) => physicalOutputColumnLeaf(candidate) === requested);
1112
+ if (exactPhysical.length > 0)
1113
+ return uniqueOutputBindingCandidates(exactPhysical);
1114
+ // Semantic projections do not always carry a physical column identity. They
1115
+ // may still bind if their declared name itself is exact; aliases alone never
1116
+ // have authority to substitute one requested identifier for another.
1117
+ return uniqueOutputBindingCandidates(candidates.filter((candidate) => normalize(candidate.name) === requested));
1118
+ }
1119
+ /**
1120
+ * A requested `... id` / `... identifier` must not fall back to lexical alias
1121
+ * matching. In particular, an alias attached to product_id cannot make it a
1122
+ * candidate for order_id when no actual order_id is selected.
1123
+ */
1124
+ function isExplicitOutputIdentity(requested) {
1125
+ return /(?:^| )(?:id|identifier)$/.test(requested);
1126
+ }
1127
+ function physicalOutputColumnLeaf(candidate) {
1128
+ if (candidate.kind !== 'sql_column')
1129
+ return undefined;
1130
+ const raw = candidate.qualifiedId ?? candidate.id;
1131
+ const columnReference = raw.replace(/^(?:dbt|runtime):column:/i, '');
1132
+ const leaf = columnReference.split('.').at(-1);
1133
+ const normalized = normalize(leaf ?? '');
1134
+ return normalized || undefined;
1135
+ }
1136
+ /**
1137
+ * Candidate IDs can be emitted once by the dbt manifest and again by runtime
1138
+ * schema/index lanes. They describe one output only when their canonical
1139
+ * physical relation.column is identical. Do not dedupe two relations that
1140
+ * merely share the same leaf: that remains a real ambiguity.
1141
+ */
1142
+ function uniqueOutputBindingCandidates(candidates) {
1143
+ const seen = new Set();
1144
+ return candidates.filter((candidate) => {
1145
+ const identity = canonicalOutputBindingIdentity(candidate);
1146
+ if (seen.has(identity))
1147
+ return false;
1148
+ seen.add(identity);
1149
+ return true;
1150
+ });
1151
+ }
1152
+ function canonicalOutputBindingIdentity(candidate) {
1153
+ if (candidate.kind === 'sql_column') {
1154
+ const raw = candidate.qualifiedId ?? candidate.id;
1155
+ const columnReference = raw.replace(/^(?:dbt|runtime):column:/i, '');
1156
+ if (columnReference.includes('.'))
1157
+ return `physical:${normalize(columnReference)}`;
1158
+ }
1159
+ return `candidate:${candidate.qualifiedId ?? candidate.id}`;
1160
+ }
1161
+ /**
1162
+ * A selected physical/semantic output's alias is part of the frozen result
1163
+ * contract. Prefer the inspected candidate name; a qualified-id leaf is a
1164
+ * safe fallback for machine-authored dbt/sql column identities.
1165
+ */
1166
+ function outputNameForCandidate(candidate, qualifiedId) {
1167
+ const candidateName = candidate?.name?.trim();
1168
+ if (candidateName && /^[A-Za-z_][A-Za-z0-9_]*$/.test(candidateName))
1169
+ return candidateName;
1170
+ const leaf = qualifiedId.split(/[:.]/).at(-1)?.replace(/[^A-Za-z0-9_]/g, '_');
1171
+ return leaf && /^[A-Za-z_][A-Za-z0-9_]*$/.test(leaf)
1172
+ ? leaf
1173
+ : normalize(candidateName || qualifiedId).replace(/\s+/g, '_');
1174
+ }
692
1175
  function qualifyDeclaredDimension(candidate, dimension) {
693
1176
  if (/[:./]/.test(dimension) || !candidate.domain)
694
1177
  return dimension;
@@ -696,9 +1179,6 @@ function qualifyDeclaredDimension(candidate, dimension) {
696
1179
  const local = dimension.toLowerCase().replace(/[^a-z0-9_-]+/g, '_');
697
1180
  return `semantic:${domain}:dimension:${local}`;
698
1181
  }
699
- function resolutionUsesRelationalEvidence(resolution) {
700
- return resolution.recommendedRoute === 'governed_sql' || resolution.recommendedRoute === 'exploratory';
701
- }
702
1182
  function memberTermMatches(candidateTerm, requested) {
703
1183
  if (candidateTerm === requested || candidateTerm.endsWith(` ${requested}`))
704
1184
  return true;
@@ -711,7 +1191,7 @@ function candidateTerms(candidate) {
711
1191
  .map(normalize)
712
1192
  .filter(Boolean);
713
1193
  }
714
- function resolveCapability(resolution, execution, measures, dimensions, filters) {
1194
+ function resolveCapability(resolution, execution, measures, dimensions, filters, requiredOutputs = [], selectedCapability, frozenAuthorityCandidates = [], snapshotCandidates = []) {
715
1195
  if (resolution.confidence === "low" ||
716
1196
  resolution.recommendedRoute === "clarify" ||
717
1197
  !execution ||
@@ -728,24 +1208,399 @@ function resolveCapability(resolution, execution, measures, dimensions, filters)
728
1208
  (resolution.recommendedRoute === "certified" ||
729
1209
  resolution.recommendedRoute === "semantic"))
730
1210
  return "blocked";
1211
+ // A frozen plan may not silently drop an explicitly requested projection.
1212
+ // This applies to exploratory SQL too: otherwise a query can execute and
1213
+ // display five rows that omit one of the user-named identifiers.
1214
+ if (requiredOutputs.some((binding) => binding.status !== 'resolved'))
1215
+ return 'blocked';
731
1216
  if (execution.compatibility === "incompatible")
732
1217
  return "blocked";
733
1218
  if (resolution.recommendedRoute === "certified" &&
734
1219
  execution.kind === "certified_block" &&
735
1220
  execution.compatibility === "compatible" &&
736
- certifiedCandidateExplicitlyCoversMeasures(execution, measures.map((binding) => binding.requested))) {
1221
+ certifiedCandidateExplicitlyCoversMeasures(execution, measures.map((binding) => binding.requested)) &&
1222
+ certifiedBlockProvesHostProjection(execution, resolution)) {
737
1223
  return "certified_execution";
738
1224
  }
739
1225
  if (resolution.recommendedRoute === 'semantic'
740
1226
  && (execution.kind === 'semantic_metric' || execution.kind === 'semantic_member')
741
- && execution.compatibility === 'compatible')
1227
+ && semanticCapabilitiesProveFrozenTuple({
1228
+ capability: selectedCapability,
1229
+ measures,
1230
+ dimensions,
1231
+ filters,
1232
+ requiredOutputs,
1233
+ timeGrain: resolution.queryIntent.timeGrain,
1234
+ hasTimeRange: Boolean(resolution.queryIntent.timeRange),
1235
+ candidates: frozenAuthorityCandidates,
1236
+ })
1237
+ && (execution.compatibility === 'compatible'
1238
+ || sameSnapshotSemanticExtensionProvesFrozenTuple({
1239
+ execution,
1240
+ capability: selectedCapability,
1241
+ measures,
1242
+ dimensions,
1243
+ filters,
1244
+ requiredOutputs,
1245
+ candidates: frozenAuthorityCandidates,
1246
+ timeGrain: resolution.queryIntent.timeGrain,
1247
+ hasTimeRange: Boolean(resolution.queryIntent.timeRange),
1248
+ })))
742
1249
  return 'semantic_execution';
743
- if (resolution.recommendedRoute === 'governed_sql')
1250
+ // A relationship/entity card and a qualified physical closure are useful
1251
+ // evidence for exploratory SQL, but they are not themselves a governed DQL
1252
+ // projection. In particular, a DQL entity may bridge an order-items model
1253
+ // to related business context while the selected tuple still consists only
1254
+ // of dbt/runtime columns. Freezing that as governed relational defers the
1255
+ // first real eligibility check until the compiler, where it becomes a
1256
+ // post-freeze COMPILATION_FAILED with no permitted fallback.
1257
+ //
1258
+ // Keep the boundary before freeze: governed relational execution requires a
1259
+ // snapshot-declared capability with a concrete governed adapter and proof
1260
+ // for every bound measure/dimension/filter/output. A raw physical path is
1261
+ // deliberately returned as blocked here so the router can evaluate its
1262
+ // existing same-snapshot exploratory cascade, which rebuilds an immutable
1263
+ // review-required plan with `recommendedRoute: exploratory`.
1264
+ if (resolution.recommendedRoute === 'governed_sql'
1265
+ // A partial DQL modeling card can contribute relationship context, but it
1266
+ // cannot be the compiler authority for a frozen governed plan. In
1267
+ // particular, an entity card that merely names an order-item grain must
1268
+ // not turn selected dbt/runtime output columns into a governed DQL
1269
+ // projection. It remains eligible evidence for the router's safe
1270
+ // same-snapshot exploratory cascade below this pre-freeze gate.
1271
+ && execution.compatibility === 'compatible'
1272
+ && governedRelationalCapabilityProvesFrozenTuple({
1273
+ capability: selectedCapability,
1274
+ measures,
1275
+ dimensions,
1276
+ filters,
1277
+ requiredOutputs,
1278
+ timeGrain: resolution.queryIntent.timeGrain,
1279
+ hasTimeRange: Boolean(resolution.queryIntent.timeRange),
1280
+ snapshotCandidates,
1281
+ }))
744
1282
  return 'governed_relational';
745
1283
  if (resolution.recommendedRoute === 'exploratory')
746
1284
  return 'bounded_exploration';
747
1285
  return 'blocked';
748
1286
  }
1287
+ /**
1288
+ * Governed relational compilation has a distinct authority boundary from
1289
+ * physical SQL safety. The latter can prove a read-only, review-required
1290
+ * closure; it cannot mint a governed compiler projection. Require the
1291
+ * selected capability to declare the governed adapter and to prove the exact
1292
+ * frozen tuple before the router is allowed to freeze this tier.
1293
+ */
1294
+ function governedRelationalCapabilityProvesFrozenTuple(input) {
1295
+ const { capability } = input;
1296
+ if (!capability)
1297
+ return false;
1298
+ if (!capability.executionCapabilities.some((execution) => execution.route === 'governed_sql' && Boolean(execution.adapterId?.trim())))
1299
+ return false;
1300
+ const metricIds = new Set([capability.metricId, ...capability.measureIds]);
1301
+ if (!input.measures.every((binding) => binding.status === 'resolved'
1302
+ && Boolean(binding.qualifiedId)
1303
+ && metricIds.has(binding.qualifiedId)))
1304
+ return false;
1305
+ const provesDimension = (binding, roles) => {
1306
+ if (binding.status !== 'resolved' || !binding.qualifiedId)
1307
+ return false;
1308
+ const dimension = eligibleCapabilityDimensions(capability, roles)
1309
+ .find((candidate) => candidate.dimensionId === binding.qualifiedId);
1310
+ return Boolean(dimension
1311
+ && governedCapabilityDimensionHasFreshAutomaticRelationshipProofV1({
1312
+ capability,
1313
+ dimension,
1314
+ candidates: input.snapshotCandidates,
1315
+ }));
1316
+ };
1317
+ if (!input.dimensions.every((binding) => provesDimension(binding, ['group_by'])))
1318
+ return false;
1319
+ if (!input.filters.every((filter) => provesDimension(filter.binding, ['filter'])))
1320
+ return false;
1321
+ // An explicit output is a host-owned result-contract field, not an
1322
+ // implication of a physical relation. A `declaredOutputIds` entry is not a
1323
+ // compiler projection on its own: the governed compiler consumes metrics
1324
+ // and grouped dimensions. Therefore a non-measure output must be the exact
1325
+ // selected capability display/rank dimension *and* already be represented
1326
+ // in the frozen grouped tuple. This keeps raw `order_id` / `product_id`
1327
+ // columns and detached cross-entity display fields on the exploratory path
1328
+ // unless the authored governed graph can actually compile them.
1329
+ // Metrics follow the same rule: capability membership describes what could
1330
+ // be compiled, while the frozen query measures describe what *will* be
1331
+ // projected. A required `gross_profit` output may not piggyback on a
1332
+ // selected `revenue` measure just because both live in the capability.
1333
+ const groupedDimensionIds = new Set(input.dimensions
1334
+ .flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : []));
1335
+ const projectedMeasureIds = new Set(input.measures
1336
+ .filter((binding) => binding.status === 'resolved')
1337
+ .flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : []));
1338
+ const provesOutput = (binding) => {
1339
+ if (binding.status !== 'resolved' || !binding.qualifiedId)
1340
+ return false;
1341
+ if (metricIds.has(binding.qualifiedId))
1342
+ return projectedMeasureIds.has(binding.qualifiedId);
1343
+ if (!groupedDimensionIds.has(binding.qualifiedId))
1344
+ return false;
1345
+ return provesDimension(binding, ['display'])
1346
+ || provesDimension(binding, ['rank_entity']);
1347
+ };
1348
+ if (!input.requiredOutputs.every(provesOutput))
1349
+ return false;
1350
+ if (input.timeGrain && !capability.timeDimensions.some((dimension) => dimension.supportedGrains.some((grain) => normalize(grain) === normalize(input.timeGrain))))
1351
+ return false;
1352
+ return !input.hasTimeRange || capability.timeDimensions.length > 0;
1353
+ }
1354
+ /**
1355
+ * A selected certified block must prove the host-owned projection itself.
1356
+ * Contextual semantic capability metadata is useful corroboration, but it is
1357
+ * never authority to add an entity label, grouping field, or explicit output
1358
+ * that the block does not declare. This defensive freeze gate covers paths
1359
+ * where a binding was collapsed/projected before capability resolution.
1360
+ */
1361
+ function certifiedBlockProvesHostProjection(block, resolution) {
1362
+ const seed = resolution.hostRequirementSeed;
1363
+ if (!seed)
1364
+ return true;
1365
+ const projectedDimensions = certifiedBlockCanUseExactCanonicalDimensionProjection(block, resolution)
1366
+ ? resolution.queryIntent.dimensions
1367
+ : seed.queryIntent.dimensions;
1368
+ const terms = uniqueSorted([
1369
+ ...projectedDimensions,
1370
+ ...seed.requirements.entityTerms,
1371
+ ...seed.requirements.entityDisplayTerms,
1372
+ ...(seed.requirements.outputTerms ?? []),
1373
+ ].map(normalize).filter(Boolean));
1374
+ if (!terms.every((term) => Boolean(certifiedCandidateDeclaredDimensionOutput(block, term)
1375
+ || certifiedCandidateDeclaredMeasureOutput(block, term))))
1376
+ return false;
1377
+ const declaredRequestedDimensions = terms
1378
+ .map((term) => certifiedCandidateDeclaredDimensionOutput(block, term))
1379
+ .filter((output) => Boolean(output))
1380
+ .map(normalize);
1381
+ // A block can return profile attributes or filterable inputs beside the
1382
+ // output that establishes its grain. Treat only an authored grain-driving
1383
+ // output as an extra grouping field. This keeps a complete customer profile
1384
+ // executable for a customer ranking while still rejecting a scalar revenue
1385
+ // request against a customer-grain block.
1386
+ const declaredBlockDimensions = certifiedCandidateGrainDimensionOutputs(block)
1387
+ .map(normalize)
1388
+ .filter(Boolean);
1389
+ // Do not certify a narrower/wider saved answer than the frozen host tuple.
1390
+ // Extra grouped dimensions alter result grain even if the requested measure
1391
+ // happens to be an authored block output.
1392
+ return declaredBlockDimensions.length === 0
1393
+ || declaredBlockDimensions.every((dimension) => declaredRequestedDimensions.includes(dimension));
1394
+ }
1395
+ function certifiedBlockCanUseExactCanonicalDimensionProjection(block, resolution) {
1396
+ const seed = resolution.hostRequirementSeed;
1397
+ return Boolean(seed
1398
+ && resolution.recommendedRoute === 'certified'
1399
+ && block?.kind === 'certified_block'
1400
+ && block.exactMatch
1401
+ && block.compatibility === 'compatible'
1402
+ && block.analyticalFitClass === 'exact'
1403
+ && certifiedCandidateExplicitlyCoversMeasures(block, resolution.queryIntent.measures)
1404
+ && resolution.queryIntent.dimensions.length > 0
1405
+ && resolution.queryIntent.dimensions.every((dimension) => Boolean(certifiedCandidateDeclaredDimensionOutput(block, dimension)))
1406
+ && !sameNormalizedTerms(resolution.queryIntent.dimensions, seed.queryIntent.dimensions));
1407
+ }
1408
+ /**
1409
+ * The semantic route may freeze only after the exact frozen tuple can be
1410
+ * expressed by the selected semantic adapter. A metric-relative native
1411
+ * grouping reference is required for every cross-model dimension/filter.
1412
+ * Same-model dimensions are already a declared group-by on the metric model.
1413
+ */
1414
+ function semanticCapabilityProvesFrozenTuple(input) {
1415
+ const { capability } = input;
1416
+ // Metric-free semantic member lookups retain their existing exact registry
1417
+ // path. A metric tuple, however, must have a normalized capability.
1418
+ if (!capability)
1419
+ return input.measures.length === 0;
1420
+ if (!capability.executionCapabilities.some((execution) => execution.route === 'semantic' && Boolean(execution.adapterId?.trim())))
1421
+ return false;
1422
+ const metricIds = new Set([capability.metricId, ...capability.measureIds]);
1423
+ if (!input.measures.every((binding) => binding.status === 'resolved' && Boolean(binding.qualifiedId) && metricIds.has(binding.qualifiedId)))
1424
+ return false;
1425
+ const provesDimension = (binding, roles) => {
1426
+ if (binding.status !== 'resolved' || !binding.qualifiedId)
1427
+ return false;
1428
+ const dimension = capability.dimensions.find((candidate) => candidate.dimensionId === binding.qualifiedId
1429
+ && roles.every((role) => candidate.supportedRoles.includes(role)));
1430
+ return Boolean(dimension && (dimension.entityId === capability.primaryEntityId
1431
+ || semanticNativeGroupingIsExact(capability, dimension)));
1432
+ };
1433
+ if (!input.dimensions.every((binding) => provesDimension(binding, ['group_by'])))
1434
+ return false;
1435
+ if (!input.filters.every((filter) => provesDimension(filter.binding, ['filter'])))
1436
+ return false;
1437
+ // Explicit output projections are not grouping dimensions. They have their
1438
+ // own frozen result contract and cannot be smuggled into a semantic tuple by
1439
+ // a same-snapshot extension.
1440
+ if (input.requiredOutputs.length > 0)
1441
+ return false;
1442
+ if (input.timeGrain && !capability.timeDimensions.some((dimension) => dimension.supportedGrains.some((grain) => normalize(grain) === normalize(input.timeGrain))))
1443
+ return false;
1444
+ return !input.hasTimeRange || capability.timeDimensions.length > 0;
1445
+ }
1446
+ /**
1447
+ * A multi-metric semantic plan remains one immutable request frame, but each
1448
+ * selected metric has to prove that frame independently before the route can
1449
+ * freeze. The first selected capability remains the stable execution anchor
1450
+ * for backward-compatible receipts; it is never allowed to stand in for a
1451
+ * second requested metric. This closes the old single-capability shortcut
1452
+ * where a multi-metric frame was marked blocked (or, worse, could later be
1453
+ * compiled against only its first measure).
1454
+ */
1455
+ function semanticCapabilitiesProveFrozenTuple(input) {
1456
+ const resolvedMeasures = input.measures.filter((binding) => binding.status === 'resolved' && Boolean(binding.qualifiedId));
1457
+ if (resolvedMeasures.length <= 1) {
1458
+ return semanticCapabilityProvesFrozenTuple(input);
1459
+ }
1460
+ // Every requested metric must have a selected normalized capability. The
1461
+ // identity match is exact; aliases, matching leaves, or a correlated metric
1462
+ // are not enough to enlarge the frozen tuple.
1463
+ const capabilities = uniqueMetricCapabilities([
1464
+ input.capability,
1465
+ ...input.candidates.map((candidate) => normalizeMetricCapabilityContract(candidate.analyticalCapability)),
1466
+ ]);
1467
+ const selected = resolvedMeasures.map((measure) => {
1468
+ const matches = capabilities.filter((capability) => capability.metricId === measure.qualifiedId
1469
+ || capability.measureIds.includes(measure.qualifiedId));
1470
+ return matches.length === 1 ? { measure, capability: matches[0] } : undefined;
1471
+ });
1472
+ if (selected.some((entry) => !entry))
1473
+ return false;
1474
+ const entries = selected;
1475
+ const semanticRoutes = entries.map((entry) => entry.capability.executionCapabilities
1476
+ .filter((route) => route.route === 'semantic' && Boolean(route.adapterId?.trim())));
1477
+ if (semanticRoutes.some((routes) => routes.length !== 1))
1478
+ return false;
1479
+ const adapterId = semanticRoutes[0][0].adapterId;
1480
+ if (!entries.every((entry, index) => semanticRoutes[index][0].adapterId === adapterId
1481
+ && entry.capability.semanticModelId === entries[0].capability.semanticModelId))
1482
+ return false;
1483
+ return entries.every(({ measure, capability }) => semanticCapabilityProvesFrozenTuple({
1484
+ capability,
1485
+ measures: [measure],
1486
+ dimensions: input.dimensions,
1487
+ filters: input.filters,
1488
+ requiredOutputs: input.requiredOutputs,
1489
+ ...(input.timeGrain ? { timeGrain: input.timeGrain } : {}),
1490
+ hasTimeRange: input.hasTimeRange,
1491
+ }));
1492
+ }
1493
+ function uniqueMetricCapabilities(capabilities) {
1494
+ const byFingerprint = new Map();
1495
+ for (const capability of capabilities) {
1496
+ if (!capability)
1497
+ continue;
1498
+ // The source fingerprint is the snapshot identity, not a metric identity:
1499
+ // one semantic model can legitimately expose several requested metrics
1500
+ // from the same immutable snapshot. A duplicate of the *same* metric can
1501
+ // appear as selected execution and selected evidence, but must not turn
1502
+ // that metric into an ambiguous capability choice.
1503
+ byFingerprint.set(`${capability.sourceFingerprint}:${capability.metricId}`, capability);
1504
+ }
1505
+ return [...byFingerprint.values()];
1506
+ }
1507
+ /**
1508
+ * Retrieval cards are scored before the role-balanced same-snapshot extension
1509
+ * is admitted. A semantic metric can therefore retain `partial` even after
1510
+ * the host has selected its one proven MetricFlow grouping field. Do not make
1511
+ * `partial` generally executable: advance it only when the immutable frozen
1512
+ * tuple proves the exact metric, every required role, and the extension's
1513
+ * metric/dimension identity from the same snapshot.
1514
+ *
1515
+ * This is semantic-adapter authority only. It never authorizes a physical join
1516
+ * or exploratory SQL, and an omitted output, unbound role, mismatched metric,
1517
+ * mismatched dimension, unsupported time grain, or absent extension remains
1518
+ * blocked for the cascade to evaluate safely.
1519
+ */
1520
+ function sameSnapshotSemanticExtensionProvesFrozenTuple(input) {
1521
+ const { execution, capability } = input;
1522
+ if (execution.kind !== 'semantic_metric' || execution.compatibility !== 'partial' || !capability)
1523
+ return false;
1524
+ if (!semanticCapabilityProvesFrozenTuple(input))
1525
+ return false;
1526
+ // Explicit output terms have their own source-column result proof. A
1527
+ // semantic role extension does not prove such an extra projection.
1528
+ if (input.requiredOutputs.length > 0 || input.measures.length === 0)
1529
+ return false;
1530
+ const metricAuthorityIds = new Set([
1531
+ execution.id,
1532
+ execution.qualifiedId,
1533
+ capability.metricId,
1534
+ ...capability.measureIds,
1535
+ ].filter((id) => Boolean(id)));
1536
+ if (!input.measures.every((binding) => binding.status === 'resolved'
1537
+ && Boolean(binding.qualifiedId)
1538
+ && metricAuthorityIds.has(binding.qualifiedId)))
1539
+ return false;
1540
+ const declaredExtensionProvesTuple = input.candidates.some((candidate) => {
1541
+ const proof = proveSameSnapshotMetricflowRoleExtensionV1({
1542
+ candidate,
1543
+ metricCandidate: execution,
1544
+ });
1545
+ if (!proof)
1546
+ return false;
1547
+ const extension = candidate.sameSnapshotRoleExtension;
1548
+ return metricAuthorityIds.has(extension.metricId)
1549
+ && input.dimensions.some((binding) => binding.qualifiedId === proof.dimension.dimensionId);
1550
+ });
1551
+ if (declaredExtensionProvesTuple)
1552
+ return true;
1553
+ // dbt/MetricFlow registry cards commonly have a registry identity such as
1554
+ // `semantic:dimension:customers.customer_name`, while the selected metric
1555
+ // capability carries its canonical MetricFlow identity
1556
+ // `semantic:uncategorized:dimension:customers.customer_name`. Both are
1557
+ // sourced from the same immutable snapshot. A selected semantic member that
1558
+ // *exactly* names one declared group-by field is therefore a direct
1559
+ // same-snapshot binding, even when the index did not materialize the older
1560
+ // `sameSnapshotRoleExtension` wrapper. This does not broaden selection:
1561
+ // only cards already in the frozen authority set are considered, and every
1562
+ // requested bound dimension must be proved by the selected metric's own
1563
+ // capability before the semantic tier can freeze.
1564
+ return input.dimensions.every((binding) => {
1565
+ if (binding.status !== 'resolved' || !binding.qualifiedId)
1566
+ return false;
1567
+ const dimension = capability.dimensions.find((item) => item.dimensionId === binding.qualifiedId
1568
+ && item.supportedRoles.includes('group_by'));
1569
+ return Boolean(dimension && input.candidates.some((candidate) =>
1570
+ // When metadata supplies an explicit extension record, it is the
1571
+ // authority for this bridge and must name the selected metric. Do not
1572
+ // bypass a mismatched record through an alias-only direct fallback.
1573
+ !candidate.sameSnapshotRoleExtension
1574
+ && selectedSemanticMemberMatchesCapabilityDimension(candidate, dimension)));
1575
+ });
1576
+ }
1577
+ /**
1578
+ * Match only stable metadata identities, never a free-form short alias. This
1579
+ * lets a selected registry card bind the same declared MetricFlow field while
1580
+ * preventing a broad word such as `customer` from making `customer_order_number`
1581
+ * look like the requested customer display key.
1582
+ */
1583
+ function selectedSemanticMemberMatchesCapabilityDimension(candidate, dimension) {
1584
+ if (candidate.kind !== 'semantic_member' || candidate.compatibility === 'incompatible')
1585
+ return false;
1586
+ const stableForms = (values) => new Set(values
1587
+ .filter((value) => Boolean(value))
1588
+ .filter((value) => /[.:/_-]/.test(value))
1589
+ .map(normalize)
1590
+ .filter(Boolean));
1591
+ const candidateForms = stableForms([
1592
+ candidate.id,
1593
+ candidate.qualifiedId,
1594
+ candidate.name,
1595
+ ...(candidate.aliases ?? []),
1596
+ ]);
1597
+ const dimensionForms = stableForms([
1598
+ dimension.dimensionId,
1599
+ dimension.label,
1600
+ ...(dimension.aliases ?? []),
1601
+ ]);
1602
+ return [...candidateForms].some((form) => dimensionForms.has(form));
1603
+ }
749
1604
  function normalize(value) {
750
1605
  return value.toLowerCase()
751
1606
  .replace(/%/g, ' percentage ')
@@ -754,6 +1609,40 @@ function normalize(value) {
754
1609
  .replace(/\s+/g, ' ')
755
1610
  .trim();
756
1611
  }
1612
+ /**
1613
+ * Resolve a host-owned display-key requirement against a capability that uses
1614
+ * the generic entity field as its declared display output. This is a
1615
+ * deterministic capability projection, not an interpretation of the user
1616
+ * question and never mutates the requirement seed.
1617
+ */
1618
+ function collapseHostEntityDisplayBindings(bindings, seed) {
1619
+ if (!seed)
1620
+ return bindings;
1621
+ const displayOnlyTerms = new Set(seed.requirements.entityDisplayTerms
1622
+ .map(normalize)
1623
+ .filter((term) => term.length > 0 && !seed.requirements.dimensions.some((dimension) => normalize(dimension) === term)));
1624
+ const entityTerms = seed.requirements.entityTerms.map(normalize).filter(Boolean);
1625
+ if (displayOnlyTerms.size === 0 || entityTerms.length === 0)
1626
+ return bindings;
1627
+ const isEntityCompanion = (binding) => {
1628
+ const requested = normalize(binding.requested);
1629
+ return entityTerms.some((entity) => requested === entity || requested.startsWith(`${entity} `));
1630
+ };
1631
+ return bindings.filter((binding, index, all) => {
1632
+ const requested = normalize(binding.requested);
1633
+ if (!displayOnlyTerms.has(requested))
1634
+ return true;
1635
+ const companion = all.find((other, otherIndex) => otherIndex !== index && isEntityCompanion(other));
1636
+ // An ambiguous generic entity is still the sole decision the reader must
1637
+ // make. Do not add an unrelated "account name" unresolved gap beside the
1638
+ // two qualified account choices.
1639
+ if (!companion || companion.status === 'unresolved')
1640
+ return true;
1641
+ if (binding.status === 'unresolved')
1642
+ return false;
1643
+ return binding.status !== 'resolved' || binding.qualifiedId !== companion.qualifiedId;
1644
+ });
1645
+ }
757
1646
  /** Resolve common analytical ranges once so no executor reinterprets "last month". */
758
1647
  export function resolvePlanTimeRange(expression, referenceTime) {
759
1648
  const text = expression.trim().toLowerCase();
@@ -819,6 +1708,20 @@ function addUnits(value, unit, count) {
819
1708
  function uniqueSorted(values) {
820
1709
  return [...new Set(values.filter(Boolean))].sort();
821
1710
  }
1711
+ function sameNormalizedTerms(left, right) {
1712
+ const normalizeTerms = (values) => uniqueSorted(values.map(normalize));
1713
+ return normalizeTerms(left).join('\u0000') === normalizeTerms(right).join('\u0000');
1714
+ }
1715
+ function uniqueCandidatesByCanonicalId(candidates, canonicalId) {
1716
+ const seen = new Set();
1717
+ return candidates.filter((candidate) => {
1718
+ const id = canonicalId(candidate);
1719
+ if (!id || seen.has(id))
1720
+ return false;
1721
+ seen.add(id);
1722
+ return true;
1723
+ });
1724
+ }
822
1725
  function sha256(value) {
823
1726
  return createHash('sha256').update(value).digest('hex');
824
1727
  }