@duckcodeailabs/dql-agent 1.13.4 → 1.14.0
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 +87 -7
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +559 -40
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +42 -25
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +100 -32
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/aggregation-safety-proof.d.ts +41 -0
- package/dist/aggregation-safety-proof.d.ts.map +1 -0
- package/dist/aggregation-safety-proof.js +511 -0
- package/dist/aggregation-safety-proof.js.map +1 -0
- package/dist/analytical-compatibility.d.ts.map +1 -1
- package/dist/analytical-compatibility.js +42 -25
- package/dist/analytical-compatibility.js.map +1 -1
- package/dist/analytical-execution-graph.d.ts +4 -21
- package/dist/analytical-execution-graph.d.ts.map +1 -1
- package/dist/analytical-execution-graph.js +27 -0
- package/dist/analytical-execution-graph.js.map +1 -1
- package/dist/analytical-frame.d.ts +12 -1
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +102 -2
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +256 -0
- package/dist/analytical-orchestration.d.ts.map +1 -0
- package/dist/analytical-orchestration.js +419 -0
- package/dist/analytical-orchestration.js.map +1 -0
- package/dist/analytical-result-facts.d.ts +54 -1
- package/dist/analytical-result-facts.d.ts.map +1 -1
- package/dist/analytical-result-facts.js +183 -0
- package/dist/analytical-result-facts.js.map +1 -1
- package/dist/answer-loop.d.ts +44 -3
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +771 -38
- package/dist/answer-loop.js.map +1 -1
- package/dist/cascade/budgets.js +1 -1
- package/dist/cascade/route-policy.d.ts +1 -1
- package/dist/cascade/route-policy.d.ts.map +1 -1
- package/dist/cascade/route-policy.js +2 -0
- package/dist/cascade/route-policy.js.map +1 -1
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +3 -0
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/conversation/turn-trust.d.ts.map +1 -1
- package/dist/conversation/turn-trust.js +6 -0
- package/dist/conversation/turn-trust.js.map +1 -1
- package/dist/generated-analytical-proposal.d.ts +37 -0
- package/dist/generated-analytical-proposal.d.ts.map +1 -0
- package/dist/generated-analytical-proposal.js +219 -0
- package/dist/generated-analytical-proposal.js.map +1 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +8 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.js +102 -6
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +30 -0
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +61 -8
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +46 -2
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/block-fit.d.ts.map +1 -1
- package/dist/metadata/block-fit.js +55 -2
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts +24 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +227 -42
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +408 -36
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/metadata/sql-context-validation.d.ts +3 -0
- package/dist/metadata/sql-context-validation.d.ts.map +1 -1
- package/dist/metadata/sql-context-validation.js +16 -5
- package/dist/metadata/sql-context-validation.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 +81 -32
- package/dist/plan-execution-adapter.js.map +1 -1
- package/dist/provider-egress.d.ts +122 -0
- package/dist/provider-egress.d.ts.map +1 -0
- package/dist/provider-egress.js +504 -0
- package/dist/provider-egress.js.map +1 -0
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +76 -33
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/dispatch.d.ts +15 -0
- package/dist/providers/dispatch.d.ts.map +1 -0
- package/dist/providers/dispatch.js +25 -0
- package/dist/providers/dispatch.js.map +1 -0
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +16 -8
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +2 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.d.ts.map +1 -1
- package/dist/providers/ollama.js +29 -10
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +92 -36
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +43 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/relationship-proof.d.ts +19 -0
- package/dist/relationship-proof.d.ts.map +1 -0
- package/dist/relationship-proof.js +75 -0
- package/dist/relationship-proof.js.map +1 -0
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +17 -11
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +25 -3
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +460 -31
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +38 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1060 -53
- package/dist/router.js.map +1 -1
- package/dist/semantic-identities.d.ts +18 -0
- package/dist/semantic-identities.d.ts.map +1 -0
- package/dist/semantic-identities.js +60 -0
- package/dist/semantic-identities.js.map +1 -0
- package/dist/semantic-proof-authority.d.ts +29 -0
- package/dist/semantic-proof-authority.d.ts.map +1 -0
- package/dist/semantic-proof-authority.js +34 -0
- package/dist/semantic-proof-authority.js.map +1 -0
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolved-analytical-plan.d.ts","sourceRoot":"","sources":["../src/resolved-analytical-plan.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"resolved-analytical-plan.d.ts","sourceRoot":"","sources":["../src/resolved-analytical-plan.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Immutable, snapshot-bound analytical plan shared by every downstream route.
|
|
3
|
-
* The router emits it authoritatively by default.
|
|
4
|
-
*
|
|
3
|
+
* The router emits it authoritatively by default. The `shadow` discriminator
|
|
4
|
+
* remains only for explicit dev/test comparison fixtures; production has no
|
|
5
|
+
* environment flag or request field that can select it.
|
|
5
6
|
*
|
|
6
7
|
* Acceptance: AGT-013, API-006.
|
|
7
8
|
*/
|
|
8
9
|
import { createHash } from "node:crypto";
|
|
10
|
+
import { normalizeMetricCapabilityContract, } from "@duckcodeailabs/dql-core";
|
|
11
|
+
import { buildResolvedRelationshipProofsV1 } from './relationship-proof.js';
|
|
9
12
|
export function buildResolvedAnalyticalPlan(input) {
|
|
10
13
|
const byLegacyId = new Map(input.candidates.map((candidate) => [candidate.id, candidate]));
|
|
11
14
|
const selectedCandidates = input.resolution.selectedConceptIds
|
|
@@ -13,18 +16,23 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
13
16
|
const executionCandidate = input.resolution.recommendedExecutionId
|
|
14
17
|
? byLegacyId.get(input.resolution.recommendedExecutionId)
|
|
15
18
|
: selectedCandidates[0];
|
|
16
|
-
const bindingCandidates =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const bindingCandidates = input.resolution.recommendedRoute === 'certified' && executionCandidate
|
|
20
|
+
// A deterministically compatible certified block has already proved the
|
|
21
|
+
// complete tuple. Bind its declared inputs/outputs as one execution
|
|
22
|
+
// authority instead of mixing in the semantic concept used to select it.
|
|
23
|
+
? [executionCandidate]
|
|
24
|
+
: resolutionUsesRelationalEvidence(input.resolution)
|
|
25
|
+
? input.candidates
|
|
26
|
+
: selectedCandidates.length > 0
|
|
27
|
+
? selectedCandidates
|
|
28
|
+
: executionCandidate
|
|
29
|
+
? [executionCandidate]
|
|
30
|
+
: input.candidates;
|
|
23
31
|
const canonicalId = (candidate) => candidate.qualifiedId ?? candidate.id;
|
|
24
32
|
const selectedConceptIds = selectedCandidates.map(canonicalId);
|
|
25
33
|
const executionId = executionCandidate ? canonicalId(executionCandidate) : undefined;
|
|
26
34
|
const measures = input.resolution.queryIntent.measures.length > 0
|
|
27
|
-
? input.resolution.queryIntent.measures.map((requested) => bindRequestedMember(requested, bindingCandidates, 'measure'))
|
|
35
|
+
? input.resolution.queryIntent.measures.map((requested) => bindRequestedMember(requested, bindingCandidates, 'measure', input.question))
|
|
28
36
|
: selectedCandidates
|
|
29
37
|
.filter((candidate) => candidate.kind === 'semantic_metric')
|
|
30
38
|
.map((candidate) => ({
|
|
@@ -40,31 +48,123 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
40
48
|
const dimensionBindingCandidates = input.resolution.analyticalFrame
|
|
41
49
|
? input.candidates
|
|
42
50
|
: bindingCandidates;
|
|
51
|
+
const selectedCapability = normalizeMetricCapabilityContract(executionCandidate?.analyticalCapability);
|
|
52
|
+
const bindDimension = (requested, roles) => {
|
|
53
|
+
const capabilityBinding = bindSelectedCapabilityDimension(requested, selectedCapability, roles, input.candidates);
|
|
54
|
+
// A provider frame may describe a selected capability, but it cannot pick
|
|
55
|
+
// one of that capability's grouping dimensions for the host. Once a
|
|
56
|
+
// normalized capability supplies a relevant binding, its identifiers,
|
|
57
|
+
// roles, entity/grain and relationship proof are authoritative.
|
|
58
|
+
// A selected capability owns its grouping dimensions, but when it reports
|
|
59
|
+
// AMBIGUITY the question text can still settle it — and until it did, an
|
|
60
|
+
// answerable lookup stopped to ask.
|
|
61
|
+
if (capabilityBinding && capabilityBinding.status === 'ambiguous') {
|
|
62
|
+
const settled = memberNamedInQuestion(capabilityBinding.candidateIds, input.candidates, input.question);
|
|
63
|
+
if (settled)
|
|
64
|
+
return { ...capabilityBinding, qualifiedId: settled, status: 'resolved', candidateIds: [settled] };
|
|
65
|
+
}
|
|
66
|
+
if (capabilityBinding && capabilityBinding.status !== 'unresolved')
|
|
67
|
+
return capabilityBinding;
|
|
68
|
+
const normalizedRequested = normalize(requested);
|
|
69
|
+
const frameIds = uniqueSorted((input.resolution.analyticalFrame?.dimensions ?? [])
|
|
70
|
+
.filter((binding) => capabilityEntailsFrameDimension(selectedCapability, binding)
|
|
71
|
+
&& (!selectedCapability || binding.role === 'time_axis')
|
|
72
|
+
&& memberTermMatches(normalize(binding.dimensionId), normalizedRequested))
|
|
73
|
+
.map((binding) => binding.dimensionId));
|
|
74
|
+
if (frameIds.length === 1) {
|
|
75
|
+
return {
|
|
76
|
+
requested,
|
|
77
|
+
qualifiedId: frameIds[0],
|
|
78
|
+
status: 'resolved',
|
|
79
|
+
candidateIds: frameIds,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (capabilityBinding)
|
|
83
|
+
return capabilityBinding;
|
|
84
|
+
return bindRequestedMember(requested, dimensionBindingCandidates, 'dimension', input.question);
|
|
85
|
+
};
|
|
86
|
+
const rankingRequested = input.resolution.questionType === 'ranking'
|
|
87
|
+
|| input.resolution.queryIntent.order !== undefined
|
|
88
|
+
|| input.resolution.queryIntent.limit !== undefined;
|
|
43
89
|
const dimensions = input.resolution.queryIntent.dimensions
|
|
44
|
-
.map((requested) =>
|
|
45
|
-
|
|
90
|
+
.map((requested) => bindDimension(requested, rankingRequested ? ['group_by', 'rank_entity'] : ['group_by']));
|
|
91
|
+
// A provider may name the field it is grouping by and forget the MEMBER the
|
|
92
|
+
// reader asked about: "What customer type is Wesley Jenkins?" came back with
|
|
93
|
+
// `filters: []`, so the run returned all 200 customers and the narration
|
|
94
|
+
// truthfully reported that his value was not present.
|
|
95
|
+
//
|
|
96
|
+
// DQL had already found him. `evidence.parsedIntent.filters` are GROUNDED
|
|
97
|
+
// member bindings — matched against the retrieved context, not guessed — so
|
|
98
|
+
// falling back to them adds no unproven predicate. The provider's own filters
|
|
99
|
+
// still win whenever it supplies any.
|
|
100
|
+
const requestedFilters = input.resolution.queryIntent.filters.length > 0
|
|
101
|
+
? input.resolution.queryIntent.filters
|
|
102
|
+
: (input.evidence.parsedIntent?.filters ?? []).filter((filter) => filter.field && filter.value);
|
|
103
|
+
const filters = requestedFilters.map((filter) => ({
|
|
46
104
|
...filter,
|
|
47
|
-
binding:
|
|
105
|
+
binding: bindDimension(filter.field, ['filter']),
|
|
48
106
|
}));
|
|
49
|
-
const
|
|
107
|
+
const capabilityDimensionProof = resolvedCapabilityDimensionProof(selectedCapability, dimensions, filters);
|
|
108
|
+
const proofCandidates = [
|
|
109
|
+
...selectedCandidates,
|
|
110
|
+
...(executionCandidate && !selectedCandidates.some((candidate) => candidate.id === executionCandidate.id)
|
|
111
|
+
? [executionCandidate]
|
|
112
|
+
: []),
|
|
113
|
+
];
|
|
114
|
+
const compatibilityProof = proofCandidates.map((candidate) => ({
|
|
50
115
|
candidateId: canonicalId(candidate),
|
|
51
116
|
compatibility: candidate.compatibility,
|
|
52
|
-
facts:
|
|
117
|
+
facts: uniqueSorted([
|
|
118
|
+
...(candidate.compatibilityFacts ?? []),
|
|
119
|
+
...(candidate.id === executionCandidate?.id && selectedCapability
|
|
120
|
+
? selectedCapabilityProofFacts(selectedCapability, capabilityDimensionProof)
|
|
121
|
+
: []),
|
|
122
|
+
]),
|
|
53
123
|
}));
|
|
54
124
|
const capability = resolveCapability(input.resolution, executionCandidate, measures, dimensions, filters);
|
|
125
|
+
const bindingGaps = bindingMissingInformation(measures, dimensions, filters);
|
|
126
|
+
// The reader named a member that no grounding lane could bind. Answering the
|
|
127
|
+
// question WITHOUT it silently changes what was asked — "what customer type
|
|
128
|
+
// is Wesley Jenkins" became "list every customer" — so the plan carries it as
|
|
129
|
+
// a gap rather than dropping it. Live value lookup is opt-in per project, so
|
|
130
|
+
// the remedy is named here instead of leaving a bare failure.
|
|
131
|
+
const unboundMembers = filters.length === 0 ? (input.evidence.unboundMemberTerms ?? []) : [];
|
|
132
|
+
const unboundMemberGaps = unboundMembers.length > 0
|
|
133
|
+
? [`The named value ${unboundMembers.map((term) => `“${term}”`).join(' and ')} could not be bound to a governed member, so it was not applied as a filter. Enable live value grounding (agent.runtimeValueGrounding.mode = "safe_automatic" with a searchSafeColumns allowlist) or filter on a modeled dimension.`]
|
|
134
|
+
: [];
|
|
135
|
+
const missingInformation = uniqueSorted([
|
|
136
|
+
...input.resolution.missingInformation,
|
|
137
|
+
...bindingGaps,
|
|
138
|
+
...unboundMemberGaps,
|
|
139
|
+
...(capability === 'blocked' && input.resolution.missingInformation.length === 0 && bindingGaps.length === 0
|
|
140
|
+
? [`The selected ${input.resolution.recommendedRoute} capability is not executable for this analytical tuple.`]
|
|
141
|
+
: []),
|
|
142
|
+
]);
|
|
55
143
|
const timeBounds = input.resolution.queryIntent.timeRange
|
|
56
144
|
? resolvePlanTimeRange(input.resolution.queryIntent.timeRange, input.referenceTime ?? new Date())
|
|
57
145
|
: undefined;
|
|
146
|
+
const snapshotId = input.evidence.knowledgeLens?.snapshotId
|
|
147
|
+
?? input.evidence.snapshotId
|
|
148
|
+
?? input.evidence.sourceFingerprint
|
|
149
|
+
?? 'snapshot-unavailable';
|
|
150
|
+
const selectedExecutionCapability = selectedCapability?.executionCapabilities.find((candidate) => candidate.route === input.resolution.recommendedRoute);
|
|
151
|
+
const relationshipProofs = selectedCapability && selectedExecutionCapability && executionId
|
|
152
|
+
? buildResolvedRelationshipProofsV1({
|
|
153
|
+
capability: selectedCapability,
|
|
154
|
+
dimensionIds: capabilityDimensionProof.map((dimension) => dimension.dimensionId),
|
|
155
|
+
route: selectedExecutionCapability.route,
|
|
156
|
+
...(selectedExecutionCapability.adapterId ? { adapterId: selectedExecutionCapability.adapterId } : {}),
|
|
157
|
+
executionId,
|
|
158
|
+
snapshotId,
|
|
159
|
+
})
|
|
160
|
+
: [];
|
|
58
161
|
const payload = {
|
|
59
162
|
schemaVersion: input.resolution.analyticalFrame
|
|
60
163
|
? 2
|
|
61
164
|
: 1,
|
|
62
|
-
mode: input.mode ?? "
|
|
165
|
+
mode: input.mode ?? "authoritative",
|
|
63
166
|
revision: 0,
|
|
64
|
-
snapshotId
|
|
65
|
-
?? input.evidence.snapshotId
|
|
66
|
-
?? input.evidence.sourceFingerprint
|
|
67
|
-
?? 'snapshot-unavailable',
|
|
167
|
+
snapshotId,
|
|
68
168
|
sourceFingerprint: input.evidence.sourceFingerprint,
|
|
69
169
|
question: input.question,
|
|
70
170
|
interpretedQuestion: input.resolution.interpretedQuestion,
|
|
@@ -72,6 +172,12 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
72
172
|
confidence: input.resolution.confidence,
|
|
73
173
|
selectedConceptIds,
|
|
74
174
|
executionId,
|
|
175
|
+
...(selectedCapability
|
|
176
|
+
? {
|
|
177
|
+
selectedCapability,
|
|
178
|
+
selectedCapabilityFingerprint: selectedCapability.sourceFingerprint,
|
|
179
|
+
}
|
|
180
|
+
: {}),
|
|
75
181
|
recommendedRoute: input.resolution.recommendedRoute,
|
|
76
182
|
capability,
|
|
77
183
|
query: {
|
|
@@ -84,10 +190,14 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
84
190
|
...(input.resolution.queryIntent.order ? { order: input.resolution.queryIntent.order } : {}),
|
|
85
191
|
...(input.resolution.queryIntent.limit !== undefined ? { limit: input.resolution.queryIntent.limit } : {}),
|
|
86
192
|
},
|
|
87
|
-
entityGrain:
|
|
88
|
-
|
|
193
|
+
entityGrain: selectedCapability
|
|
194
|
+
? resolvedCapabilityEntityGrain(selectedCapability, dimensions)
|
|
195
|
+
: input.resolution.analyticalFrame?.entityGrainIds[0] ?? executionCandidate?.primaryEntity,
|
|
89
196
|
sourceRelationIds: uniqueSorted(selectedCandidates.flatMap((candidate) => candidate.sourceObjects ?? [])),
|
|
90
|
-
relationshipPathIds:
|
|
197
|
+
relationshipPathIds: selectedCapability
|
|
198
|
+
? uniqueSorted(relationshipProofs.flatMap((proof) => proof.kind === 'dql_relationship_path' ? proof.relationshipPathIds : []))
|
|
199
|
+
: uniqueSorted(selectedCandidates.flatMap((candidate) => candidate.relationshipEvidence ?? [])),
|
|
200
|
+
relationshipProofs,
|
|
91
201
|
compatibilityProof,
|
|
92
202
|
outputContract: {
|
|
93
203
|
measures: uniqueSorted(measures.flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : [binding.requested])),
|
|
@@ -107,8 +217,34 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
107
217
|
const retrieved = byLegacyId.get(candidate.id);
|
|
108
218
|
return { id: retrieved ? canonicalId(retrieved) : candidate.id, reason: candidate.reason };
|
|
109
219
|
}),
|
|
110
|
-
missingInformation
|
|
220
|
+
missingInformation,
|
|
111
221
|
clarification: input.resolution.clarifyingQuestion,
|
|
222
|
+
...(capability === 'blocked'
|
|
223
|
+
? {
|
|
224
|
+
resolutionFailure: {
|
|
225
|
+
outcome: input.resolution.compatibilityOutcome
|
|
226
|
+
?? (input.resolution.recommendedRoute === 'clarify' || input.resolution.confidence === 'low'
|
|
227
|
+
? 'clarify'
|
|
228
|
+
: bindingCandidateIds(measures, dimensions, filters).length > 0
|
|
229
|
+
? 'clarify'
|
|
230
|
+
: 'modeling_gap'),
|
|
231
|
+
codes: uniqueSorted([
|
|
232
|
+
...(input.resolution.compatibilityFailures ?? []).map((failure) => failure.code),
|
|
233
|
+
...bindingFailureCodes(measures, dimensions, filters),
|
|
234
|
+
]),
|
|
235
|
+
candidateIds: uniqueSorted([
|
|
236
|
+
...(input.resolution.compatibilityFailures ?? []).flatMap((failure) => failure.candidateIds),
|
|
237
|
+
...bindingCandidateIds(measures, dimensions, filters),
|
|
238
|
+
]),
|
|
239
|
+
...(selectedCapability?.metricId
|
|
240
|
+
? { selectedCapabilityId: selectedCapability.metricId }
|
|
241
|
+
: {}),
|
|
242
|
+
...(executionId ? { selectedExecutionId: executionId } : {}),
|
|
243
|
+
finalCapability: capability,
|
|
244
|
+
bindings: unresolvedBindingReceipts(measures, dimensions, filters),
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
: {}),
|
|
112
248
|
knowledgeLens: input.evidence.knowledgeLens,
|
|
113
249
|
...((input.resolution.analyticalPolicyIds?.length ?? 0) > 0
|
|
114
250
|
? {
|
|
@@ -133,6 +269,233 @@ export function buildResolvedAnalyticalPlan(input) {
|
|
|
133
269
|
fingerprint,
|
|
134
270
|
});
|
|
135
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* The selected metric capability is the sole authority for metric-relative
|
|
274
|
+
* dimensions. Standalone member cards can help the model explain meaning, but
|
|
275
|
+
* cannot introduce a second execution binding or substitute a same-name ID.
|
|
276
|
+
*/
|
|
277
|
+
function bindSelectedCapabilityDimension(requested, capability, requiredRoles, candidates) {
|
|
278
|
+
if (!capability)
|
|
279
|
+
return undefined;
|
|
280
|
+
const eligible = eligibleCapabilityDimensions(capability, requiredRoles);
|
|
281
|
+
const exactIds = uniqueSorted(eligible
|
|
282
|
+
.filter((dimension) => dimension.dimensionId === requested)
|
|
283
|
+
.map((dimension) => dimension.dimensionId));
|
|
284
|
+
if (exactIds.length === 1) {
|
|
285
|
+
return {
|
|
286
|
+
requested,
|
|
287
|
+
qualifiedId: exactIds[0],
|
|
288
|
+
status: 'resolved',
|
|
289
|
+
candidateIds: exactIds,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const requestedEntityEvidence = candidates.filter((candidate) => candidate.semanticObjectType === 'entity'
|
|
293
|
+
&& candidateTerms(candidate).some((term) => termMatchSpecificity(term, canonicalTokens(requested)) > 0));
|
|
294
|
+
const entityScoped = requestedEntityEvidence.length > 0
|
|
295
|
+
? eligible.filter((dimension) => capabilityDimensionMatchesEntityEvidence(dimension, requestedEntityEvidence))
|
|
296
|
+
: [];
|
|
297
|
+
const bindingPool = entityScoped.length > 0 ? entityScoped : eligible;
|
|
298
|
+
const scored = bindingPool
|
|
299
|
+
.map((dimension) => ({
|
|
300
|
+
dimension,
|
|
301
|
+
score: capabilityDimensionMatchScore(requested, dimension, capability, candidates),
|
|
302
|
+
}))
|
|
303
|
+
.filter((match) => match.score > 0);
|
|
304
|
+
if (scored.length === 0) {
|
|
305
|
+
return {
|
|
306
|
+
requested,
|
|
307
|
+
status: 'unresolved',
|
|
308
|
+
// Role-compatible but semantically unrelated members are not choices a
|
|
309
|
+
// user can use to repair this binding. Keep the list empty so the router
|
|
310
|
+
// reports a modeling/binding gap instead of offering arbitrary booleans.
|
|
311
|
+
candidateIds: [],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const bestScore = Math.max(...scored.map((match) => match.score));
|
|
315
|
+
const matches = scored
|
|
316
|
+
.filter((match) => match.score === bestScore)
|
|
317
|
+
.map((match) => match.dimension);
|
|
318
|
+
const ids = uniqueSorted(matches.map((dimension) => dimension.dimensionId));
|
|
319
|
+
return {
|
|
320
|
+
requested,
|
|
321
|
+
...(ids.length === 1 ? { qualifiedId: ids[0] } : {}),
|
|
322
|
+
status: ids.length === 1 ? 'resolved' : 'ambiguous',
|
|
323
|
+
candidateIds: ids,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function capabilityDimensionMatchScore(requested, dimension, capability, candidates) {
|
|
327
|
+
const requestedTokens = canonicalTokens(requested);
|
|
328
|
+
if (requestedTokens.length === 0)
|
|
329
|
+
return 0;
|
|
330
|
+
const evidenceTerms = candidates
|
|
331
|
+
.filter((candidate) => candidate.kind === 'semantic_member'
|
|
332
|
+
&& candidate.semanticObjectType !== 'entity'
|
|
333
|
+
&& (candidate.qualifiedId ?? candidate.id) === dimension.dimensionId)
|
|
334
|
+
.flatMap((candidate) => [candidate.name, ...(candidate.aliases ?? [])]);
|
|
335
|
+
const evidenceScore = Math.max(0, ...evidenceTerms.map((term) => termMatchSpecificity(term, requestedTokens)));
|
|
336
|
+
const authoredDisplayScore = Math.max(0, ...[dimension.label, ...(dimension.aliases ?? [])]
|
|
337
|
+
.filter((term) => Boolean(term))
|
|
338
|
+
.map((term) => termMatchSpecificity(term, requestedTokens)));
|
|
339
|
+
const localScore = termMatchSpecificity(localQualifiedLeaf(dimension.dimensionId), requestedTokens);
|
|
340
|
+
const conventionalIdentityScore = conventionalIdentitySpecificity(localQualifiedLeaf(dimension.dimensionId), requestedTokens);
|
|
341
|
+
const entityScore = termMatchSpecificity(localQualifiedLeaf(dimension.entityId), requestedTokens);
|
|
342
|
+
const identityScore = Math.max(evidenceScore > 0 ? 1_000 + evidenceScore : 0, authoredDisplayScore > 0 ? 900 + authoredDisplayScore : 0, conventionalIdentityScore > 0 ? 1_100 + conventionalIdentityScore : 0, localScore > 0 ? 600 + localScore : 0, entityScore > 0 ? 300 + entityScore : 0);
|
|
343
|
+
if (identityScore === 0)
|
|
344
|
+
return 0;
|
|
345
|
+
const authorityScore = (dimension.supportedRoles.includes('display') ? 8 : 0)
|
|
346
|
+
+ (dimension.entityId === capability.primaryEntityId ? 4 : 0)
|
|
347
|
+
+ (capability.resultGrainIds.includes(dimension.entityId) ? 4 : 0)
|
|
348
|
+
+ ((dimension.relationshipPathIds?.length ?? 0) > 0 ? 4 : 0);
|
|
349
|
+
return identityScore + authorityScore;
|
|
350
|
+
}
|
|
351
|
+
function conventionalIdentitySpecificity(term, requestedTokens) {
|
|
352
|
+
const termTokens = canonicalTokens(term);
|
|
353
|
+
if (!requestedTokens.every((token) => termTokens.includes(token)))
|
|
354
|
+
return 0;
|
|
355
|
+
const remaining = termTokens.filter((token) => !requestedTokens.includes(token));
|
|
356
|
+
const identityDescriptors = new Set(['display', 'identifier', 'id', 'key', 'label', 'name', 'title']);
|
|
357
|
+
return remaining.length === 1 && identityDescriptors.has(remaining[0]) ? 100 : 0;
|
|
358
|
+
}
|
|
359
|
+
function capabilityDimensionMatchesEntityEvidence(dimension, entityCandidates) {
|
|
360
|
+
const ownerTokens = canonicalTokens(localQualifiedOwner(dimension.dimensionId));
|
|
361
|
+
return entityCandidates.some((candidate) => {
|
|
362
|
+
const candidateId = candidate.qualifiedId ?? candidate.id;
|
|
363
|
+
if (dimension.entityId === candidateId)
|
|
364
|
+
return true;
|
|
365
|
+
if (ownerTokens.length === 0)
|
|
366
|
+
return false;
|
|
367
|
+
const entityTerms = [candidate.name, ...(candidate.aliases ?? []), localQualifiedLeaf(candidateId)];
|
|
368
|
+
return entityTerms.some((term) => {
|
|
369
|
+
const tokens = canonicalTokens(localQualifiedLeaf(term));
|
|
370
|
+
return tokens.length > 0 && tokens.every((token) => ownerTokens.includes(token));
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
function termMatchSpecificity(term, requestedTokens) {
|
|
375
|
+
const termTokens = canonicalTokens(term);
|
|
376
|
+
if (termTokens.length === 0 || !requestedTokens.every((token) => termTokens.includes(token)))
|
|
377
|
+
return 0;
|
|
378
|
+
if (termTokens.length === requestedTokens.length
|
|
379
|
+
&& termTokens.every((token, index) => token === requestedTokens[index]))
|
|
380
|
+
return 100;
|
|
381
|
+
return 50 + requestedTokens.length;
|
|
382
|
+
}
|
|
383
|
+
function canonicalTokens(value) {
|
|
384
|
+
return normalize(value)
|
|
385
|
+
.split(' ')
|
|
386
|
+
.filter(Boolean)
|
|
387
|
+
.map(singularizeBindingToken);
|
|
388
|
+
}
|
|
389
|
+
function singularizeBindingToken(value) {
|
|
390
|
+
if (value.length <= 4 || value.endsWith('ss') || value.endsWith('us') || value.endsWith('is'))
|
|
391
|
+
return value;
|
|
392
|
+
if (value.endsWith('ies'))
|
|
393
|
+
return `${value.slice(0, -3)}y`;
|
|
394
|
+
return value.endsWith('s') ? value.slice(0, -1) : value;
|
|
395
|
+
}
|
|
396
|
+
function localQualifiedLeaf(value) {
|
|
397
|
+
const leaf = value.split(/[:./]+/).filter(Boolean).at(-1);
|
|
398
|
+
return leaf ?? value;
|
|
399
|
+
}
|
|
400
|
+
function localQualifiedOwner(value) {
|
|
401
|
+
const local = value.split(':').at(-1) ?? value;
|
|
402
|
+
const segments = local.split(/[./]+/).filter(Boolean);
|
|
403
|
+
return segments.length > 1 ? segments[0] : '';
|
|
404
|
+
}
|
|
405
|
+
function eligibleCapabilityDimensions(capability, requiredRoles) {
|
|
406
|
+
if (!capability)
|
|
407
|
+
return [];
|
|
408
|
+
return capability.dimensions.filter((dimension) => {
|
|
409
|
+
if (!requiredRoles.every((role) => dimension.supportedRoles.includes(role)))
|
|
410
|
+
return false;
|
|
411
|
+
const sameEntity = dimension.entityId === capability.primaryEntityId;
|
|
412
|
+
const declaredGrain = capability.resultGrainIds.includes(dimension.entityId);
|
|
413
|
+
const relationshipProven = (dimension.relationshipPathIds?.length ?? 0) > 0;
|
|
414
|
+
const nativeGroupingProven = Boolean(dimension.nativeGroupingReference)
|
|
415
|
+
&& capability.executionCapabilities.some((execution) => execution.route === 'semantic');
|
|
416
|
+
return sameEntity || declaredGrain || relationshipProven || nativeGroupingProven;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function capabilityEntailsFrameDimension(capability, binding) {
|
|
420
|
+
if (!capability)
|
|
421
|
+
return false;
|
|
422
|
+
if (binding.role === 'time_axis') {
|
|
423
|
+
return capability.timeDimensions.some((dimension) => dimension.dimensionId === binding.dimensionId);
|
|
424
|
+
}
|
|
425
|
+
return eligibleCapabilityDimensions(capability, [binding.role])
|
|
426
|
+
.some((dimension) => dimension.dimensionId === binding.dimensionId);
|
|
427
|
+
}
|
|
428
|
+
function resolvedCapabilityDimensionProof(capability, dimensions, filters) {
|
|
429
|
+
if (!capability)
|
|
430
|
+
return [];
|
|
431
|
+
const boundIds = new Set([
|
|
432
|
+
...dimensions.flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : []),
|
|
433
|
+
...filters.flatMap((filter) => filter.binding.qualifiedId ? [filter.binding.qualifiedId] : []),
|
|
434
|
+
]);
|
|
435
|
+
return capability.dimensions
|
|
436
|
+
.filter((dimension) => boundIds.has(dimension.dimensionId))
|
|
437
|
+
.sort((left, right) => left.dimensionId.localeCompare(right.dimensionId));
|
|
438
|
+
}
|
|
439
|
+
function resolvedCapabilityEntityGrain(capability, dimensions) {
|
|
440
|
+
const groupedIds = new Set(dimensions.flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : []));
|
|
441
|
+
const groupedEntityIds = uniqueSorted(capability.dimensions
|
|
442
|
+
.filter((dimension) => groupedIds.has(dimension.dimensionId))
|
|
443
|
+
.map((dimension) => dimension.entityId));
|
|
444
|
+
if (groupedEntityIds.length === 1 && capability.resultGrainIds.includes(groupedEntityIds[0])) {
|
|
445
|
+
return groupedEntityIds[0];
|
|
446
|
+
}
|
|
447
|
+
return capability.defaultResultGrainId;
|
|
448
|
+
}
|
|
449
|
+
function selectedCapabilityProofFacts(capability, dimensions) {
|
|
450
|
+
return [
|
|
451
|
+
`capability:metric:${capability.metricId}`,
|
|
452
|
+
`capability:primary_entity:${capability.primaryEntityId}`,
|
|
453
|
+
`capability:default_result_grain:${capability.defaultResultGrainId}`,
|
|
454
|
+
...capability.resultGrainIds.map((grain) => `capability:result_grain:${grain}`),
|
|
455
|
+
...dimensions.map((dimension) => `capability:dimension:${dimension.dimensionId}:${dimension.entityId}`),
|
|
456
|
+
...dimensions.flatMap((dimension) => dimension.nativeGroupingReference
|
|
457
|
+
? [`capability:native_grouping:${dimension.dimensionId}:${dimension.nativeGroupingReference}`]
|
|
458
|
+
: []),
|
|
459
|
+
...dimensions.flatMap((dimension) => (dimension.relationshipPathIds ?? []).map((relationshipId) => `capability:relationship:${relationshipId}`)),
|
|
460
|
+
...capability.executionCapabilities.map((execution) => `capability:route:${execution.route}:${execution.adapterId ?? 'native'}`),
|
|
461
|
+
];
|
|
462
|
+
}
|
|
463
|
+
function unresolvedBindingReceipts(measures, dimensions, filters) {
|
|
464
|
+
return [
|
|
465
|
+
...measures.map((binding) => ({ kind: 'measure', binding })),
|
|
466
|
+
...dimensions.map((binding) => ({ kind: 'dimension', binding })),
|
|
467
|
+
...filters.map((filter) => ({ kind: 'filter', binding: filter.binding })),
|
|
468
|
+
]
|
|
469
|
+
.filter(({ binding }) => binding.status !== 'resolved')
|
|
470
|
+
.map(({ kind, binding }) => ({
|
|
471
|
+
kind,
|
|
472
|
+
requested: binding.requested,
|
|
473
|
+
status: binding.status,
|
|
474
|
+
candidateIds: [...binding.candidateIds],
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
477
|
+
function bindingCandidateIds(measures, dimensions, filters) {
|
|
478
|
+
return unresolvedBindingReceipts(measures, dimensions, filters)
|
|
479
|
+
.flatMap((binding) => binding.candidateIds);
|
|
480
|
+
}
|
|
481
|
+
function bindingFailureCodes(measures, dimensions, filters) {
|
|
482
|
+
return unresolvedBindingReceipts(measures, dimensions, filters)
|
|
483
|
+
.map((binding) => `${binding.kind.toUpperCase()}_${binding.status.toUpperCase()}`);
|
|
484
|
+
}
|
|
485
|
+
function bindingMissingInformation(measures, dimensions, filters) {
|
|
486
|
+
return [
|
|
487
|
+
...measures.map((binding) => ({ kind: 'measure', binding })),
|
|
488
|
+
...dimensions.map((binding) => ({ kind: 'dimension', binding })),
|
|
489
|
+
...filters.map((filter) => ({ kind: 'filter', binding: filter.binding })),
|
|
490
|
+
].flatMap(({ kind, binding }) => {
|
|
491
|
+
if (binding.status === 'resolved')
|
|
492
|
+
return [];
|
|
493
|
+
const choices = binding.candidateIds.length > 0
|
|
494
|
+
? ` Qualified choices: ${binding.candidateIds.join(', ')}.`
|
|
495
|
+
: '';
|
|
496
|
+
return [`Requested ${kind} “${binding.requested}” is ${binding.status}.${choices}`];
|
|
497
|
+
});
|
|
498
|
+
}
|
|
136
499
|
/**
|
|
137
500
|
* Apply an explicitly typed follow-up delta. No prose, prior SQL, or answer text
|
|
138
501
|
* is inspected; every carried member remains a qualified binding from the root.
|
|
@@ -220,24 +583,74 @@ function cloneBinding(binding) {
|
|
|
220
583
|
function cloneFilter(filter) {
|
|
221
584
|
return { ...filter, binding: cloneBinding(filter.binding) };
|
|
222
585
|
}
|
|
223
|
-
|
|
586
|
+
/**
|
|
587
|
+
* Disambiguate a member binding using the QUESTION TEXT.
|
|
588
|
+
*
|
|
589
|
+
* A planner (or a provider) often reduces "customer type" to the bare term
|
|
590
|
+
* `type`, and worse, may split it into two requested dimensions ("customer",
|
|
591
|
+
* "type"). A bare `type` then matches `customers.customer_type` AND
|
|
592
|
+
* `products.product_type` and reads as ambiguous, so an answerable question
|
|
593
|
+
* stops to ask which dimension was meant. The question already answers it:
|
|
594
|
+
* "customer type" appears in it and "product type" does not.
|
|
595
|
+
*
|
|
596
|
+
* Returns the single id the reader actually named, or undefined when the text
|
|
597
|
+
* does not settle it. Only multi-word names count — one shared token is not
|
|
598
|
+
* evidence.
|
|
599
|
+
*/
|
|
600
|
+
function memberNamedInQuestion(ids, candidates, question) {
|
|
601
|
+
if (ids.length < 2)
|
|
602
|
+
return undefined;
|
|
603
|
+
const questionText = normalize(question);
|
|
604
|
+
if (!questionText)
|
|
605
|
+
return undefined;
|
|
606
|
+
const named = ids.filter((id) => {
|
|
607
|
+
const candidate = candidates.find((item) => (item.qualifiedId ?? item.id) === id || item.id === id);
|
|
608
|
+
const terms = [candidate?.name ?? '', ...(candidate?.aliases ?? []), id.split(/[.:/]/).at(-1) ?? '']
|
|
609
|
+
.map((term) => normalize(String(term)))
|
|
610
|
+
.filter((term) => term.split(' ').length > 1);
|
|
611
|
+
return terms.some((term) => questionText === term
|
|
612
|
+
|| questionText.startsWith(`${term} `)
|
|
613
|
+
|| questionText.endsWith(` ${term}`)
|
|
614
|
+
|| questionText.includes(` ${term} `));
|
|
615
|
+
});
|
|
616
|
+
return named.length === 1 ? named[0] : undefined;
|
|
617
|
+
}
|
|
618
|
+
function bindRequestedMember(requested, candidates, kind, question = '') {
|
|
224
619
|
const normalized = normalize(requested);
|
|
225
|
-
|
|
620
|
+
// A lexical decoy that compatibility already rejected must never introduce
|
|
621
|
+
// a second binding identity into the frozen plan. Only candidates still
|
|
622
|
+
// eligible for the requested tuple can contribute member authority.
|
|
623
|
+
const eligibleCandidates = candidates.filter((candidate) => candidate.compatibility !== 'incompatible');
|
|
624
|
+
const directCandidates = eligibleCandidates.filter((candidate) => {
|
|
226
625
|
const kindMatches = kind === 'measure'
|
|
227
626
|
? candidate.kind === 'semantic_metric' || candidate.kind === 'sql_column'
|
|
228
627
|
: candidate.kind === 'semantic_member' || candidate.kind === 'sql_column';
|
|
229
628
|
return kindMatches
|
|
230
629
|
&& candidateTerms(candidate).some((term) => memberTermMatches(term, normalized));
|
|
231
630
|
});
|
|
232
|
-
const
|
|
631
|
+
const collectedIds = uniqueSorted([
|
|
233
632
|
...directCandidates.map((candidate) => candidate.qualifiedId ?? candidate.id),
|
|
234
|
-
...(kind === 'dimension' ?
|
|
633
|
+
...(kind === 'dimension' ? eligibleCandidates.flatMap((candidate) => (candidate.dimensions ?? []).filter((dimension) => {
|
|
235
634
|
const value = normalize(dimension);
|
|
236
635
|
return value === normalized || value.endsWith(` ${normalized}`);
|
|
237
636
|
}).map((dimension) => qualifyDeclaredDimension(candidate, dimension))) : []),
|
|
238
637
|
]);
|
|
239
|
-
|
|
240
|
-
|
|
638
|
+
// A candidate contributes its qualified identity AND its own declared
|
|
639
|
+
// dimension list, so a dimension that IS the candidate arrived twice —
|
|
640
|
+
// `semantic:dimension:customers.customer_type` alongside a bare
|
|
641
|
+
// `customer_type`. Two ids read as ambiguous, the binding failed, and the
|
|
642
|
+
// plan blocked, which is why a metric-free attribute lookup could never
|
|
643
|
+
// freeze. A bare name that is the LEAF of a qualified id in the same set is
|
|
644
|
+
// the same member named twice, not a second choice.
|
|
645
|
+
const qualifiedIds = collectedIds.filter((id) => /[:./]/.test(id));
|
|
646
|
+
const ids = collectedIds.filter((id) => {
|
|
647
|
+
if (/[:./]/.test(id))
|
|
648
|
+
return true;
|
|
649
|
+
const bare = normalize(id);
|
|
650
|
+
return !qualifiedIds.some((qualified) => normalize(qualified.split(/[.:/]/).at(-1) ?? '') === bare);
|
|
651
|
+
});
|
|
652
|
+
if (ids.length === 0 && kind === 'measure' && eligibleCandidates.length === 1) {
|
|
653
|
+
const certified = eligibleCandidates[0];
|
|
241
654
|
if (certified.kind === 'certified_block' && certified.compatibility === 'compatible') {
|
|
242
655
|
const id = certified.qualifiedId ?? certified.id;
|
|
243
656
|
return {
|
|
@@ -248,6 +661,17 @@ function bindRequestedMember(requested, candidates, kind) {
|
|
|
248
661
|
};
|
|
249
662
|
}
|
|
250
663
|
}
|
|
664
|
+
const namedWinner = memberNamedInQuestion(ids, eligibleCandidates, question);
|
|
665
|
+
if (namedWinner) {
|
|
666
|
+
const winnerCandidate = directCandidates.find((candidate) => (candidate.qualifiedId ?? candidate.id) === namedWinner);
|
|
667
|
+
return {
|
|
668
|
+
requested,
|
|
669
|
+
qualifiedId: namedWinner,
|
|
670
|
+
...(winnerCandidate?.aggregation ? { aggregation: winnerCandidate.aggregation } : {}),
|
|
671
|
+
status: 'resolved',
|
|
672
|
+
candidateIds: [namedWinner],
|
|
673
|
+
};
|
|
674
|
+
}
|
|
251
675
|
const direct = ids.length === 1
|
|
252
676
|
? directCandidates.find((candidate) => (candidate.qualifiedId ?? candidate.id) === ids[0])
|
|
253
677
|
: undefined;
|
|
@@ -316,7 +740,12 @@ function resolveCapability(resolution, execution, measures, dimensions, filters)
|
|
|
316
740
|
return 'blocked';
|
|
317
741
|
}
|
|
318
742
|
function normalize(value) {
|
|
319
|
-
return value.toLowerCase()
|
|
743
|
+
return value.toLowerCase()
|
|
744
|
+
.replace(/%/g, ' percentage ')
|
|
745
|
+
.replace(/[_./:-]+/g, ' ')
|
|
746
|
+
.replace(/[^a-z0-9 ]+/g, ' ')
|
|
747
|
+
.replace(/\s+/g, ' ')
|
|
748
|
+
.trim();
|
|
320
749
|
}
|
|
321
750
|
/** Resolve common analytical ranges once so no executor reinterprets "last month". */
|
|
322
751
|
export function resolvePlanTimeRange(expression, referenceTime) {
|