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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/agent-run-engine.d.ts +63 -2
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +864 -29
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-planner.d.ts +6 -0
  6. package/dist/agent-run-planner.d.ts.map +1 -1
  7. package/dist/agent-run-planner.js +2 -0
  8. package/dist/agent-run-planner.js.map +1 -1
  9. package/dist/agent-run-store.d.ts.map +1 -1
  10. package/dist/agent-run-store.js +48 -3
  11. package/dist/agent-run-store.js.map +1 -1
  12. package/dist/agentic/analyst-loop.d.ts +7 -1
  13. package/dist/agentic/analyst-loop.d.ts.map +1 -1
  14. package/dist/agentic/analyst-loop.js +163 -0
  15. package/dist/agentic/analyst-loop.js.map +1 -1
  16. package/dist/agentic/assumptions.d.ts +1 -1
  17. package/dist/agentic/assumptions.d.ts.map +1 -1
  18. package/dist/agentic/assumptions.js.map +1 -1
  19. package/dist/agentic/research-agent.d.ts.map +1 -1
  20. package/dist/agentic/research-agent.js +10 -1
  21. package/dist/agentic/research-agent.js.map +1 -1
  22. package/dist/agentic/sql-authorization.d.ts +8 -0
  23. package/dist/agentic/sql-authorization.d.ts.map +1 -1
  24. package/dist/agentic/sql-authorization.js +3 -0
  25. package/dist/agentic/sql-authorization.js.map +1 -1
  26. package/dist/analytical-compatibility.d.ts.map +1 -1
  27. package/dist/analytical-compatibility.js +8 -1
  28. package/dist/analytical-compatibility.js.map +1 -1
  29. package/dist/analytical-failure-repair.d.ts.map +1 -1
  30. package/dist/analytical-failure-repair.js +6 -2
  31. package/dist/analytical-failure-repair.js.map +1 -1
  32. package/dist/analytical-frame.d.ts +23 -1
  33. package/dist/analytical-frame.d.ts.map +1 -1
  34. package/dist/analytical-frame.js +322 -16
  35. package/dist/analytical-frame.js.map +1 -1
  36. package/dist/analytical-orchestration.d.ts +314 -3
  37. package/dist/analytical-orchestration.d.ts.map +1 -1
  38. package/dist/analytical-orchestration.js +486 -43
  39. package/dist/analytical-orchestration.js.map +1 -1
  40. package/dist/answer-loop.d.ts +64 -8
  41. package/dist/answer-loop.d.ts.map +1 -1
  42. package/dist/answer-loop.js +528 -67
  43. package/dist/answer-loop.js.map +1 -1
  44. package/dist/ask-observability/index.d.ts +11 -0
  45. package/dist/ask-observability/index.d.ts.map +1 -0
  46. package/dist/ask-observability/index.js +7 -0
  47. package/dist/ask-observability/index.js.map +1 -0
  48. package/dist/ask-observability/instrumentation.d.ts +35 -0
  49. package/dist/ask-observability/instrumentation.d.ts.map +1 -0
  50. package/dist/ask-observability/instrumentation.js +472 -0
  51. package/dist/ask-observability/instrumentation.js.map +1 -0
  52. package/dist/ask-observability/observer.d.ts +62 -0
  53. package/dist/ask-observability/observer.d.ts.map +1 -0
  54. package/dist/ask-observability/observer.js +537 -0
  55. package/dist/ask-observability/observer.js.map +1 -0
  56. package/dist/ask-observability/portable.d.ts +98 -0
  57. package/dist/ask-observability/portable.d.ts.map +1 -0
  58. package/dist/ask-observability/portable.js +527 -0
  59. package/dist/ask-observability/portable.js.map +1 -0
  60. package/dist/ask-observability/store.d.ts +130 -0
  61. package/dist/ask-observability/store.d.ts.map +1 -0
  62. package/dist/ask-observability/store.js +1114 -0
  63. package/dist/ask-observability/store.js.map +1 -0
  64. package/dist/ask-observability/types.d.ts +357 -0
  65. package/dist/ask-observability/types.d.ts.map +1 -0
  66. package/dist/ask-observability/types.js +2 -0
  67. package/dist/ask-observability/types.js.map +1 -0
  68. package/dist/ask-observability/utils.d.ts +21 -0
  69. package/dist/ask-observability/utils.d.ts.map +1 -0
  70. package/dist/ask-observability/utils.js +102 -0
  71. package/dist/ask-observability/utils.js.map +1 -0
  72. package/dist/generated-analytical-proposal.d.ts +37 -1
  73. package/dist/generated-analytical-proposal.d.ts.map +1 -1
  74. package/dist/generated-analytical-proposal.js +211 -3
  75. package/dist/generated-analytical-proposal.js.map +1 -1
  76. package/dist/governed-relational-compiler.d.ts +15 -0
  77. package/dist/governed-relational-compiler.d.ts.map +1 -1
  78. package/dist/governed-relational-compiler.js +99 -1
  79. package/dist/governed-relational-compiler.js.map +1 -1
  80. package/dist/index.d.ts +9 -7
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +6 -4
  83. package/dist/index.js.map +1 -1
  84. package/dist/intent-controller.d.ts +10 -0
  85. package/dist/intent-controller.d.ts.map +1 -1
  86. package/dist/intent-controller.js +1 -1
  87. package/dist/intent-controller.js.map +1 -1
  88. package/dist/meaning-resolution.d.ts +78 -6
  89. package/dist/meaning-resolution.d.ts.map +1 -1
  90. package/dist/meaning-resolution.js +331 -38
  91. package/dist/meaning-resolution.js.map +1 -1
  92. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  93. package/dist/metadata/analysis-planner.js +21 -1
  94. package/dist/metadata/analysis-planner.js.map +1 -1
  95. package/dist/metadata/catalog.d.ts.map +1 -1
  96. package/dist/metadata/catalog.js +1 -1
  97. package/dist/metadata/catalog.js.map +1 -1
  98. package/dist/metadata/meaning-evidence.d.ts +9 -2
  99. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  100. package/dist/metadata/meaning-evidence.js +261 -11
  101. package/dist/metadata/meaning-evidence.js.map +1 -1
  102. package/dist/plan-execution-adapter.d.ts +9 -1
  103. package/dist/plan-execution-adapter.d.ts.map +1 -1
  104. package/dist/plan-execution-adapter.js +93 -13
  105. package/dist/plan-execution-adapter.js.map +1 -1
  106. package/dist/provider-egress.d.ts +24 -10
  107. package/dist/provider-egress.d.ts.map +1 -1
  108. package/dist/provider-egress.js +22 -17
  109. package/dist/provider-egress.js.map +1 -1
  110. package/dist/providers/claude.d.ts.map +1 -1
  111. package/dist/providers/claude.js +10 -9
  112. package/dist/providers/claude.js.map +1 -1
  113. package/dist/providers/dispatch.d.ts +27 -1
  114. package/dist/providers/dispatch.d.ts.map +1 -1
  115. package/dist/providers/dispatch.js +63 -1
  116. package/dist/providers/dispatch.js.map +1 -1
  117. package/dist/providers/gemini.js +10 -10
  118. package/dist/providers/gemini.js.map +1 -1
  119. package/dist/providers/index.d.ts +2 -2
  120. package/dist/providers/index.d.ts.map +1 -1
  121. package/dist/providers/index.js +1 -1
  122. package/dist/providers/index.js.map +1 -1
  123. package/dist/providers/ollama.js +15 -15
  124. package/dist/providers/ollama.js.map +1 -1
  125. package/dist/providers/openai.js +22 -22
  126. package/dist/providers/openai.js.map +1 -1
  127. package/dist/providers/types.d.ts +52 -0
  128. package/dist/providers/types.d.ts.map +1 -1
  129. package/dist/providers/types.js.map +1 -1
  130. package/dist/relationship-proof.d.ts +81 -0
  131. package/dist/relationship-proof.d.ts.map +1 -1
  132. package/dist/relationship-proof.js +228 -4
  133. package/dist/relationship-proof.js.map +1 -1
  134. package/dist/resolved-analytical-plan.d.ts +18 -4
  135. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  136. package/dist/resolved-analytical-plan.js +764 -47
  137. package/dist/resolved-analytical-plan.js.map +1 -1
  138. package/dist/router.d.ts +10 -1
  139. package/dist/router.d.ts.map +1 -1
  140. package/dist/router.js +1785 -213
  141. package/dist/router.js.map +1 -1
  142. package/dist/semantic-bridge/compose.d.ts.map +1 -1
  143. package/dist/semantic-bridge/compose.js +23 -4
  144. package/dist/semantic-bridge/compose.js.map +1 -1
  145. package/package.json +4 -4
@@ -8,8 +8,9 @@
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, } from './meaning-resolution.js';
12
+ import { currentQuestionGroundedParsedIntent } from './analytical-orchestration.js';
13
+ import { buildResolvedRelationshipProofsV1, buildGovernedRelationshipSafetyProofsV1, governedCapabilityDimensionHasFreshAutomaticRelationshipProofV1, semanticDimensionUsesExactAdapterGrouping, } from './relationship-proof.js';
13
14
  export function buildResolvedAnalyticalPlan(input) {
14
15
  const byLegacyId = new Map(input.candidates.map((candidate) => [candidate.id, candidate]));
15
16
  const selectedCandidates = input.resolution.selectedConceptIds
@@ -22,7 +23,12 @@ export function buildResolvedAnalyticalPlan(input) {
22
23
  // complete tuple. Bind its declared inputs/outputs as one execution
23
24
  // authority instead of mixing in the semantic concept used to select it.
24
25
  ? [executionCandidate]
25
- : resolutionUsesRelationalEvidence(input.resolution)
26
+ // A governed-relational route may bind through its certified relationship
27
+ // closure. Exploratory is different: the router has already selected a
28
+ // minimal qualified physical closure before plan freeze. Re-opening the
29
+ // whole snapshot here lets unrelated columns re-enter an exact output
30
+ // contract and turns `order_id`/`product_id` into false ambiguities.
31
+ : input.resolution.recommendedRoute === 'governed_sql'
26
32
  ? input.candidates
27
33
  : selectedCandidates.length > 0
28
34
  ? selectedCandidates
@@ -30,7 +36,6 @@ export function buildResolvedAnalyticalPlan(input) {
30
36
  ? [executionCandidate]
31
37
  : input.candidates;
32
38
  const canonicalId = (candidate) => candidate.qualifiedId ?? candidate.id;
33
- const selectedConceptIds = selectedCandidates.map(canonicalId);
34
39
  const executionId = executionCandidate ? canonicalId(executionCandidate) : undefined;
35
40
  const measures = input.resolution.queryIntent.measures.length > 0
36
41
  ? input.resolution.queryIntent.measures.map((requested) => bindRequestedMember(requested, bindingCandidates, 'measure', input.question))
@@ -50,7 +55,134 @@ export function buildResolvedAnalyticalPlan(input) {
50
55
  ? input.candidates
51
56
  : bindingCandidates;
52
57
  const selectedCapability = normalizeMetricCapabilityContract(executionCandidate?.analyticalCapability);
58
+ const selectedCertifiedBlock = input.resolution.recommendedRoute === 'certified'
59
+ && executionCandidate?.kind === 'certified_block'
60
+ && executionCandidate.compatibility === 'compatible'
61
+ ? executionCandidate
62
+ : undefined;
63
+ // The direct router may canonicalize a literal request (for example,
64
+ // “food and drink”) to an exact block's declared output (`category`) after
65
+ // that exact block independently proved the tuple. Keep that narrow
66
+ // zero-provider projection distinct from the ordinary host requirement
67
+ // contract below; a model-selected block can never create it.
68
+ const exactCertifiedDimensionProjection = certifiedBlockCanUseExactCanonicalDimensionProjection(executionCandidate, input.resolution);
69
+ const hostEntityDisplayTerms = new Set(input.resolution.hostRequirementSeed?.requirements.entityDisplayTerms
70
+ .map(normalize)
71
+ .filter(Boolean) ?? []);
72
+ // A certified route is not a semantic capability route with a green badge.
73
+ // Every host-owned grouping/display/output term must be declared by the
74
+ // selected block itself before that block can freeze. The optional normalized
75
+ // capability can corroborate the selected block, but it may not substitute a
76
+ // contextual semantic field for a block output the block never promised.
77
+ const hostCertifiedProjectionTerms = new Set([
78
+ ...(input.resolution.hostRequirementSeed?.queryIntent.dimensions ?? []),
79
+ ...(input.resolution.hostRequirementSeed?.requirements.entityDisplayTerms ?? []),
80
+ ...(input.resolution.hostRequirementSeed?.requirements.outputTerms ?? []),
81
+ ].map(normalize).filter(Boolean));
82
+ const bindCertifiedDeclaredDimension = (requested) => {
83
+ const outputName = selectedCertifiedBlock
84
+ ? certifiedCandidateDeclaredDimensionOutput(selectedCertifiedBlock, requested)
85
+ : undefined;
86
+ if (!selectedCertifiedBlock || !outputName) {
87
+ return { requested, status: 'unresolved', candidateIds: [] };
88
+ }
89
+ const candidateId = canonicalId(selectedCertifiedBlock);
90
+ return {
91
+ requested,
92
+ qualifiedId: candidateId,
93
+ outputName,
94
+ status: 'resolved',
95
+ candidateIds: [candidateId],
96
+ };
97
+ };
98
+ const bindCertifiedDeclaredOutput = (requested) => {
99
+ const outputName = selectedCertifiedBlock
100
+ ? certifiedCandidateDeclaredDimensionOutput(selectedCertifiedBlock, requested)
101
+ ?? certifiedCandidateDeclaredMeasureOutput(selectedCertifiedBlock, requested)
102
+ : undefined;
103
+ if (!selectedCertifiedBlock || !outputName) {
104
+ return { requested, status: 'unresolved', candidateIds: [] };
105
+ }
106
+ const candidateId = canonicalId(selectedCertifiedBlock);
107
+ return {
108
+ requested,
109
+ qualifiedId: candidateId,
110
+ outputName,
111
+ status: 'resolved',
112
+ candidateIds: [candidateId],
113
+ };
114
+ };
53
115
  const bindDimension = (requested, roles) => {
116
+ const normalizedRequested = normalize(requested);
117
+ if (selectedCertifiedBlock && hostCertifiedProjectionTerms.has(normalizedRequested)) {
118
+ return bindCertifiedDeclaredDimension(requested);
119
+ }
120
+ // The candidate-ID meaning protocol cannot author a frame, but the host
121
+ // has already bound a V2 frame from the immutable requirement seed. A
122
+ // seeded rank/display requirement may use the human phrase `customer
123
+ // name` while the selected MetricFlow capability truthfully names the
124
+ // native field simply `customer`. Re-running that field through generic
125
+ // lexical scoring loses the authoritative host binding and makes an
126
+ // executable legacy capability look unmodeled. Reuse only the exact host
127
+ // frame rank entity, and only after the selected capability proves every
128
+ // required role. This is not a name-based fallback and cannot select
129
+ // Customer Type, Customer Owner, or a cross-model same-leaf field.
130
+ const hostRankEntityDimensionId = hostEntityDisplayTerms.has(normalizedRequested)
131
+ ? input.resolution.analyticalFrame?.ranking?.entityDimensionId
132
+ : undefined;
133
+ const hostRankEntityDimension = hostRankEntityDimensionId
134
+ ? selectedCapability?.dimensions.find((dimension) => dimension.dimensionId === hostRankEntityDimensionId)
135
+ : undefined;
136
+ if (hostRankEntityDimension && roles.every((role) => hostRankEntityDimension.supportedRoles.includes(role))) {
137
+ return {
138
+ requested,
139
+ qualifiedId: hostRankEntityDimension.dimensionId,
140
+ status: 'resolved',
141
+ candidateIds: [hostRankEntityDimension.dimensionId],
142
+ };
143
+ }
144
+ // The host-built V2 frame may intentionally retain a genuine native
145
+ // display-key ambiguity (for example Billing Account versus Service
146
+ // Account). Keep that exact, metric-owned choice set when RAP binds the
147
+ // same seeded phrase. Re-running it through lexical candidate matching
148
+ // would discard the qualified alternatives and turn a useful
149
+ // clarification into a false “unresolved” gap. The frame is already
150
+ // snapshot-bound; still require the selected capability to prove every
151
+ // requested role before carrying a choice forward.
152
+ const sourceAmbiguity = input.resolution.analyticalFrame?.ambiguity.find((entry) => {
153
+ const [lane, ...parts] = entry.field.split('.');
154
+ return lane === 'dimensions' && normalize(parts.join('.')) === normalizedRequested;
155
+ });
156
+ const sourceChoices = uniqueSorted((sourceAmbiguity?.candidateIds ?? []).filter((candidateId) => eligibleCapabilityDimensions(selectedCapability, roles)
157
+ .some((dimension) => dimension.dimensionId === candidateId)));
158
+ if (sourceChoices.length === 1) {
159
+ return {
160
+ requested,
161
+ qualifiedId: sourceChoices[0],
162
+ status: 'resolved',
163
+ candidateIds: sourceChoices,
164
+ };
165
+ }
166
+ if (sourceChoices.length > 1) {
167
+ return {
168
+ requested,
169
+ status: 'ambiguous',
170
+ candidateIds: sourceChoices,
171
+ };
172
+ }
173
+ // An exact certified block is its own execution authority. Its declared
174
+ // output contract may use the entity identity (`customer`) where the
175
+ // host-owned ranking seed deliberately retains the display phrase
176
+ // (`customer name`). Do not let the optional normalized semantic
177
+ // capability re-run that proven block output through lexical capability
178
+ // matching: the capability is context for the block, while the block's
179
+ // declared output is the certified proof. This remains narrow to the
180
+ // selected compatible block and cannot bind an unrelated retrieved field.
181
+ const certifiedOutput = selectedCertifiedBlock
182
+ ? certifiedCandidateDeclaredDimensionOutput(selectedCertifiedBlock, requested)
183
+ : undefined;
184
+ if (certifiedOutput && selectedCertifiedBlock)
185
+ return bindCertifiedDeclaredDimension(requested);
54
186
  const capabilityBinding = bindSelectedCapabilityDimension(requested, selectedCapability, roles, input.candidates);
55
187
  // A provider frame may describe a selected capability, but it cannot pick
56
188
  // one of that capability's grouping dimensions for the host. Once a
@@ -66,10 +198,8 @@ export function buildResolvedAnalyticalPlan(input) {
66
198
  }
67
199
  if (capabilityBinding && capabilityBinding.status !== 'unresolved')
68
200
  return capabilityBinding;
69
- const normalizedRequested = normalize(requested);
70
201
  const frameIds = uniqueSorted((input.resolution.analyticalFrame?.dimensions ?? [])
71
202
  .filter((binding) => capabilityEntailsFrameDimension(selectedCapability, binding)
72
- && (!selectedCapability || binding.role === 'time_axis')
73
203
  && memberTermMatches(normalize(binding.dimensionId), normalizedRequested))
74
204
  .map((binding) => binding.dimensionId));
75
205
  if (frameIds.length === 1) {
@@ -87,8 +217,68 @@ export function buildResolvedAnalyticalPlan(input) {
87
217
  const rankingRequested = input.resolution.questionType === 'ranking'
88
218
  || input.resolution.queryIntent.order !== undefined
89
219
  || input.resolution.queryIntent.limit !== undefined;
90
- const dimensions = input.resolution.queryIntent.dimensions
91
- .map((requested) => bindDimension(requested, rankingRequested ? ['group_by', 'rank_entity'] : ['group_by']));
220
+ // A ranked report can group by several dimensions, but exactly one of them
221
+ // is the entity being ranked. Requiring `rank_entity` on every grouping
222
+ // field made a complete tuple such as "top customers by product category"
223
+ // clarify because `product_type` correctly supports grouping but not being
224
+ // the ranked customer entity.
225
+ const rankEntityDimensionId = input.resolution.analyticalFrame?.ranking?.entityDimensionId;
226
+ const requiredDimensionRoles = (requested) => {
227
+ if (!rankingRequested)
228
+ return ['group_by'];
229
+ const rankEntity = selectedCapability?.dimensions.find((dimension) => dimension.dimensionId === rankEntityDimensionId);
230
+ const matchesHostEntityDisplay = hostEntityDisplayTerms.has(normalize(requested))
231
+ && Boolean(rankEntity && rankEntity.dimensionId === rankEntityDimensionId);
232
+ const matchesRankEntity = matchesHostEntityDisplay || Boolean(rankEntity && selectedCapability
233
+ && capabilityDimensionMatchScore(requested, rankEntity, selectedCapability, input.candidates) > 0);
234
+ return matchesRankEntity ? ['group_by', 'rank_entity'] : ['group_by'];
235
+ };
236
+ // The requirement seed is the immutable, host-owned tuple for a bounded
237
+ // meaning call. Route reconciliation may still carry a legacy/broad
238
+ // `queryIntent` alongside that seed for compatibility, but it must never
239
+ // reintroduce an entity noun that the seed deliberately separated from its
240
+ // display/rank requirement. For example, `top customers by product
241
+ // category` binds the host terms `customer name` and `product category`;
242
+ // a legacy `customer` dimension would make Customer Type and Customer Order
243
+ // Number compete with the metric-native Customer Name key after the frame
244
+ // had already resolved it. Structured selections are merged into the
245
+ // seed before this point, so the seeded dimensions remain complete.
246
+ //
247
+ // An exact authored certified asset is a zero-provider host decision. Its
248
+ // direct router canonicalizes literal value wording (for example, “food and
249
+ // drink”) to the block's declared output (`category`) only after that exact
250
+ // block itself proved the requested measures and output contract. A model
251
+ // cannot reach this branch: candidate-ID meaning merge restores the host
252
+ // seed before RAP construction. Preserve this narrow canonical projection
253
+ // instead of turning a certified answer back into two invented dimensions.
254
+ // Unseeded V1/legacy callers retain their historical query-intent path.
255
+ const seededDimensionTerms = exactCertifiedDimensionProjection
256
+ ? input.resolution.queryIntent.dimensions
257
+ : input.resolution.hostRequirementSeed
258
+ ? input.resolution.hostRequirementSeed.queryIntent.dimensions
259
+ : input.resolution.queryIntent.dimensions;
260
+ // A server-issued clarification choice is a typed continuation binding. It
261
+ // may complete a display/grouping role that did not occur literally in the
262
+ // source question (for example “Show the top names by revenue” → Customer
263
+ // Name), but only after the router revalidated the stable choice against
264
+ // this snapshot. Candidate-ID model output cannot use this carrier.
265
+ const authoritativeDimensionTerms = [...new Set([
266
+ ...seededDimensionTerms,
267
+ ...(input.resolution.structuredDimensionIds ?? []),
268
+ ])];
269
+ const rawDimensions = authoritativeDimensionTerms
270
+ .map((requested) => bindDimension(requested, requiredDimensionRoles(requested)));
271
+ // The host-owned seed retains an entity display term (for example, "customer
272
+ // name") so a model cannot quietly turn "top customers" into an anonymous
273
+ // entity id. Some authored semantic/certified capabilities deliberately
274
+ // expose that display role under the generic entity identity ("customer").
275
+ // In that case the label is not a second requested grouping dimension. Keep
276
+ // it in the immutable seed/receipt, but remove only a redundant execution
277
+ // binding when the same resolved entity proves it, or an unresolved label
278
+ // when a resolved generic entity already covers the role. A real explicit
279
+ // `by customer name` dimension remains untouched because it appears in the
280
+ // seed's ordinary dimensions as well as its display terms.
281
+ const dimensions = collapseHostEntityDisplayBindings(rawDimensions, input.resolution.hostRequirementSeed);
92
282
  // A provider may name the field it is grouping by and forget the MEMBER the
93
283
  // reader asked about: "What customer type is Wesley Jenkins?" came back with
94
284
  // `filters: []`, so the run returned all 200 customers and the narration
@@ -100,18 +290,52 @@ export function buildResolvedAnalyticalPlan(input) {
100
290
  // still win whenever it supplies any.
101
291
  const requestedFilters = input.resolution.queryIntent.filters.length > 0
102
292
  ? input.resolution.queryIntent.filters
103
- : (input.evidence.parsedIntent?.filters ?? []).filter((filter) => filter.field && filter.value);
293
+ : (input.resolution.hostRequirementSeed?.queryIntent.filters
294
+ ?? currentQuestionGroundedParsedIntent(input.question, input.evidence.parsedIntent)?.filters
295
+ ?? []).filter((filter) => filter.field && filter.value);
104
296
  const filters = requestedFilters.map((filter) => ({
105
297
  ...filter,
106
298
  binding: bindDimension(filter.field, ['filter']),
107
299
  }));
108
- const capabilityDimensionProof = resolvedCapabilityDimensionProof(selectedCapability, dimensions, filters);
109
- const proofCandidates = [
110
- ...selectedCandidates,
111
- ...(executionCandidate && !selectedCandidates.some((candidate) => candidate.id === executionCandidate.id)
300
+ // Output terms are a distinct host-owned projection. A model may choose a
301
+ // qualified binding for one only from the admitted snapshot, but it cannot
302
+ // turn explicit `order ID` / `product ID` output terms into dimensions or
303
+ // omit them from an exploratory result contract.
304
+ //
305
+ // Host-owned output bindings have to come from the qualified meaning
306
+ // closure, not from the provisional route label. A meaning response may
307
+ // still carry the legacy `governed_sql` recommendation before the cascade
308
+ // proves whether the tuple is relational or exploratory. Re-opening the
309
+ // complete snapshot in that state turns unrelated `orders.order_id` /
310
+ // `raw_items.order_id` cards into false alternatives to the explicitly
311
+ // selected `order_items.order_id` output.
312
+ //
313
+ // If no meaning candidate was selected, preserve the legacy evidence path
314
+ // so a deterministic/certified binding can still explain a genuine gap.
315
+ // Once selection exists, however, only that snapshot-bound closure can
316
+ // satisfy a user-named output. The authorizer still proves target,
317
+ // read-only, relation, and exact-expression safety before SQL dispatch.
318
+ const outputBindingCandidates = selectedCandidates.length > 0
319
+ ? selectedCandidates
320
+ : executionCandidate
112
321
  ? [executionCandidate]
113
- : []),
114
- ];
322
+ : bindingCandidates;
323
+ const requiredOutputs = (input.resolution.hostRequirementSeed?.requirements.outputTerms ?? [])
324
+ .map((requested) => selectedCertifiedBlock
325
+ ? bindCertifiedDeclaredOutput(requested)
326
+ : bindRequestedMember(requested, outputBindingCandidates, 'output', input.question));
327
+ const outputAuthorityCandidates = requiredOutputs.flatMap((binding) => {
328
+ const ids = new Set([binding.qualifiedId, ...binding.candidateIds].filter((id) => Boolean(id)));
329
+ return input.candidates.filter((candidate) => ids.has(canonicalId(candidate)) || ids.has(candidate.id));
330
+ });
331
+ const frozenAuthorityCandidates = uniqueCandidatesByCanonicalId([
332
+ ...selectedCandidates,
333
+ ...(executionCandidate ? [executionCandidate] : []),
334
+ ...outputAuthorityCandidates,
335
+ ], canonicalId);
336
+ const selectedConceptIds = frozenAuthorityCandidates.map(canonicalId);
337
+ const capabilityDimensionProof = resolvedCapabilityDimensionProof(selectedCapability, dimensions, filters);
338
+ const proofCandidates = frozenAuthorityCandidates;
115
339
  const compatibilityProof = proofCandidates.map((candidate) => ({
116
340
  candidateId: canonicalId(candidate),
117
341
  compatibility: candidate.compatibility,
@@ -122,8 +346,8 @@ export function buildResolvedAnalyticalPlan(input) {
122
346
  : []),
123
347
  ]),
124
348
  }));
125
- const capability = resolveCapability(input.resolution, executionCandidate, measures, dimensions, filters);
126
- const bindingGaps = bindingMissingInformation(measures, dimensions, filters);
349
+ const capability = resolveCapability(input.resolution, executionCandidate, measures, dimensions, filters, requiredOutputs, selectedCapability, frozenAuthorityCandidates, input.candidates);
350
+ const bindingGaps = bindingMissingInformation(measures, dimensions, filters, requiredOutputs);
127
351
  // The reader named a member that no grounding lane could bind. Answering the
128
352
  // question WITHOUT it silently changes what was asked — "what customer type
129
353
  // is Wesley Jenkins" became "list every customer" — so the plan carries it as
@@ -159,6 +383,15 @@ export function buildResolvedAnalyticalPlan(input) {
159
383
  snapshotId,
160
384
  })
161
385
  : [];
386
+ const governedRelationshipSafetyProofs = selectedCapability
387
+ && selectedExecutionCapability?.route === 'governed_sql'
388
+ ? buildGovernedRelationshipSafetyProofsV1({
389
+ capability: selectedCapability,
390
+ dimensions: capabilityDimensionProof,
391
+ candidates: input.candidates,
392
+ snapshotId,
393
+ })
394
+ : [];
162
395
  const payload = {
163
396
  schemaVersion: input.resolution.analyticalFrame
164
397
  ? 2
@@ -194,15 +427,17 @@ export function buildResolvedAnalyticalPlan(input) {
194
427
  entityGrain: selectedCapability
195
428
  ? resolvedCapabilityEntityGrain(selectedCapability, dimensions)
196
429
  : input.resolution.analyticalFrame?.entityGrainIds[0] ?? executionCandidate?.primaryEntity,
197
- sourceRelationIds: uniqueSorted(selectedCandidates.flatMap((candidate) => candidate.sourceObjects ?? [])),
430
+ sourceRelationIds: uniqueSorted(frozenAuthorityCandidates.flatMap((candidate) => candidate.sourceObjects ?? [])),
198
431
  relationshipPathIds: selectedCapability
199
432
  ? uniqueSorted(relationshipProofs.flatMap((proof) => proof.kind === 'dql_relationship_path' ? proof.relationshipPathIds : []))
200
- : uniqueSorted(selectedCandidates.flatMap((candidate) => candidate.relationshipEvidence ?? [])),
433
+ : uniqueSorted(frozenAuthorityCandidates.flatMap((candidate) => candidate.relationshipEvidence ?? [])),
201
434
  relationshipProofs,
435
+ governedRelationshipSafetyProofs,
202
436
  compatibilityProof,
203
437
  outputContract: {
204
438
  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])),
439
+ dimensions: uniqueSorted(dimensions.flatMap((binding) => binding.outputName ? [binding.outputName] : binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
440
+ requiredOutputs: requiredOutputs.map(cloneBinding),
206
441
  ...(input.resolution.queryIntent.timeGrain
207
442
  ? { timeGrain: input.resolution.queryIntent.timeGrain }
208
443
  : {}),
@@ -226,23 +461,23 @@ export function buildResolvedAnalyticalPlan(input) {
226
461
  outcome: input.resolution.compatibilityOutcome
227
462
  ?? (input.resolution.recommendedRoute === 'clarify' || input.resolution.confidence === 'low'
228
463
  ? 'clarify'
229
- : bindingCandidateIds(measures, dimensions, filters).length > 0
464
+ : bindingCandidateIds(measures, dimensions, filters, requiredOutputs).length > 0
230
465
  ? 'clarify'
231
466
  : 'modeling_gap'),
232
467
  codes: uniqueSorted([
233
468
  ...(input.resolution.compatibilityFailures ?? []).map((failure) => failure.code),
234
- ...bindingFailureCodes(measures, dimensions, filters),
469
+ ...bindingFailureCodes(measures, dimensions, filters, requiredOutputs),
235
470
  ]),
236
471
  candidateIds: uniqueSorted([
237
472
  ...(input.resolution.compatibilityFailures ?? []).flatMap((failure) => failure.candidateIds),
238
- ...bindingCandidateIds(measures, dimensions, filters),
473
+ ...bindingCandidateIds(measures, dimensions, filters, requiredOutputs),
239
474
  ]),
240
475
  ...(selectedCapability?.metricId
241
476
  ? { selectedCapabilityId: selectedCapability.metricId }
242
477
  : {}),
243
478
  ...(executionId ? { selectedExecutionId: executionId } : {}),
244
479
  finalCapability: capability,
245
- bindings: unresolvedBindingReceipts(measures, dimensions, filters),
480
+ bindings: unresolvedBindingReceipts(measures, dimensions, filters, requiredOutputs),
246
481
  },
247
482
  }
248
483
  : {}),
@@ -410,13 +645,31 @@ function eligibleCapabilityDimensions(capability, requiredRoles) {
410
645
  if (!requiredRoles.every((role) => dimension.supportedRoles.includes(role)))
411
646
  return false;
412
647
  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;
648
+ // A semantic metric may only use a cross-model dimension when the
649
+ // capability carries the exact adapter-native grouping reference which
650
+ // the compiler can consume. Result-grain membership and a generic DQL
651
+ // relationship ID prove neither MetricFlow spelling nor semantic adapter
652
+ // reachability, so they must not admit a semantic freeze on their own.
653
+ // Non-semantic routes retain their separately governed relationship
654
+ // authority below.
655
+ const semanticNativeGroupingProven = semanticNativeGroupingIsExact(capability, dimension);
656
+ const nonSemanticRoute = capability.executionCapabilities.some((execution) => execution.route !== 'semantic');
657
+ const relationalProof = nonSemanticRoute
658
+ && (capability.resultGrainIds.includes(dimension.entityId)
659
+ || (dimension.relationshipPathIds?.length ?? 0) > 0);
660
+ return sameEntity || semanticNativeGroupingProven || relationalProof;
418
661
  });
419
662
  }
663
+ /**
664
+ * Verify a cross-model semantic grouping before it is allowed to bind the
665
+ * frozen plan. The capability is the only authority here: leaf-name equality,
666
+ * a broad result grain, and a physical/DQL relationship are intentionally not
667
+ * enough to claim MetricFlow can compose this tuple.
668
+ */
669
+ function semanticNativeGroupingIsExact(capability, dimension) {
670
+ return dimension.entityId === capability.primaryEntityId
671
+ || semanticDimensionUsesExactAdapterGrouping(capability, dimension);
672
+ }
420
673
  function capabilityEntailsFrameDimension(capability, binding) {
421
674
  if (!capability)
422
675
  return false;
@@ -461,11 +714,12 @@ function selectedCapabilityProofFacts(capability, dimensions) {
461
714
  ...capability.executionCapabilities.map((execution) => `capability:route:${execution.route}:${execution.adapterId ?? 'native'}`),
462
715
  ];
463
716
  }
464
- function unresolvedBindingReceipts(measures, dimensions, filters) {
717
+ function unresolvedBindingReceipts(measures, dimensions, filters, requiredOutputs = []) {
465
718
  return [
466
719
  ...measures.map((binding) => ({ kind: 'measure', binding })),
467
720
  ...dimensions.map((binding) => ({ kind: 'dimension', binding })),
468
721
  ...filters.map((filter) => ({ kind: 'filter', binding: filter.binding })),
722
+ ...requiredOutputs.map((binding) => ({ kind: 'output', binding })),
469
723
  ]
470
724
  .filter(({ binding }) => binding.status !== 'resolved')
471
725
  .map(({ kind, binding }) => ({
@@ -475,19 +729,20 @@ function unresolvedBindingReceipts(measures, dimensions, filters) {
475
729
  candidateIds: [...binding.candidateIds],
476
730
  }));
477
731
  }
478
- function bindingCandidateIds(measures, dimensions, filters) {
479
- return unresolvedBindingReceipts(measures, dimensions, filters)
732
+ function bindingCandidateIds(measures, dimensions, filters, requiredOutputs = []) {
733
+ return unresolvedBindingReceipts(measures, dimensions, filters, requiredOutputs)
480
734
  .flatMap((binding) => binding.candidateIds);
481
735
  }
482
- function bindingFailureCodes(measures, dimensions, filters) {
483
- return unresolvedBindingReceipts(measures, dimensions, filters)
736
+ function bindingFailureCodes(measures, dimensions, filters, requiredOutputs = []) {
737
+ return unresolvedBindingReceipts(measures, dimensions, filters, requiredOutputs)
484
738
  .map((binding) => `${binding.kind.toUpperCase()}_${binding.status.toUpperCase()}`);
485
739
  }
486
- function bindingMissingInformation(measures, dimensions, filters) {
740
+ function bindingMissingInformation(measures, dimensions, filters, requiredOutputs = []) {
487
741
  return [
488
742
  ...measures.map((binding) => ({ kind: 'measure', binding })),
489
743
  ...dimensions.map((binding) => ({ kind: 'dimension', binding })),
490
744
  ...filters.map((filter) => ({ kind: 'filter', binding: filter.binding })),
745
+ ...requiredOutputs.map((binding) => ({ kind: 'output', binding })),
491
746
  ].flatMap(({ kind, binding }) => {
492
747
  if (binding.status === 'resolved')
493
748
  return [];
@@ -523,6 +778,7 @@ export function deriveResolvedAnalyticalPlan(parent, delta) {
523
778
  ? (timeRange ? resolvePlanTimeRange(timeRange, delta.referenceTime ?? new Date()) : undefined)
524
779
  : parent.query.timeBounds;
525
780
  const unresolved = [...measures, ...dimensions, ...filters.map((filter) => filter.binding)]
781
+ .concat((parent.outputContract.requiredOutputs ?? []).map(cloneBinding))
526
782
  .filter((binding) => binding.status !== 'resolved');
527
783
  const payload = {
528
784
  ...parent,
@@ -544,7 +800,8 @@ export function deriveResolvedAnalyticalPlan(parent, delta) {
544
800
  },
545
801
  outputContract: {
546
802
  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])),
803
+ dimensions: uniqueSorted(dimensions.flatMap((binding) => binding.outputName ? [binding.outputName] : binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
804
+ requiredOutputs: (parent.outputContract.requiredOutputs ?? []).map(cloneBinding),
548
805
  ...((delta.timeGrain ?? parent.query.timeGrain)
549
806
  ? { timeGrain: delta.timeGrain ?? parent.query.timeGrain }
550
807
  : {}),
@@ -622,13 +879,32 @@ function bindRequestedMember(requested, candidates, kind, question = '') {
622
879
  // a second binding identity into the frozen plan. Only candidates still
623
880
  // eligible for the requested tuple can contribute member authority.
624
881
  const eligibleCandidates = candidates.filter((candidate) => candidate.compatibility !== 'incompatible');
625
- const directCandidates = eligibleCandidates.filter((candidate) => {
882
+ const kindCandidates = eligibleCandidates.filter((candidate) => {
626
883
  const kindMatches = kind === 'measure'
627
884
  ? 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));
885
+ : kind === 'dimension'
886
+ ? candidate.kind === 'semantic_member' || candidate.kind === 'sql_column'
887
+ // Explicit projected fields are not grouping dimensions. A metric can
888
+ // satisfy a requested price/revenue output only when it is selected as
889
+ // the measure; the projection lane itself accepts qualified members or
890
+ // physical columns.
891
+ : candidate.kind === 'semantic_member' || candidate.kind === 'sql_column';
892
+ return kindMatches;
631
893
  });
894
+ // Explicit outputs are a projection contract, not an approximate semantic
895
+ // search. Prefer a physical qualified column whose leaf is exactly the
896
+ // requested field. This keeps `product_id` from becoming an alternative for
897
+ // `order id` through a noisy alias and collapses duplicate cards for the
898
+ // same physical relation.column. Different exact physical columns remain
899
+ // distinct, so genuine ambiguity is still surfaced for clarification.
900
+ const exactOutputCandidates = kind === 'output'
901
+ ? exactOutputBindingCandidates(kindCandidates, normalized)
902
+ : [];
903
+ const directCandidates = kind === 'output'
904
+ ? exactOutputCandidates.length > 0 || isExplicitOutputIdentity(normalized)
905
+ ? exactOutputCandidates
906
+ : uniqueOutputBindingCandidates(kindCandidates.filter((candidate) => candidateTerms(candidate).some((term) => memberTermMatches(term, normalized))))
907
+ : kindCandidates.filter((candidate) => candidateTerms(candidate).some((term) => memberTermMatches(term, normalized)));
632
908
  const collectedIds = uniqueSorted([
633
909
  ...directCandidates.map((candidate) => candidate.qualifiedId ?? candidate.id),
634
910
  ...(kind === 'dimension' ? eligibleCandidates.flatMap((candidate) => (candidate.dimensions ?? []).filter((dimension) => {
@@ -667,12 +943,29 @@ function bindRequestedMember(requested, candidates, kind, question = '') {
667
943
  };
668
944
  }
669
945
  }
946
+ if (ids.length === 0 && (kind === 'dimension' || kind === 'output') && eligibleCandidates.length === 1) {
947
+ const certified = eligibleCandidates[0];
948
+ const outputName = certifiedCandidateDeclaredDimensionOutput(certified, requested);
949
+ if (certified.kind === 'certified_block'
950
+ && certified.compatibility === 'compatible'
951
+ && outputName) {
952
+ const id = certified.qualifiedId ?? certified.id;
953
+ return {
954
+ requested,
955
+ qualifiedId: id,
956
+ outputName,
957
+ status: 'resolved',
958
+ candidateIds: [id],
959
+ };
960
+ }
961
+ }
670
962
  const namedWinner = memberNamedInQuestion(ids, eligibleCandidates, question);
671
963
  if (namedWinner) {
672
964
  const winnerCandidate = directCandidates.find((candidate) => (candidate.qualifiedId ?? candidate.id) === namedWinner);
673
965
  return {
674
966
  requested,
675
967
  qualifiedId: namedWinner,
968
+ ...(kind === 'output' ? { outputName: outputNameForCandidate(winnerCandidate, namedWinner) } : {}),
676
969
  ...(winnerCandidate?.aggregation ? { aggregation: winnerCandidate.aggregation } : {}),
677
970
  status: 'resolved',
678
971
  candidateIds: [namedWinner],
@@ -684,11 +977,85 @@ function bindRequestedMember(requested, candidates, kind, question = '') {
684
977
  return {
685
978
  requested,
686
979
  ...(ids.length === 1 ? { qualifiedId: ids[0] } : {}),
980
+ ...(kind === 'output' && ids.length === 1
981
+ ? { outputName: outputNameForCandidate(direct, ids[0]) }
982
+ : {}),
687
983
  ...(direct?.aggregation ? { aggregation: direct.aggregation } : {}),
688
984
  status: ids.length === 1 ? 'resolved' : ids.length > 1 ? 'ambiguous' : 'unresolved',
689
985
  candidateIds: ids,
690
986
  };
691
987
  }
988
+ /**
989
+ * Return the exact host-authoritative output candidates for a requested
990
+ * projection. Physical columns win over descriptive semantic aliases: when a
991
+ * selected closure contains `order_items.order_id`, a semantic entity named
992
+ * `order_id` is useful retrieval context but is not a competing SQL output.
993
+ */
994
+ function exactOutputBindingCandidates(candidates, requested) {
995
+ const exactPhysical = candidates.filter((candidate) => physicalOutputColumnLeaf(candidate) === requested);
996
+ if (exactPhysical.length > 0)
997
+ return uniqueOutputBindingCandidates(exactPhysical);
998
+ // Semantic projections do not always carry a physical column identity. They
999
+ // may still bind if their declared name itself is exact; aliases alone never
1000
+ // have authority to substitute one requested identifier for another.
1001
+ return uniqueOutputBindingCandidates(candidates.filter((candidate) => normalize(candidate.name) === requested));
1002
+ }
1003
+ /**
1004
+ * A requested `... id` / `... identifier` must not fall back to lexical alias
1005
+ * matching. In particular, an alias attached to product_id cannot make it a
1006
+ * candidate for order_id when no actual order_id is selected.
1007
+ */
1008
+ function isExplicitOutputIdentity(requested) {
1009
+ return /(?:^| )(?:id|identifier)$/.test(requested);
1010
+ }
1011
+ function physicalOutputColumnLeaf(candidate) {
1012
+ if (candidate.kind !== 'sql_column')
1013
+ return undefined;
1014
+ const raw = candidate.qualifiedId ?? candidate.id;
1015
+ const columnReference = raw.replace(/^(?:dbt|runtime):column:/i, '');
1016
+ const leaf = columnReference.split('.').at(-1);
1017
+ const normalized = normalize(leaf ?? '');
1018
+ return normalized || undefined;
1019
+ }
1020
+ /**
1021
+ * Candidate IDs can be emitted once by the dbt manifest and again by runtime
1022
+ * schema/index lanes. They describe one output only when their canonical
1023
+ * physical relation.column is identical. Do not dedupe two relations that
1024
+ * merely share the same leaf: that remains a real ambiguity.
1025
+ */
1026
+ function uniqueOutputBindingCandidates(candidates) {
1027
+ const seen = new Set();
1028
+ return candidates.filter((candidate) => {
1029
+ const identity = canonicalOutputBindingIdentity(candidate);
1030
+ if (seen.has(identity))
1031
+ return false;
1032
+ seen.add(identity);
1033
+ return true;
1034
+ });
1035
+ }
1036
+ function canonicalOutputBindingIdentity(candidate) {
1037
+ if (candidate.kind === 'sql_column') {
1038
+ const raw = candidate.qualifiedId ?? candidate.id;
1039
+ const columnReference = raw.replace(/^(?:dbt|runtime):column:/i, '');
1040
+ if (columnReference.includes('.'))
1041
+ return `physical:${normalize(columnReference)}`;
1042
+ }
1043
+ return `candidate:${candidate.qualifiedId ?? candidate.id}`;
1044
+ }
1045
+ /**
1046
+ * A selected physical/semantic output's alias is part of the frozen result
1047
+ * contract. Prefer the inspected candidate name; a qualified-id leaf is a
1048
+ * safe fallback for machine-authored dbt/sql column identities.
1049
+ */
1050
+ function outputNameForCandidate(candidate, qualifiedId) {
1051
+ const candidateName = candidate?.name?.trim();
1052
+ if (candidateName && /^[A-Za-z_][A-Za-z0-9_]*$/.test(candidateName))
1053
+ return candidateName;
1054
+ const leaf = qualifiedId.split(/[:.]/).at(-1)?.replace(/[^A-Za-z0-9_]/g, '_');
1055
+ return leaf && /^[A-Za-z_][A-Za-z0-9_]*$/.test(leaf)
1056
+ ? leaf
1057
+ : normalize(candidateName || qualifiedId).replace(/\s+/g, '_');
1058
+ }
692
1059
  function qualifyDeclaredDimension(candidate, dimension) {
693
1060
  if (/[:./]/.test(dimension) || !candidate.domain)
694
1061
  return dimension;
@@ -696,9 +1063,6 @@ function qualifyDeclaredDimension(candidate, dimension) {
696
1063
  const local = dimension.toLowerCase().replace(/[^a-z0-9_-]+/g, '_');
697
1064
  return `semantic:${domain}:dimension:${local}`;
698
1065
  }
699
- function resolutionUsesRelationalEvidence(resolution) {
700
- return resolution.recommendedRoute === 'governed_sql' || resolution.recommendedRoute === 'exploratory';
701
- }
702
1066
  function memberTermMatches(candidateTerm, requested) {
703
1067
  if (candidateTerm === requested || candidateTerm.endsWith(` ${requested}`))
704
1068
  return true;
@@ -711,7 +1075,7 @@ function candidateTerms(candidate) {
711
1075
  .map(normalize)
712
1076
  .filter(Boolean);
713
1077
  }
714
- function resolveCapability(resolution, execution, measures, dimensions, filters) {
1078
+ function resolveCapability(resolution, execution, measures, dimensions, filters, requiredOutputs = [], selectedCapability, frozenAuthorityCandidates = [], snapshotCandidates = []) {
715
1079
  if (resolution.confidence === "low" ||
716
1080
  resolution.recommendedRoute === "clarify" ||
717
1081
  !execution ||
@@ -728,24 +1092,329 @@ function resolveCapability(resolution, execution, measures, dimensions, filters)
728
1092
  (resolution.recommendedRoute === "certified" ||
729
1093
  resolution.recommendedRoute === "semantic"))
730
1094
  return "blocked";
1095
+ // A frozen plan may not silently drop an explicitly requested projection.
1096
+ // This applies to exploratory SQL too: otherwise a query can execute and
1097
+ // display five rows that omit one of the user-named identifiers.
1098
+ if (requiredOutputs.some((binding) => binding.status !== 'resolved'))
1099
+ return 'blocked';
731
1100
  if (execution.compatibility === "incompatible")
732
1101
  return "blocked";
733
1102
  if (resolution.recommendedRoute === "certified" &&
734
1103
  execution.kind === "certified_block" &&
735
1104
  execution.compatibility === "compatible" &&
736
- certifiedCandidateExplicitlyCoversMeasures(execution, measures.map((binding) => binding.requested))) {
1105
+ certifiedCandidateExplicitlyCoversMeasures(execution, measures.map((binding) => binding.requested)) &&
1106
+ certifiedBlockProvesHostProjection(execution, resolution)) {
737
1107
  return "certified_execution";
738
1108
  }
739
1109
  if (resolution.recommendedRoute === 'semantic'
740
1110
  && (execution.kind === 'semantic_metric' || execution.kind === 'semantic_member')
741
- && execution.compatibility === 'compatible')
1111
+ && semanticCapabilitiesProveFrozenTuple({
1112
+ capability: selectedCapability,
1113
+ measures,
1114
+ dimensions,
1115
+ filters,
1116
+ requiredOutputs,
1117
+ timeGrain: resolution.queryIntent.timeGrain,
1118
+ hasTimeRange: Boolean(resolution.queryIntent.timeRange),
1119
+ candidates: frozenAuthorityCandidates,
1120
+ })
1121
+ && (execution.compatibility === 'compatible'
1122
+ || sameSnapshotSemanticExtensionProvesFrozenTuple({
1123
+ execution,
1124
+ capability: selectedCapability,
1125
+ measures,
1126
+ dimensions,
1127
+ filters,
1128
+ requiredOutputs,
1129
+ candidates: frozenAuthorityCandidates,
1130
+ timeGrain: resolution.queryIntent.timeGrain,
1131
+ hasTimeRange: Boolean(resolution.queryIntent.timeRange),
1132
+ })))
742
1133
  return 'semantic_execution';
743
- if (resolution.recommendedRoute === 'governed_sql')
1134
+ // A relationship/entity card and a qualified physical closure are useful
1135
+ // evidence for exploratory SQL, but they are not themselves a governed DQL
1136
+ // projection. In particular, a DQL entity may bridge an order-items model
1137
+ // to related business context while the selected tuple still consists only
1138
+ // of dbt/runtime columns. Freezing that as governed relational defers the
1139
+ // first real eligibility check until the compiler, where it becomes a
1140
+ // post-freeze COMPILATION_FAILED with no permitted fallback.
1141
+ //
1142
+ // Keep the boundary before freeze: governed relational execution requires a
1143
+ // snapshot-declared capability with a concrete governed adapter and proof
1144
+ // for every bound measure/dimension/filter/output. A raw physical path is
1145
+ // deliberately returned as blocked here so the router can evaluate its
1146
+ // existing same-snapshot exploratory cascade, which rebuilds an immutable
1147
+ // review-required plan with `recommendedRoute: exploratory`.
1148
+ if (resolution.recommendedRoute === 'governed_sql'
1149
+ // A partial DQL modeling card can contribute relationship context, but it
1150
+ // cannot be the compiler authority for a frozen governed plan. In
1151
+ // particular, an entity card that merely names an order-item grain must
1152
+ // not turn selected dbt/runtime output columns into a governed DQL
1153
+ // projection. It remains eligible evidence for the router's safe
1154
+ // same-snapshot exploratory cascade below this pre-freeze gate.
1155
+ && execution.compatibility === 'compatible'
1156
+ && governedRelationalCapabilityProvesFrozenTuple({
1157
+ capability: selectedCapability,
1158
+ measures,
1159
+ dimensions,
1160
+ filters,
1161
+ requiredOutputs,
1162
+ timeGrain: resolution.queryIntent.timeGrain,
1163
+ hasTimeRange: Boolean(resolution.queryIntent.timeRange),
1164
+ snapshotCandidates,
1165
+ }))
744
1166
  return 'governed_relational';
745
1167
  if (resolution.recommendedRoute === 'exploratory')
746
1168
  return 'bounded_exploration';
747
1169
  return 'blocked';
748
1170
  }
1171
+ /**
1172
+ * Governed relational compilation has a distinct authority boundary from
1173
+ * physical SQL safety. The latter can prove a read-only, review-required
1174
+ * closure; it cannot mint a governed compiler projection. Require the
1175
+ * selected capability to declare the governed adapter and to prove the exact
1176
+ * frozen tuple before the router is allowed to freeze this tier.
1177
+ */
1178
+ function governedRelationalCapabilityProvesFrozenTuple(input) {
1179
+ const { capability } = input;
1180
+ if (!capability)
1181
+ return false;
1182
+ if (!capability.executionCapabilities.some((execution) => execution.route === 'governed_sql' && Boolean(execution.adapterId?.trim())))
1183
+ return false;
1184
+ const metricIds = new Set([capability.metricId, ...capability.measureIds]);
1185
+ if (!input.measures.every((binding) => binding.status === 'resolved'
1186
+ && Boolean(binding.qualifiedId)
1187
+ && metricIds.has(binding.qualifiedId)))
1188
+ return false;
1189
+ const provesDimension = (binding, roles) => {
1190
+ if (binding.status !== 'resolved' || !binding.qualifiedId)
1191
+ return false;
1192
+ const dimension = eligibleCapabilityDimensions(capability, roles)
1193
+ .find((candidate) => candidate.dimensionId === binding.qualifiedId);
1194
+ return Boolean(dimension
1195
+ && governedCapabilityDimensionHasFreshAutomaticRelationshipProofV1({
1196
+ capability,
1197
+ dimension,
1198
+ candidates: input.snapshotCandidates,
1199
+ }));
1200
+ };
1201
+ if (!input.dimensions.every((binding) => provesDimension(binding, ['group_by'])))
1202
+ return false;
1203
+ if (!input.filters.every((filter) => provesDimension(filter.binding, ['filter'])))
1204
+ return false;
1205
+ // An explicit output is a host-owned result-contract field, not an
1206
+ // implication of a physical relation. A `declaredOutputIds` entry is not a
1207
+ // compiler projection on its own: the governed compiler consumes metrics
1208
+ // and grouped dimensions. Therefore a non-measure output must be the exact
1209
+ // selected capability display/rank dimension *and* already be represented
1210
+ // in the frozen grouped tuple. This keeps raw `order_id` / `product_id`
1211
+ // columns and detached cross-entity display fields on the exploratory path
1212
+ // unless the authored governed graph can actually compile them.
1213
+ // Metrics follow the same rule: capability membership describes what could
1214
+ // be compiled, while the frozen query measures describe what *will* be
1215
+ // projected. A required `gross_profit` output may not piggyback on a
1216
+ // selected `revenue` measure just because both live in the capability.
1217
+ const groupedDimensionIds = new Set(input.dimensions
1218
+ .flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : []));
1219
+ const projectedMeasureIds = new Set(input.measures
1220
+ .filter((binding) => binding.status === 'resolved')
1221
+ .flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : []));
1222
+ const provesOutput = (binding) => {
1223
+ if (binding.status !== 'resolved' || !binding.qualifiedId)
1224
+ return false;
1225
+ if (metricIds.has(binding.qualifiedId))
1226
+ return projectedMeasureIds.has(binding.qualifiedId);
1227
+ if (!groupedDimensionIds.has(binding.qualifiedId))
1228
+ return false;
1229
+ return provesDimension(binding, ['display'])
1230
+ || provesDimension(binding, ['rank_entity']);
1231
+ };
1232
+ if (!input.requiredOutputs.every(provesOutput))
1233
+ return false;
1234
+ if (input.timeGrain && !capability.timeDimensions.some((dimension) => dimension.supportedGrains.some((grain) => normalize(grain) === normalize(input.timeGrain))))
1235
+ return false;
1236
+ return !input.hasTimeRange || capability.timeDimensions.length > 0;
1237
+ }
1238
+ /**
1239
+ * A selected certified block must prove the host-owned projection itself.
1240
+ * Contextual semantic capability metadata is useful corroboration, but it is
1241
+ * never authority to add an entity label, grouping field, or explicit output
1242
+ * that the block does not declare. This defensive freeze gate covers paths
1243
+ * where a binding was collapsed/projected before capability resolution.
1244
+ */
1245
+ function certifiedBlockProvesHostProjection(block, resolution) {
1246
+ const seed = resolution.hostRequirementSeed;
1247
+ if (!seed)
1248
+ return true;
1249
+ const projectedDimensions = certifiedBlockCanUseExactCanonicalDimensionProjection(block, resolution)
1250
+ ? resolution.queryIntent.dimensions
1251
+ : seed.queryIntent.dimensions;
1252
+ const terms = uniqueSorted([
1253
+ ...projectedDimensions,
1254
+ ...seed.requirements.entityDisplayTerms,
1255
+ ...(seed.requirements.outputTerms ?? []),
1256
+ ].map(normalize).filter(Boolean));
1257
+ return terms.every((term) => Boolean(certifiedCandidateDeclaredDimensionOutput(block, term)
1258
+ || certifiedCandidateDeclaredMeasureOutput(block, term)));
1259
+ }
1260
+ function certifiedBlockCanUseExactCanonicalDimensionProjection(block, resolution) {
1261
+ const seed = resolution.hostRequirementSeed;
1262
+ return Boolean(seed
1263
+ && resolution.recommendedRoute === 'certified'
1264
+ && block?.kind === 'certified_block'
1265
+ && block.exactMatch
1266
+ && block.compatibility === 'compatible'
1267
+ && block.analyticalFitClass === 'exact'
1268
+ && certifiedCandidateExplicitlyCoversMeasures(block, resolution.queryIntent.measures)
1269
+ && resolution.queryIntent.dimensions.length > 0
1270
+ && resolution.queryIntent.dimensions.every((dimension) => Boolean(certifiedCandidateDeclaredDimensionOutput(block, dimension)))
1271
+ && !sameNormalizedTerms(resolution.queryIntent.dimensions, seed.queryIntent.dimensions));
1272
+ }
1273
+ /**
1274
+ * The semantic route may freeze only after the exact frozen tuple can be
1275
+ * expressed by the selected semantic adapter. A metric-relative native
1276
+ * grouping reference is required for every cross-model dimension/filter.
1277
+ * Same-model dimensions are already a declared group-by on the metric model.
1278
+ */
1279
+ function semanticCapabilityProvesFrozenTuple(input) {
1280
+ const { capability } = input;
1281
+ // Metric-free semantic member lookups retain their existing exact registry
1282
+ // path. A metric tuple, however, must have a normalized capability.
1283
+ if (!capability)
1284
+ return input.measures.length === 0;
1285
+ if (!capability.executionCapabilities.some((execution) => execution.route === 'semantic' && Boolean(execution.adapterId?.trim())))
1286
+ return false;
1287
+ const metricIds = new Set([capability.metricId, ...capability.measureIds]);
1288
+ if (!input.measures.every((binding) => binding.status === 'resolved' && Boolean(binding.qualifiedId) && metricIds.has(binding.qualifiedId)))
1289
+ return false;
1290
+ const provesDimension = (binding, roles) => {
1291
+ if (binding.status !== 'resolved' || !binding.qualifiedId)
1292
+ return false;
1293
+ const dimension = capability.dimensions.find((candidate) => candidate.dimensionId === binding.qualifiedId
1294
+ && roles.every((role) => candidate.supportedRoles.includes(role)));
1295
+ return Boolean(dimension && (dimension.entityId === capability.primaryEntityId
1296
+ || semanticNativeGroupingIsExact(capability, dimension)));
1297
+ };
1298
+ if (!input.dimensions.every((binding) => provesDimension(binding, ['group_by'])))
1299
+ return false;
1300
+ if (!input.filters.every((filter) => provesDimension(filter.binding, ['filter'])))
1301
+ return false;
1302
+ // Explicit output projections are not grouping dimensions. They have their
1303
+ // own frozen result contract and cannot be smuggled into a semantic tuple by
1304
+ // a same-snapshot extension.
1305
+ if (input.requiredOutputs.length > 0)
1306
+ return false;
1307
+ if (input.timeGrain && !capability.timeDimensions.some((dimension) => dimension.supportedGrains.some((grain) => normalize(grain) === normalize(input.timeGrain))))
1308
+ return false;
1309
+ return !input.hasTimeRange || capability.timeDimensions.length > 0;
1310
+ }
1311
+ /**
1312
+ * A multi-metric semantic plan remains one immutable request frame, but each
1313
+ * selected metric has to prove that frame independently before the route can
1314
+ * freeze. The first selected capability remains the stable execution anchor
1315
+ * for backward-compatible receipts; it is never allowed to stand in for a
1316
+ * second requested metric. This closes the old single-capability shortcut
1317
+ * where a multi-metric frame was marked blocked (or, worse, could later be
1318
+ * compiled against only its first measure).
1319
+ */
1320
+ function semanticCapabilitiesProveFrozenTuple(input) {
1321
+ const resolvedMeasures = input.measures.filter((binding) => binding.status === 'resolved' && Boolean(binding.qualifiedId));
1322
+ if (resolvedMeasures.length <= 1) {
1323
+ return semanticCapabilityProvesFrozenTuple(input);
1324
+ }
1325
+ // Every requested metric must have a selected normalized capability. The
1326
+ // identity match is exact; aliases, matching leaves, or a correlated metric
1327
+ // are not enough to enlarge the frozen tuple.
1328
+ const capabilities = uniqueMetricCapabilities([
1329
+ input.capability,
1330
+ ...input.candidates.map((candidate) => normalizeMetricCapabilityContract(candidate.analyticalCapability)),
1331
+ ]);
1332
+ const selected = resolvedMeasures.map((measure) => {
1333
+ const matches = capabilities.filter((capability) => capability.metricId === measure.qualifiedId
1334
+ || capability.measureIds.includes(measure.qualifiedId));
1335
+ return matches.length === 1 ? { measure, capability: matches[0] } : undefined;
1336
+ });
1337
+ if (selected.some((entry) => !entry))
1338
+ return false;
1339
+ const entries = selected;
1340
+ const semanticRoutes = entries.map((entry) => entry.capability.executionCapabilities
1341
+ .filter((route) => route.route === 'semantic' && Boolean(route.adapterId?.trim())));
1342
+ if (semanticRoutes.some((routes) => routes.length !== 1))
1343
+ return false;
1344
+ const adapterId = semanticRoutes[0][0].adapterId;
1345
+ if (!entries.every((entry, index) => semanticRoutes[index][0].adapterId === adapterId
1346
+ && entry.capability.semanticModelId === entries[0].capability.semanticModelId))
1347
+ return false;
1348
+ return entries.every(({ measure, capability }) => semanticCapabilityProvesFrozenTuple({
1349
+ capability,
1350
+ measures: [measure],
1351
+ dimensions: input.dimensions,
1352
+ filters: input.filters,
1353
+ requiredOutputs: input.requiredOutputs,
1354
+ ...(input.timeGrain ? { timeGrain: input.timeGrain } : {}),
1355
+ hasTimeRange: input.hasTimeRange,
1356
+ }));
1357
+ }
1358
+ function uniqueMetricCapabilities(capabilities) {
1359
+ const byFingerprint = new Map();
1360
+ for (const capability of capabilities) {
1361
+ if (!capability)
1362
+ continue;
1363
+ // The source fingerprint is the snapshot identity, not a metric identity:
1364
+ // one semantic model can legitimately expose several requested metrics
1365
+ // from the same immutable snapshot. A duplicate of the *same* metric can
1366
+ // appear as selected execution and selected evidence, but must not turn
1367
+ // that metric into an ambiguous capability choice.
1368
+ byFingerprint.set(`${capability.sourceFingerprint}:${capability.metricId}`, capability);
1369
+ }
1370
+ return [...byFingerprint.values()];
1371
+ }
1372
+ /**
1373
+ * Retrieval cards are scored before the role-balanced same-snapshot extension
1374
+ * is admitted. A semantic metric can therefore retain `partial` even after
1375
+ * the host has selected its one proven MetricFlow grouping field. Do not make
1376
+ * `partial` generally executable: advance it only when the immutable frozen
1377
+ * tuple proves the exact metric, every required role, and the extension's
1378
+ * metric/dimension identity from the same snapshot.
1379
+ *
1380
+ * This is semantic-adapter authority only. It never authorizes a physical join
1381
+ * or exploratory SQL, and an omitted output, unbound role, mismatched metric,
1382
+ * mismatched dimension, unsupported time grain, or absent extension remains
1383
+ * blocked for the cascade to evaluate safely.
1384
+ */
1385
+ function sameSnapshotSemanticExtensionProvesFrozenTuple(input) {
1386
+ const { execution, capability } = input;
1387
+ if (execution.kind !== 'semantic_metric' || execution.compatibility !== 'partial' || !capability)
1388
+ return false;
1389
+ if (!semanticCapabilityProvesFrozenTuple(input))
1390
+ return false;
1391
+ // Explicit output terms have their own source-column result proof. A
1392
+ // semantic role extension does not prove such an extra projection.
1393
+ if (input.requiredOutputs.length > 0 || input.measures.length === 0)
1394
+ return false;
1395
+ const metricAuthorityIds = new Set([
1396
+ execution.id,
1397
+ execution.qualifiedId,
1398
+ capability.metricId,
1399
+ ...capability.measureIds,
1400
+ ].filter((id) => Boolean(id)));
1401
+ if (!input.measures.every((binding) => binding.status === 'resolved'
1402
+ && Boolean(binding.qualifiedId)
1403
+ && metricAuthorityIds.has(binding.qualifiedId)))
1404
+ return false;
1405
+ return input.candidates.some((candidate) => {
1406
+ const extension = candidate.sameSnapshotRoleExtension;
1407
+ if (!extension
1408
+ || extension.version !== 1
1409
+ || extension.role !== 'categorical_dimension'
1410
+ || (extension.basis !== 'sole_metricflow_grouping_dimension'
1411
+ && extension.basis !== 'exact_metricflow_grouping_dimension')
1412
+ || !metricAuthorityIds.has(extension.metricId)
1413
+ || (candidate.qualifiedId ?? candidate.id) !== extension.dimensionId)
1414
+ return false;
1415
+ return input.dimensions.some((binding) => binding.qualifiedId === extension.dimensionId);
1416
+ });
1417
+ }
749
1418
  function normalize(value) {
750
1419
  return value.toLowerCase()
751
1420
  .replace(/%/g, ' percentage ')
@@ -754,6 +1423,40 @@ function normalize(value) {
754
1423
  .replace(/\s+/g, ' ')
755
1424
  .trim();
756
1425
  }
1426
+ /**
1427
+ * Resolve a host-owned display-key requirement against a capability that uses
1428
+ * the generic entity field as its declared display output. This is a
1429
+ * deterministic capability projection, not an interpretation of the user
1430
+ * question and never mutates the requirement seed.
1431
+ */
1432
+ function collapseHostEntityDisplayBindings(bindings, seed) {
1433
+ if (!seed)
1434
+ return bindings;
1435
+ const displayOnlyTerms = new Set(seed.requirements.entityDisplayTerms
1436
+ .map(normalize)
1437
+ .filter((term) => term.length > 0 && !seed.requirements.dimensions.some((dimension) => normalize(dimension) === term)));
1438
+ const entityTerms = seed.requirements.entityTerms.map(normalize).filter(Boolean);
1439
+ if (displayOnlyTerms.size === 0 || entityTerms.length === 0)
1440
+ return bindings;
1441
+ const isEntityCompanion = (binding) => {
1442
+ const requested = normalize(binding.requested);
1443
+ return entityTerms.some((entity) => requested === entity || requested.startsWith(`${entity} `));
1444
+ };
1445
+ return bindings.filter((binding, index, all) => {
1446
+ const requested = normalize(binding.requested);
1447
+ if (!displayOnlyTerms.has(requested))
1448
+ return true;
1449
+ const companion = all.find((other, otherIndex) => otherIndex !== index && isEntityCompanion(other));
1450
+ // An ambiguous generic entity is still the sole decision the reader must
1451
+ // make. Do not add an unrelated "account name" unresolved gap beside the
1452
+ // two qualified account choices.
1453
+ if (!companion || companion.status === 'unresolved')
1454
+ return true;
1455
+ if (binding.status === 'unresolved')
1456
+ return false;
1457
+ return binding.status !== 'resolved' || binding.qualifiedId !== companion.qualifiedId;
1458
+ });
1459
+ }
757
1460
  /** Resolve common analytical ranges once so no executor reinterprets "last month". */
758
1461
  export function resolvePlanTimeRange(expression, referenceTime) {
759
1462
  const text = expression.trim().toLowerCase();
@@ -819,6 +1522,20 @@ function addUnits(value, unit, count) {
819
1522
  function uniqueSorted(values) {
820
1523
  return [...new Set(values.filter(Boolean))].sort();
821
1524
  }
1525
+ function sameNormalizedTerms(left, right) {
1526
+ const normalizeTerms = (values) => uniqueSorted(values.map(normalize));
1527
+ return normalizeTerms(left).join('\u0000') === normalizeTerms(right).join('\u0000');
1528
+ }
1529
+ function uniqueCandidatesByCanonicalId(candidates, canonicalId) {
1530
+ const seen = new Set();
1531
+ return candidates.filter((candidate) => {
1532
+ const id = canonicalId(candidate);
1533
+ if (!id || seen.has(id))
1534
+ return false;
1535
+ seen.add(id);
1536
+ return true;
1537
+ });
1538
+ }
822
1539
  function sha256(value) {
823
1540
  return createHash('sha256').update(value).digest('hex');
824
1541
  }