@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.
- package/dist/agent-run-engine.d.ts +63 -2
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +864 -29
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-planner.d.ts +6 -0
- package/dist/agent-run-planner.d.ts.map +1 -1
- package/dist/agent-run-planner.js +2 -0
- package/dist/agent-run-planner.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +48 -3
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +7 -1
- package/dist/agentic/analyst-loop.d.ts.map +1 -1
- package/dist/agentic/analyst-loop.js +163 -0
- package/dist/agentic/analyst-loop.js.map +1 -1
- package/dist/agentic/assumptions.d.ts +1 -1
- package/dist/agentic/assumptions.d.ts.map +1 -1
- package/dist/agentic/assumptions.js.map +1 -1
- package/dist/agentic/research-agent.d.ts.map +1 -1
- package/dist/agentic/research-agent.js +10 -1
- package/dist/agentic/research-agent.js.map +1 -1
- package/dist/agentic/sql-authorization.d.ts +8 -0
- package/dist/agentic/sql-authorization.d.ts.map +1 -1
- package/dist/agentic/sql-authorization.js +3 -0
- package/dist/agentic/sql-authorization.js.map +1 -1
- package/dist/analytical-compatibility.d.ts.map +1 -1
- package/dist/analytical-compatibility.js +8 -1
- package/dist/analytical-compatibility.js.map +1 -1
- package/dist/analytical-failure-repair.d.ts.map +1 -1
- package/dist/analytical-failure-repair.js +6 -2
- package/dist/analytical-failure-repair.js.map +1 -1
- package/dist/analytical-frame.d.ts +23 -1
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +322 -16
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +314 -3
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +486 -43
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +64 -8
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +528 -67
- package/dist/answer-loop.js.map +1 -1
- package/dist/ask-observability/index.d.ts +11 -0
- package/dist/ask-observability/index.d.ts.map +1 -0
- package/dist/ask-observability/index.js +7 -0
- package/dist/ask-observability/index.js.map +1 -0
- package/dist/ask-observability/instrumentation.d.ts +35 -0
- package/dist/ask-observability/instrumentation.d.ts.map +1 -0
- package/dist/ask-observability/instrumentation.js +472 -0
- package/dist/ask-observability/instrumentation.js.map +1 -0
- package/dist/ask-observability/observer.d.ts +62 -0
- package/dist/ask-observability/observer.d.ts.map +1 -0
- package/dist/ask-observability/observer.js +537 -0
- package/dist/ask-observability/observer.js.map +1 -0
- package/dist/ask-observability/portable.d.ts +98 -0
- package/dist/ask-observability/portable.d.ts.map +1 -0
- package/dist/ask-observability/portable.js +527 -0
- package/dist/ask-observability/portable.js.map +1 -0
- package/dist/ask-observability/store.d.ts +130 -0
- package/dist/ask-observability/store.d.ts.map +1 -0
- package/dist/ask-observability/store.js +1114 -0
- package/dist/ask-observability/store.js.map +1 -0
- package/dist/ask-observability/types.d.ts +357 -0
- package/dist/ask-observability/types.d.ts.map +1 -0
- package/dist/ask-observability/types.js +2 -0
- package/dist/ask-observability/types.js.map +1 -0
- package/dist/ask-observability/utils.d.ts +21 -0
- package/dist/ask-observability/utils.d.ts.map +1 -0
- package/dist/ask-observability/utils.js +102 -0
- package/dist/ask-observability/utils.js.map +1 -0
- package/dist/generated-analytical-proposal.d.ts +37 -1
- package/dist/generated-analytical-proposal.d.ts.map +1 -1
- package/dist/generated-analytical-proposal.js +211 -3
- package/dist/generated-analytical-proposal.js.map +1 -1
- package/dist/governed-relational-compiler.d.ts +15 -0
- package/dist/governed-relational-compiler.d.ts.map +1 -1
- package/dist/governed-relational-compiler.js +99 -1
- package/dist/governed-relational-compiler.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +10 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/meaning-resolution.d.ts +78 -6
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +331 -38
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +21 -1
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +1 -1
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +9 -2
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +261 -11
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/plan-execution-adapter.d.ts +9 -1
- package/dist/plan-execution-adapter.d.ts.map +1 -1
- package/dist/plan-execution-adapter.js +93 -13
- package/dist/plan-execution-adapter.js.map +1 -1
- package/dist/provider-egress.d.ts +24 -10
- package/dist/provider-egress.d.ts.map +1 -1
- package/dist/provider-egress.js +22 -17
- package/dist/provider-egress.js.map +1 -1
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +10 -9
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/dispatch.d.ts +27 -1
- package/dist/providers/dispatch.d.ts.map +1 -1
- package/dist/providers/dispatch.js +63 -1
- package/dist/providers/dispatch.js.map +1 -1
- package/dist/providers/gemini.js +10 -10
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.js +15 -15
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.js +22 -22
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +52 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/relationship-proof.d.ts +81 -0
- package/dist/relationship-proof.d.ts.map +1 -1
- package/dist/relationship-proof.js +228 -4
- package/dist/relationship-proof.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +18 -4
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +764 -47
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +10 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1785 -213
- package/dist/router.js.map +1 -1
- package/dist/semantic-bridge/compose.d.ts.map +1 -1
- package/dist/semantic-bridge/compose.js +23 -4
- package/dist/semantic-bridge/compose.js.map +1 -1
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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.
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
882
|
+
const kindCandidates = eligibleCandidates.filter((candidate) => {
|
|
626
883
|
const kindMatches = kind === 'measure'
|
|
627
884
|
? candidate.kind === 'semantic_metric' || candidate.kind === 'sql_column'
|
|
628
|
-
:
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
&&
|
|
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
|
-
|
|
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
|
}
|