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