@duckcodeailabs/dql-agent 1.14.1 → 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 +71 -2
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +1048 -31
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +12 -0
- package/dist/agent-run-gates.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 +211 -28
- 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/synthesis.d.ts +4 -0
- package/dist/agentic/research/synthesis.d.ts.map +1 -1
- package/dist/agentic/research/synthesis.js +23 -8
- package/dist/agentic/research/synthesis.js.map +1 -1
- package/dist/agentic/research-agent.d.ts.map +1 -1
- package/dist/agentic/research-agent.js +13 -3
- 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 +233 -2
- 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 +332 -17
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +613 -0
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +1007 -0
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +110 -6
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +1180 -125
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +32 -1
- package/dist/answer-shape.d.ts.map +1 -1
- package/dist/answer-shape.js +49 -3
- package/dist/answer-shape.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/conversation/snapshot.d.ts +20 -0
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +30 -1
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/fixtures/ask-ai-office-shaped.d.ts +153 -0
- package/dist/fixtures/ask-ai-office-shaped.d.ts.map +1 -0
- package/dist/fixtures/ask-ai-office-shaped.js +94 -0
- package/dist/fixtures/ask-ai-office-shaped.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 +11 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +38 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +31 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/meaning-resolution.d.ts +141 -2
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +444 -30
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +144 -7
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/block-fit.d.ts +18 -0
- package/dist/metadata/block-fit.d.ts.map +1 -1
- package/dist/metadata/block-fit.js +250 -38
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +58 -12
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +19 -3
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +518 -54
- 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/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +23 -1
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +22 -1
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +773 -49
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +14 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +3668 -425
- 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/dist/semantic-bridge/member-select.d.ts.map +1 -1
- package/dist/semantic-bridge/member-select.js +39 -21
- package/dist/semantic-bridge/member-select.js.map +1 -1
- package/package.json +5 -5
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import { normalizeMetricCapabilityContract, } from "@duckcodeailabs/dql-core";
|
|
11
|
-
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';
|
|
12
14
|
export function buildResolvedAnalyticalPlan(input) {
|
|
13
15
|
const byLegacyId = new Map(input.candidates.map((candidate) => [candidate.id, candidate]));
|
|
14
16
|
const selectedCandidates = input.resolution.selectedConceptIds
|
|
@@ -21,7 +23,12 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
21
23
|
// complete tuple. Bind its declared inputs/outputs as one execution
|
|
22
24
|
// authority instead of mixing in the semantic concept used to select it.
|
|
23
25
|
? [executionCandidate]
|
|
24
|
-
|
|
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'
|
|
25
32
|
? input.candidates
|
|
26
33
|
: selectedCandidates.length > 0
|
|
27
34
|
? selectedCandidates
|
|
@@ -29,7 +36,6 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
29
36
|
? [executionCandidate]
|
|
30
37
|
: input.candidates;
|
|
31
38
|
const canonicalId = (candidate) => candidate.qualifiedId ?? candidate.id;
|
|
32
|
-
const selectedConceptIds = selectedCandidates.map(canonicalId);
|
|
33
39
|
const executionId = executionCandidate ? canonicalId(executionCandidate) : undefined;
|
|
34
40
|
const measures = input.resolution.queryIntent.measures.length > 0
|
|
35
41
|
? input.resolution.queryIntent.measures.map((requested) => bindRequestedMember(requested, bindingCandidates, 'measure', input.question))
|
|
@@ -49,7 +55,134 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
49
55
|
? input.candidates
|
|
50
56
|
: bindingCandidates;
|
|
51
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
|
+
};
|
|
52
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);
|
|
53
186
|
const capabilityBinding = bindSelectedCapabilityDimension(requested, selectedCapability, roles, input.candidates);
|
|
54
187
|
// A provider frame may describe a selected capability, but it cannot pick
|
|
55
188
|
// one of that capability's grouping dimensions for the host. Once a
|
|
@@ -65,10 +198,8 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
65
198
|
}
|
|
66
199
|
if (capabilityBinding && capabilityBinding.status !== 'unresolved')
|
|
67
200
|
return capabilityBinding;
|
|
68
|
-
const normalizedRequested = normalize(requested);
|
|
69
201
|
const frameIds = uniqueSorted((input.resolution.analyticalFrame?.dimensions ?? [])
|
|
70
202
|
.filter((binding) => capabilityEntailsFrameDimension(selectedCapability, binding)
|
|
71
|
-
&& (!selectedCapability || binding.role === 'time_axis')
|
|
72
203
|
&& memberTermMatches(normalize(binding.dimensionId), normalizedRequested))
|
|
73
204
|
.map((binding) => binding.dimensionId));
|
|
74
205
|
if (frameIds.length === 1) {
|
|
@@ -86,8 +217,68 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
86
217
|
const rankingRequested = input.resolution.questionType === 'ranking'
|
|
87
218
|
|| input.resolution.queryIntent.order !== undefined
|
|
88
219
|
|| input.resolution.queryIntent.limit !== undefined;
|
|
89
|
-
|
|
90
|
-
|
|
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);
|
|
91
282
|
// A provider may name the field it is grouping by and forget the MEMBER the
|
|
92
283
|
// reader asked about: "What customer type is Wesley Jenkins?" came back with
|
|
93
284
|
// `filters: []`, so the run returned all 200 customers and the narration
|
|
@@ -99,18 +290,52 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
99
290
|
// still win whenever it supplies any.
|
|
100
291
|
const requestedFilters = input.resolution.queryIntent.filters.length > 0
|
|
101
292
|
? input.resolution.queryIntent.filters
|
|
102
|
-
: (input.
|
|
293
|
+
: (input.resolution.hostRequirementSeed?.queryIntent.filters
|
|
294
|
+
?? currentQuestionGroundedParsedIntent(input.question, input.evidence.parsedIntent)?.filters
|
|
295
|
+
?? []).filter((filter) => filter.field && filter.value);
|
|
103
296
|
const filters = requestedFilters.map((filter) => ({
|
|
104
297
|
...filter,
|
|
105
298
|
binding: bindDimension(filter.field, ['filter']),
|
|
106
299
|
}));
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
111
321
|
? [executionCandidate]
|
|
112
|
-
:
|
|
113
|
-
]
|
|
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;
|
|
114
339
|
const compatibilityProof = proofCandidates.map((candidate) => ({
|
|
115
340
|
candidateId: canonicalId(candidate),
|
|
116
341
|
compatibility: candidate.compatibility,
|
|
@@ -121,8 +346,8 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
121
346
|
: []),
|
|
122
347
|
]),
|
|
123
348
|
}));
|
|
124
|
-
const capability = resolveCapability(input.resolution, executionCandidate, measures, dimensions, filters);
|
|
125
|
-
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);
|
|
126
351
|
// The reader named a member that no grounding lane could bind. Answering the
|
|
127
352
|
// question WITHOUT it silently changes what was asked — "what customer type
|
|
128
353
|
// is Wesley Jenkins" became "list every customer" — so the plan carries it as
|
|
@@ -158,6 +383,15 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
158
383
|
snapshotId,
|
|
159
384
|
})
|
|
160
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
|
+
: [];
|
|
161
395
|
const payload = {
|
|
162
396
|
schemaVersion: input.resolution.analyticalFrame
|
|
163
397
|
? 2
|
|
@@ -193,15 +427,17 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
193
427
|
entityGrain: selectedCapability
|
|
194
428
|
? resolvedCapabilityEntityGrain(selectedCapability, dimensions)
|
|
195
429
|
: input.resolution.analyticalFrame?.entityGrainIds[0] ?? executionCandidate?.primaryEntity,
|
|
196
|
-
sourceRelationIds: uniqueSorted(
|
|
430
|
+
sourceRelationIds: uniqueSorted(frozenAuthorityCandidates.flatMap((candidate) => candidate.sourceObjects ?? [])),
|
|
197
431
|
relationshipPathIds: selectedCapability
|
|
198
432
|
? uniqueSorted(relationshipProofs.flatMap((proof) => proof.kind === 'dql_relationship_path' ? proof.relationshipPathIds : []))
|
|
199
|
-
: uniqueSorted(
|
|
433
|
+
: uniqueSorted(frozenAuthorityCandidates.flatMap((candidate) => candidate.relationshipEvidence ?? [])),
|
|
200
434
|
relationshipProofs,
|
|
435
|
+
governedRelationshipSafetyProofs,
|
|
201
436
|
compatibilityProof,
|
|
202
437
|
outputContract: {
|
|
203
|
-
measures: uniqueSorted(measures.flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
|
|
204
|
-
dimensions: uniqueSorted(dimensions.flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
|
|
438
|
+
measures: uniqueSorted(measures.flatMap((binding) => binding.outputName ? [binding.outputName] : 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),
|
|
205
441
|
...(input.resolution.queryIntent.timeGrain
|
|
206
442
|
? { timeGrain: input.resolution.queryIntent.timeGrain }
|
|
207
443
|
: {}),
|
|
@@ -225,23 +461,23 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
225
461
|
outcome: input.resolution.compatibilityOutcome
|
|
226
462
|
?? (input.resolution.recommendedRoute === 'clarify' || input.resolution.confidence === 'low'
|
|
227
463
|
? 'clarify'
|
|
228
|
-
: bindingCandidateIds(measures, dimensions, filters).length > 0
|
|
464
|
+
: bindingCandidateIds(measures, dimensions, filters, requiredOutputs).length > 0
|
|
229
465
|
? 'clarify'
|
|
230
466
|
: 'modeling_gap'),
|
|
231
467
|
codes: uniqueSorted([
|
|
232
468
|
...(input.resolution.compatibilityFailures ?? []).map((failure) => failure.code),
|
|
233
|
-
...bindingFailureCodes(measures, dimensions, filters),
|
|
469
|
+
...bindingFailureCodes(measures, dimensions, filters, requiredOutputs),
|
|
234
470
|
]),
|
|
235
471
|
candidateIds: uniqueSorted([
|
|
236
472
|
...(input.resolution.compatibilityFailures ?? []).flatMap((failure) => failure.candidateIds),
|
|
237
|
-
...bindingCandidateIds(measures, dimensions, filters),
|
|
473
|
+
...bindingCandidateIds(measures, dimensions, filters, requiredOutputs),
|
|
238
474
|
]),
|
|
239
475
|
...(selectedCapability?.metricId
|
|
240
476
|
? { selectedCapabilityId: selectedCapability.metricId }
|
|
241
477
|
: {}),
|
|
242
478
|
...(executionId ? { selectedExecutionId: executionId } : {}),
|
|
243
479
|
finalCapability: capability,
|
|
244
|
-
bindings: unresolvedBindingReceipts(measures, dimensions, filters),
|
|
480
|
+
bindings: unresolvedBindingReceipts(measures, dimensions, filters, requiredOutputs),
|
|
245
481
|
},
|
|
246
482
|
}
|
|
247
483
|
: {}),
|
|
@@ -409,13 +645,31 @@ function eligibleCapabilityDimensions(capability, requiredRoles) {
|
|
|
409
645
|
if (!requiredRoles.every((role) => dimension.supportedRoles.includes(role)))
|
|
410
646
|
return false;
|
|
411
647
|
const sameEntity = dimension.entityId === capability.primaryEntityId;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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;
|
|
417
661
|
});
|
|
418
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
|
+
}
|
|
419
673
|
function capabilityEntailsFrameDimension(capability, binding) {
|
|
420
674
|
if (!capability)
|
|
421
675
|
return false;
|
|
@@ -460,11 +714,12 @@ function selectedCapabilityProofFacts(capability, dimensions) {
|
|
|
460
714
|
...capability.executionCapabilities.map((execution) => `capability:route:${execution.route}:${execution.adapterId ?? 'native'}`),
|
|
461
715
|
];
|
|
462
716
|
}
|
|
463
|
-
function unresolvedBindingReceipts(measures, dimensions, filters) {
|
|
717
|
+
function unresolvedBindingReceipts(measures, dimensions, filters, requiredOutputs = []) {
|
|
464
718
|
return [
|
|
465
719
|
...measures.map((binding) => ({ kind: 'measure', binding })),
|
|
466
720
|
...dimensions.map((binding) => ({ kind: 'dimension', binding })),
|
|
467
721
|
...filters.map((filter) => ({ kind: 'filter', binding: filter.binding })),
|
|
722
|
+
...requiredOutputs.map((binding) => ({ kind: 'output', binding })),
|
|
468
723
|
]
|
|
469
724
|
.filter(({ binding }) => binding.status !== 'resolved')
|
|
470
725
|
.map(({ kind, binding }) => ({
|
|
@@ -474,19 +729,20 @@ function unresolvedBindingReceipts(measures, dimensions, filters) {
|
|
|
474
729
|
candidateIds: [...binding.candidateIds],
|
|
475
730
|
}));
|
|
476
731
|
}
|
|
477
|
-
function bindingCandidateIds(measures, dimensions, filters) {
|
|
478
|
-
return unresolvedBindingReceipts(measures, dimensions, filters)
|
|
732
|
+
function bindingCandidateIds(measures, dimensions, filters, requiredOutputs = []) {
|
|
733
|
+
return unresolvedBindingReceipts(measures, dimensions, filters, requiredOutputs)
|
|
479
734
|
.flatMap((binding) => binding.candidateIds);
|
|
480
735
|
}
|
|
481
|
-
function bindingFailureCodes(measures, dimensions, filters) {
|
|
482
|
-
return unresolvedBindingReceipts(measures, dimensions, filters)
|
|
736
|
+
function bindingFailureCodes(measures, dimensions, filters, requiredOutputs = []) {
|
|
737
|
+
return unresolvedBindingReceipts(measures, dimensions, filters, requiredOutputs)
|
|
483
738
|
.map((binding) => `${binding.kind.toUpperCase()}_${binding.status.toUpperCase()}`);
|
|
484
739
|
}
|
|
485
|
-
function bindingMissingInformation(measures, dimensions, filters) {
|
|
740
|
+
function bindingMissingInformation(measures, dimensions, filters, requiredOutputs = []) {
|
|
486
741
|
return [
|
|
487
742
|
...measures.map((binding) => ({ kind: 'measure', binding })),
|
|
488
743
|
...dimensions.map((binding) => ({ kind: 'dimension', binding })),
|
|
489
744
|
...filters.map((filter) => ({ kind: 'filter', binding: filter.binding })),
|
|
745
|
+
...requiredOutputs.map((binding) => ({ kind: 'output', binding })),
|
|
490
746
|
].flatMap(({ kind, binding }) => {
|
|
491
747
|
if (binding.status === 'resolved')
|
|
492
748
|
return [];
|
|
@@ -522,6 +778,7 @@ export function deriveResolvedAnalyticalPlan(parent, delta) {
|
|
|
522
778
|
? (timeRange ? resolvePlanTimeRange(timeRange, delta.referenceTime ?? new Date()) : undefined)
|
|
523
779
|
: parent.query.timeBounds;
|
|
524
780
|
const unresolved = [...measures, ...dimensions, ...filters.map((filter) => filter.binding)]
|
|
781
|
+
.concat((parent.outputContract.requiredOutputs ?? []).map(cloneBinding))
|
|
525
782
|
.filter((binding) => binding.status !== 'resolved');
|
|
526
783
|
const payload = {
|
|
527
784
|
...parent,
|
|
@@ -542,8 +799,9 @@ export function deriveResolvedAnalyticalPlan(parent, delta) {
|
|
|
542
799
|
...(delta.limit !== undefined || parent.query.limit !== undefined ? { limit: delta.limit ?? parent.query.limit } : {}),
|
|
543
800
|
},
|
|
544
801
|
outputContract: {
|
|
545
|
-
measures: uniqueSorted(measures.flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
|
|
546
|
-
dimensions: uniqueSorted(dimensions.flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
|
|
802
|
+
measures: uniqueSorted(measures.flatMap((binding) => binding.outputName ? [binding.outputName] : 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),
|
|
547
805
|
...((delta.timeGrain ?? parent.query.timeGrain)
|
|
548
806
|
? { timeGrain: delta.timeGrain ?? parent.query.timeGrain }
|
|
549
807
|
: {}),
|
|
@@ -621,13 +879,32 @@ function bindRequestedMember(requested, candidates, kind, question = '') {
|
|
|
621
879
|
// a second binding identity into the frozen plan. Only candidates still
|
|
622
880
|
// eligible for the requested tuple can contribute member authority.
|
|
623
881
|
const eligibleCandidates = candidates.filter((candidate) => candidate.compatibility !== 'incompatible');
|
|
624
|
-
const
|
|
882
|
+
const kindCandidates = eligibleCandidates.filter((candidate) => {
|
|
625
883
|
const kindMatches = kind === 'measure'
|
|
626
884
|
? candidate.kind === 'semantic_metric' || candidate.kind === 'sql_column'
|
|
627
|
-
:
|
|
628
|
-
|
|
629
|
-
|
|
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;
|
|
630
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)));
|
|
631
908
|
const collectedIds = uniqueSorted([
|
|
632
909
|
...directCandidates.map((candidate) => candidate.qualifiedId ?? candidate.id),
|
|
633
910
|
...(kind === 'dimension' ? eligibleCandidates.flatMap((candidate) => (candidate.dimensions ?? []).filter((dimension) => {
|
|
@@ -651,11 +928,32 @@ function bindRequestedMember(requested, candidates, kind, question = '') {
|
|
|
651
928
|
});
|
|
652
929
|
if (ids.length === 0 && kind === 'measure' && eligibleCandidates.length === 1) {
|
|
653
930
|
const certified = eligibleCandidates[0];
|
|
654
|
-
|
|
931
|
+
const outputName = certifiedCandidateDeclaredMeasureOutput(certified, requested);
|
|
932
|
+
if (certified.kind === 'certified_block'
|
|
933
|
+
&& certified.compatibility === 'compatible'
|
|
934
|
+
&& certifiedCandidateExplicitlyCoversMeasures(certified, [requested])
|
|
935
|
+
&& outputName) {
|
|
936
|
+
const id = certified.qualifiedId ?? certified.id;
|
|
937
|
+
return {
|
|
938
|
+
requested,
|
|
939
|
+
qualifiedId: id,
|
|
940
|
+
outputName,
|
|
941
|
+
status: 'resolved',
|
|
942
|
+
candidateIds: [id],
|
|
943
|
+
};
|
|
944
|
+
}
|
|
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) {
|
|
655
952
|
const id = certified.qualifiedId ?? certified.id;
|
|
656
953
|
return {
|
|
657
954
|
requested,
|
|
658
955
|
qualifiedId: id,
|
|
956
|
+
outputName,
|
|
659
957
|
status: 'resolved',
|
|
660
958
|
candidateIds: [id],
|
|
661
959
|
};
|
|
@@ -667,6 +965,7 @@ function bindRequestedMember(requested, candidates, kind, question = '') {
|
|
|
667
965
|
return {
|
|
668
966
|
requested,
|
|
669
967
|
qualifiedId: namedWinner,
|
|
968
|
+
...(kind === 'output' ? { outputName: outputNameForCandidate(winnerCandidate, namedWinner) } : {}),
|
|
670
969
|
...(winnerCandidate?.aggregation ? { aggregation: winnerCandidate.aggregation } : {}),
|
|
671
970
|
status: 'resolved',
|
|
672
971
|
candidateIds: [namedWinner],
|
|
@@ -678,11 +977,85 @@ function bindRequestedMember(requested, candidates, kind, question = '') {
|
|
|
678
977
|
return {
|
|
679
978
|
requested,
|
|
680
979
|
...(ids.length === 1 ? { qualifiedId: ids[0] } : {}),
|
|
980
|
+
...(kind === 'output' && ids.length === 1
|
|
981
|
+
? { outputName: outputNameForCandidate(direct, ids[0]) }
|
|
982
|
+
: {}),
|
|
681
983
|
...(direct?.aggregation ? { aggregation: direct.aggregation } : {}),
|
|
682
984
|
status: ids.length === 1 ? 'resolved' : ids.length > 1 ? 'ambiguous' : 'unresolved',
|
|
683
985
|
candidateIds: ids,
|
|
684
986
|
};
|
|
685
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
|
+
}
|
|
686
1059
|
function qualifyDeclaredDimension(candidate, dimension) {
|
|
687
1060
|
if (/[:./]/.test(dimension) || !candidate.domain)
|
|
688
1061
|
return dimension;
|
|
@@ -690,9 +1063,6 @@ function qualifyDeclaredDimension(candidate, dimension) {
|
|
|
690
1063
|
const local = dimension.toLowerCase().replace(/[^a-z0-9_-]+/g, '_');
|
|
691
1064
|
return `semantic:${domain}:dimension:${local}`;
|
|
692
1065
|
}
|
|
693
|
-
function resolutionUsesRelationalEvidence(resolution) {
|
|
694
|
-
return resolution.recommendedRoute === 'governed_sql' || resolution.recommendedRoute === 'exploratory';
|
|
695
|
-
}
|
|
696
1066
|
function memberTermMatches(candidateTerm, requested) {
|
|
697
1067
|
if (candidateTerm === requested || candidateTerm.endsWith(` ${requested}`))
|
|
698
1068
|
return true;
|
|
@@ -705,7 +1075,7 @@ function candidateTerms(candidate) {
|
|
|
705
1075
|
.map(normalize)
|
|
706
1076
|
.filter(Boolean);
|
|
707
1077
|
}
|
|
708
|
-
function resolveCapability(resolution, execution, measures, dimensions, filters) {
|
|
1078
|
+
function resolveCapability(resolution, execution, measures, dimensions, filters, requiredOutputs = [], selectedCapability, frozenAuthorityCandidates = [], snapshotCandidates = []) {
|
|
709
1079
|
if (resolution.confidence === "low" ||
|
|
710
1080
|
resolution.recommendedRoute === "clarify" ||
|
|
711
1081
|
!execution ||
|
|
@@ -722,23 +1092,329 @@ function resolveCapability(resolution, execution, measures, dimensions, filters)
|
|
|
722
1092
|
(resolution.recommendedRoute === "certified" ||
|
|
723
1093
|
resolution.recommendedRoute === "semantic"))
|
|
724
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';
|
|
725
1100
|
if (execution.compatibility === "incompatible")
|
|
726
1101
|
return "blocked";
|
|
727
1102
|
if (resolution.recommendedRoute === "certified" &&
|
|
728
1103
|
execution.kind === "certified_block" &&
|
|
729
|
-
execution.compatibility === "compatible"
|
|
1104
|
+
execution.compatibility === "compatible" &&
|
|
1105
|
+
certifiedCandidateExplicitlyCoversMeasures(execution, measures.map((binding) => binding.requested)) &&
|
|
1106
|
+
certifiedBlockProvesHostProjection(execution, resolution)) {
|
|
730
1107
|
return "certified_execution";
|
|
731
1108
|
}
|
|
732
1109
|
if (resolution.recommendedRoute === 'semantic'
|
|
733
1110
|
&& (execution.kind === 'semantic_metric' || execution.kind === 'semantic_member')
|
|
734
|
-
&&
|
|
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
|
+
})))
|
|
735
1133
|
return 'semantic_execution';
|
|
736
|
-
|
|
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
|
+
}))
|
|
737
1166
|
return 'governed_relational';
|
|
738
1167
|
if (resolution.recommendedRoute === 'exploratory')
|
|
739
1168
|
return 'bounded_exploration';
|
|
740
1169
|
return 'blocked';
|
|
741
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
|
+
}
|
|
742
1418
|
function normalize(value) {
|
|
743
1419
|
return value.toLowerCase()
|
|
744
1420
|
.replace(/%/g, ' percentage ')
|
|
@@ -747,6 +1423,40 @@ function normalize(value) {
|
|
|
747
1423
|
.replace(/\s+/g, ' ')
|
|
748
1424
|
.trim();
|
|
749
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
|
+
}
|
|
750
1460
|
/** Resolve common analytical ranges once so no executor reinterprets "last month". */
|
|
751
1461
|
export function resolvePlanTimeRange(expression, referenceTime) {
|
|
752
1462
|
const text = expression.trim().toLowerCase();
|
|
@@ -812,6 +1522,20 @@ function addUnits(value, unit, count) {
|
|
|
812
1522
|
function uniqueSorted(values) {
|
|
813
1523
|
return [...new Set(values.filter(Boolean))].sort();
|
|
814
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
|
+
}
|
|
815
1539
|
function sha256(value) {
|
|
816
1540
|
return createHash('sha256').update(value).digest('hex');
|
|
817
1541
|
}
|