@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
package/dist/analytical-frame.js
CHANGED
|
@@ -35,9 +35,18 @@ export function projectResolvedAnalyticalFrame(input) {
|
|
|
35
35
|
? [binding.qualifiedId]
|
|
36
36
|
: []));
|
|
37
37
|
const rankingRequested = Boolean(sourceFrame.ranking || plan.query.order || plan.query.limit !== undefined);
|
|
38
|
+
const sourceRankEntityDimensionId = sourceFrame.ranking?.entityDimensionId;
|
|
38
39
|
const dimensions = groupedDimensionIds.flatMap((dimensionId, index) => [
|
|
39
40
|
{ dimensionId, role: 'group_by' },
|
|
40
|
-
|
|
41
|
+
// Preserve the source frame's rank entity. Reassigning rank ownership to
|
|
42
|
+
// the first grouped dimension silently changed "top customers by product
|
|
43
|
+
// category" into "top product categories", then the compatibility guard
|
|
44
|
+
// correctly rejected the category for lacking `rank_entity` support.
|
|
45
|
+
...(rankingRequested && (sourceRankEntityDimensionId
|
|
46
|
+
? dimensionId === sourceRankEntityDimensionId
|
|
47
|
+
: index === 0)
|
|
48
|
+
? [{ dimensionId, role: 'rank_entity' }]
|
|
49
|
+
: []),
|
|
41
50
|
]);
|
|
42
51
|
for (const filter of plan.query.filters) {
|
|
43
52
|
const dimensionId = filter.binding.status === 'resolved' ? filter.binding.qualifiedId : undefined;
|
|
@@ -48,11 +57,97 @@ export function projectResolvedAnalyticalFrame(input) {
|
|
|
48
57
|
if (sourceFrame.timeContext?.timeDimensionId) {
|
|
49
58
|
dimensions.push({ dimensionId: sourceFrame.timeContext.timeDimensionId, role: 'time_axis' });
|
|
50
59
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
// Keep an unresolved host-built ambiguity intact while projecting the RAP.
|
|
61
|
+
// Its candidate IDs came from the selected metric's native capability and
|
|
62
|
+
// are the only valid structured clarification choices. Reconstructing only
|
|
63
|
+
// from a failed lexical RAP rebind turns two viable `billing account` /
|
|
64
|
+
// `service account` meanings into an empty generic gap. If RAP resolved the
|
|
65
|
+
// corresponding binding, the authoritative plan supersedes the source
|
|
66
|
+
// ambiguity as usual.
|
|
67
|
+
// Ambiguity is lane-scoped. A physical/semantic ID may legitimately occur
|
|
68
|
+
// in more than one role, but a group-by binding must never consume a filter
|
|
69
|
+
// clarification (or vice versa). Keep the source-frame and frozen RAP proof
|
|
70
|
+
// sets separated by the meaning lane before intersecting them below.
|
|
71
|
+
const resolvedPlanBindingIds = {
|
|
72
|
+
metrics: new Set(plan.query.measures.flatMap((binding) => binding.status === 'resolved' && binding.qualifiedId ? [binding.qualifiedId] : [])),
|
|
73
|
+
dimensions: new Set(plan.query.dimensions.flatMap((binding) => binding.status === 'resolved' && binding.qualifiedId ? [binding.qualifiedId] : [])),
|
|
74
|
+
filters: new Set(plan.query.filters.flatMap((filter) => filter.binding.status === 'resolved' && filter.binding.qualifiedId ? [filter.binding.qualifiedId] : [])),
|
|
75
|
+
};
|
|
76
|
+
const sourceFrameBindingIds = {
|
|
77
|
+
metrics: new Set(sourceFrame.metricConceptIds),
|
|
78
|
+
dimensions: new Set(sourceFrame.dimensions
|
|
79
|
+
.filter((binding) => binding.role !== 'filter')
|
|
80
|
+
.map((binding) => binding.dimensionId)),
|
|
81
|
+
filters: new Set([
|
|
82
|
+
...sourceFrame.dimensions
|
|
83
|
+
.filter((binding) => binding.role === 'filter')
|
|
84
|
+
.map((binding) => binding.dimensionId),
|
|
85
|
+
...sourceFrame.memberBindings.map((binding) => binding.dimensionId),
|
|
86
|
+
]),
|
|
87
|
+
};
|
|
88
|
+
const resolvedSourceAmbiguityKeys = new Set();
|
|
89
|
+
const unresolvedSourceAmbiguity = sourceFrame.ambiguity.filter((entry) => {
|
|
90
|
+
const [lane, ...parts] = entry.field.split('.');
|
|
91
|
+
const requested = normalize(parts.join('.'));
|
|
92
|
+
if (!requested)
|
|
93
|
+
return true;
|
|
94
|
+
// A server-issued clarification selection is already represented as one
|
|
95
|
+
// exact capability binding in the host-built frame and the RAP. Do not
|
|
96
|
+
// retain the lexical ambiguity that prompted that selection merely because
|
|
97
|
+
// the original natural-language phrase (for example, "name") is still in
|
|
98
|
+
// the audit query list. This is intentionally identity-based rather than
|
|
99
|
+
// label-based and applies equally to metric, dimension, and filter lanes.
|
|
100
|
+
// The exact offered ID must be resolved in that same lane on both sides;
|
|
101
|
+
// a genuine ambiguity remains when zero or more than one offered candidate
|
|
102
|
+
// is resolved in the frozen tuple.
|
|
103
|
+
const laneBindingIds = lane === 'metrics' || lane === 'dimensions' || lane === 'filters'
|
|
104
|
+
? { source: sourceFrameBindingIds[lane], plan: resolvedPlanBindingIds[lane] }
|
|
105
|
+
: undefined;
|
|
106
|
+
const exactlyResolvedCandidateIds = laneBindingIds
|
|
107
|
+
? [...new Set(entry.candidateIds.filter((candidateId) => laneBindingIds.source.has(candidateId)
|
|
108
|
+
&& laneBindingIds.plan.has(candidateId)))]
|
|
109
|
+
: [];
|
|
110
|
+
if (exactlyResolvedCandidateIds.length === 1) {
|
|
111
|
+
resolvedSourceAmbiguityKeys.add(`${lane}.${requested}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
// Candidate-bearing source ambiguity is an identity contract. A RAP
|
|
115
|
+
// binding with the same lexical request may be an unrelated third field;
|
|
116
|
+
// it cannot clear the offered A/B choice. Retain that ambiguity unless the
|
|
117
|
+
// exact candidate proof above resolved one and only one offered ID.
|
|
118
|
+
// Legacy source ambiguity without candidates has no such identity proof,
|
|
119
|
+
// so it retains the historical lexical fallback below.
|
|
120
|
+
if (entry.candidateIds.length > 0)
|
|
121
|
+
return true;
|
|
122
|
+
const binding = lane === 'dimensions'
|
|
123
|
+
? plan.query.dimensions.find((item) => normalize(item.requested) === requested)
|
|
124
|
+
: lane === 'metrics'
|
|
125
|
+
? plan.query.measures.find((item) => normalize(item.requested) === requested)
|
|
126
|
+
: lane === 'filters'
|
|
127
|
+
? plan.query.filters.find((item) => normalize(item.binding.requested) === requested)?.binding
|
|
128
|
+
: undefined;
|
|
129
|
+
return !binding || binding.status !== 'resolved';
|
|
130
|
+
});
|
|
131
|
+
const ambiguity = uniqueAmbiguity([
|
|
132
|
+
...unresolvedSourceAmbiguity,
|
|
133
|
+
// Preserve the original query for audit, but do not let its unresolved
|
|
134
|
+
// lexical binding recreate an ambiguity that was discharged by exactly one
|
|
135
|
+
// of the source-offered IDs. This applies to every binding lane: a metric
|
|
136
|
+
// ambiguity has the same identity contract as a display-dimension one.
|
|
137
|
+
...plan.query.measures.flatMap((binding) => resolvedSourceAmbiguityKeys.has(`metrics.${normalize(binding.requested)}`)
|
|
138
|
+
? []
|
|
139
|
+
: frameBindingAmbiguity('metrics', binding)),
|
|
140
|
+
// The query list retains the original lexical phrase for audit. Once an
|
|
141
|
+
// exact server-issued candidate resolved the corresponding source
|
|
142
|
+
// ambiguity, that audit binding must not re-create the same ambiguity in
|
|
143
|
+
// the projected frame.
|
|
144
|
+
...plan.query.dimensions.flatMap((binding) => resolvedSourceAmbiguityKeys.has(`dimensions.${normalize(binding.requested)}`)
|
|
145
|
+
? []
|
|
146
|
+
: frameBindingAmbiguity('dimensions', binding)),
|
|
147
|
+
...plan.query.filters.flatMap((filter) => resolvedSourceAmbiguityKeys.has(`filters.${normalize(filter.binding.requested)}`)
|
|
148
|
+
? []
|
|
149
|
+
: frameBindingAmbiguity('filters', filter.binding)),
|
|
150
|
+
]);
|
|
56
151
|
const dimensionOutputs = groupedDimensionIds.map((dimensionId) => ({
|
|
57
152
|
id: localId(dimensionId),
|
|
58
153
|
kind: 'dimension',
|
|
@@ -90,14 +185,7 @@ export function projectResolvedAnalyticalFrame(input) {
|
|
|
90
185
|
metricConceptIds,
|
|
91
186
|
entityGrainIds: plan.entityGrain ? [plan.entityGrain] : sourceFrame.entityGrainIds,
|
|
92
187
|
dimensions,
|
|
93
|
-
memberBindings: plan.query.filters
|
|
94
|
-
? [{
|
|
95
|
-
dimensionId: filter.binding.qualifiedId,
|
|
96
|
-
canonicalValues: [filter.value],
|
|
97
|
-
source: 'question',
|
|
98
|
-
confidence: 'exact',
|
|
99
|
-
}]
|
|
100
|
-
: []),
|
|
188
|
+
memberBindings: memberBindingsForResolvedPlanFilters(plan.query.filters),
|
|
101
189
|
...(ranking ? { ranking } : {}),
|
|
102
190
|
requestedOutputs,
|
|
103
191
|
ambiguity,
|
|
@@ -114,9 +202,47 @@ function frameBindingAmbiguity(lane, binding) {
|
|
|
114
202
|
: `${lane.slice(0, -1).toUpperCase()}_UNRESOLVED`,
|
|
115
203
|
}];
|
|
116
204
|
}
|
|
205
|
+
function uniqueAmbiguity(entries) {
|
|
206
|
+
return entries.filter((entry, index, all) => all.findIndex((candidate) => candidate.field === entry.field
|
|
207
|
+
&& candidate.reasonCode === entry.reasonCode
|
|
208
|
+
&& candidate.candidateIds.join('\u0000') === entry.candidateIds.join('\u0000')) === index);
|
|
209
|
+
}
|
|
117
210
|
function uniqueOutputs(outputs) {
|
|
118
211
|
return outputs.filter((output, index, all) => all.findIndex((candidate) => candidate.id === output.id) === index);
|
|
119
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* A plural prior-result continuation is one membership predicate (`IN`), not
|
|
215
|
+
* ten independent equality predicates. Both the deterministic and resolved
|
|
216
|
+
* frame paths use this helper so a persisted member set cannot become an
|
|
217
|
+
* impossible `customer_name = A AND customer_name = B` condition after a
|
|
218
|
+
* restart.
|
|
219
|
+
*/
|
|
220
|
+
function appendMemberBinding(bindings, next) {
|
|
221
|
+
const index = bindings.findIndex((binding) => binding.dimensionId === next.dimensionId);
|
|
222
|
+
if (index < 0) {
|
|
223
|
+
bindings.push({ ...next, canonicalValues: unique(next.canonicalValues) });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const existing = bindings[index];
|
|
227
|
+
bindings[index] = {
|
|
228
|
+
...existing,
|
|
229
|
+
canonicalValues: unique([...existing.canonicalValues, ...next.canonicalValues]),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function memberBindingsForResolvedPlanFilters(filters) {
|
|
233
|
+
const bindings = [];
|
|
234
|
+
for (const filter of filters) {
|
|
235
|
+
if (filter.binding.status !== 'resolved' || !filter.binding.qualifiedId)
|
|
236
|
+
continue;
|
|
237
|
+
appendMemberBinding(bindings, {
|
|
238
|
+
dimensionId: filter.binding.qualifiedId,
|
|
239
|
+
canonicalValues: [filter.value],
|
|
240
|
+
source: 'question',
|
|
241
|
+
confidence: 'exact',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return bindings;
|
|
245
|
+
}
|
|
120
246
|
function unique(values) {
|
|
121
247
|
return [...new Set(values)];
|
|
122
248
|
}
|
|
@@ -145,11 +271,26 @@ export function buildDeterministicAnalyticalFrame(input) {
|
|
|
145
271
|
...(input.evidence.parsedIntent?.order ? { order: input.evidence.parsedIntent.order } : {}),
|
|
146
272
|
...(input.evidence.parsedIntent?.limit !== undefined ? { limit: input.evidence.parsedIntent.limit } : {}),
|
|
147
273
|
};
|
|
274
|
+
const rankingRequested = meaningType === 'ranking' || queryIntent.limit !== undefined || /\b(top|bottom|highest|lowest|rank)\b/i.test(input.question);
|
|
275
|
+
const requestedEntityTerms = unique(input.entityTerms ?? []).map(normalize).filter(Boolean);
|
|
276
|
+
const requestedEntityDisplayTerms = unique(input.entityDisplayTerms ?? []).map(normalize).filter(Boolean);
|
|
148
277
|
const requestedDimensionTerms = new Set(queryIntent.dimensions.map(normalize).filter(Boolean));
|
|
278
|
+
// The immutable host seed has already separated the entity role from the
|
|
279
|
+
// categorical lane. Retain this defensive removal for older callers whose
|
|
280
|
+
// query intent still contains both forms; otherwise a generic `customer`
|
|
281
|
+
// candidate can manufacture a false ambiguity among Customer Name, Customer
|
|
282
|
+
// Type, and Customer Order Number.
|
|
283
|
+
for (const term of [...requestedEntityTerms, ...requestedEntityDisplayTerms]) {
|
|
284
|
+
requestedDimensionTerms.delete(term);
|
|
285
|
+
}
|
|
149
286
|
for (const filter of queryIntent.filters)
|
|
150
287
|
requestedDimensionTerms.add(normalize(filter.field));
|
|
151
288
|
for (const candidate of input.candidates) {
|
|
152
|
-
|
|
289
|
+
// Entity/member cards may be useful to the bounded meaning call, but they
|
|
290
|
+
// are not dimension identities. Letting a selected Customer entity card
|
|
291
|
+
// pass through this loop reintroduced its broad lexical name as a generic
|
|
292
|
+
// dimension request and made every customer-* capability child compete.
|
|
293
|
+
if (candidate.kind !== 'semantic_member' || candidate.semanticObjectType !== 'dimension')
|
|
153
294
|
continue;
|
|
154
295
|
const capabilityDimension = resolveCapabilityDimension(candidate.qualifiedId ?? candidate.id, capability);
|
|
155
296
|
if (!capabilityDimension)
|
|
@@ -161,7 +302,81 @@ export function buildDeterministicAnalyticalFrame(input) {
|
|
|
161
302
|
}
|
|
162
303
|
const ambiguity = [];
|
|
163
304
|
const resolvedDimensions = new Map();
|
|
305
|
+
const structuredSelectedDimensionIds = new Set();
|
|
306
|
+
// A ranking may group by more than one field, but only the explicitly
|
|
307
|
+
// resolved entity display key is the entity being ranked. Keep that role
|
|
308
|
+
// separate from generic rankable capability metadata: adapters sometimes
|
|
309
|
+
// advertise `rank_entity` for a categorical grouping such as product type,
|
|
310
|
+
// which permits ranking *by* that field but does not make it an answer to
|
|
311
|
+
// “top customers by product category”.
|
|
312
|
+
const entityDisplayDimensionIds = new Set();
|
|
313
|
+
// A structured dimension choice is already a qualified, server-bound
|
|
314
|
+
// meaning. Bind it only through the metric's authored capability contract;
|
|
315
|
+
// this is deliberately stronger than adding the option label back into the
|
|
316
|
+
// natural-language question and prevents a selected member from inventing a
|
|
317
|
+
// join or a display role it does not own.
|
|
318
|
+
for (const selectedId of unique(input.selectedDimensionIds ?? [])) {
|
|
319
|
+
// A model may select a supplied semantic entity card to identify the
|
|
320
|
+
// entity. It is not a qualified display dimension and must never resolve
|
|
321
|
+
// by lexical fallback to the first same-named capability child.
|
|
322
|
+
const dimension = resolveExactCapabilityDimension(selectedId, capability);
|
|
323
|
+
if (dimension) {
|
|
324
|
+
resolvedDimensions.set(dimension.dimensionId, dimension);
|
|
325
|
+
structuredSelectedDimensionIds.add(dimension.dimensionId);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for (const requested of requestedEntityDisplayTerms) {
|
|
329
|
+
// A structured clarification click is a server-issued, capability-bound
|
|
330
|
+
// answer to this exact display-key ambiguity. Once it has proved a single
|
|
331
|
+
// native display/rank field for the requested label, do not recreate the
|
|
332
|
+
// original Account Name vs Customer Name ambiguity from the broad word
|
|
333
|
+
// "names". The selected field was already checked against this metric's
|
|
334
|
+
// authored capability above; this branch only consumes that binding.
|
|
335
|
+
const selectedDisplayMatches = [...structuredSelectedDimensionIds]
|
|
336
|
+
.map((dimensionId) => resolvedDimensions.get(dimensionId))
|
|
337
|
+
.filter((dimension) => Boolean(dimension))
|
|
338
|
+
.filter((dimension) => dimension.supportedRoles.includes('group_by')
|
|
339
|
+
&& (rankingRequested
|
|
340
|
+
? dimension.supportedRoles.includes('rank_entity')
|
|
341
|
+
: dimension.supportedRoles.includes('display')));
|
|
342
|
+
if (selectedDisplayMatches.length === 1) {
|
|
343
|
+
entityDisplayDimensionIds.add(selectedDisplayMatches[0].dimensionId);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const matches = resolveEntityDisplayTerm({
|
|
347
|
+
requested,
|
|
348
|
+
entityTerms: requestedEntityTerms,
|
|
349
|
+
capability,
|
|
350
|
+
rankingRequested,
|
|
351
|
+
});
|
|
352
|
+
if (matches.length === 1) {
|
|
353
|
+
resolvedDimensions.set(matches[0].dimensionId, matches[0]);
|
|
354
|
+
entityDisplayDimensionIds.add(matches[0].dimensionId);
|
|
355
|
+
}
|
|
356
|
+
else if (matches.length > 1) {
|
|
357
|
+
ambiguity.push({
|
|
358
|
+
field: `dimensions.${requested}`,
|
|
359
|
+
candidateIds: matches.map((dimension) => dimension.dimensionId).sort(),
|
|
360
|
+
reasonCode: 'DIMENSION_AMBIGUOUS',
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
164
364
|
for (const requested of requestedDimensionTerms) {
|
|
365
|
+
// Generic parser terms such as `name` can match more than one capability
|
|
366
|
+
// field. A server-issued qualified selection has already resolved that
|
|
367
|
+
// exact ambiguity, so preserve its native field rather than re-opening
|
|
368
|
+
// the broad lexical candidate set on a restart/follow-up.
|
|
369
|
+
const selectedMatches = [...structuredSelectedDimensionIds]
|
|
370
|
+
.map((dimensionId) => resolvedDimensions.get(dimensionId))
|
|
371
|
+
.filter((dimension) => Boolean(dimension))
|
|
372
|
+
.filter((dimension) => dimensionMatchesTerm(dimension, requested)
|
|
373
|
+
|| selectedSameSnapshotDimensionBindsRequestedTerm({
|
|
374
|
+
dimension,
|
|
375
|
+
requested,
|
|
376
|
+
candidates: input.candidates,
|
|
377
|
+
}));
|
|
378
|
+
if (selectedMatches.length === 1)
|
|
379
|
+
continue;
|
|
165
380
|
const matches = resolveDimensionTerm(requested, capability, input.candidates);
|
|
166
381
|
if (matches.length === 1)
|
|
167
382
|
resolvedDimensions.set(matches[0].dimensionId, matches[0]);
|
|
@@ -182,14 +397,13 @@ export function buildDeterministicAnalyticalFrame(input) {
|
|
|
182
397
|
const dimension = matches[0];
|
|
183
398
|
filterDimensionIds.add(dimension.dimensionId);
|
|
184
399
|
resolvedDimensions.set(dimension.dimensionId, dimension);
|
|
185
|
-
memberBindings
|
|
400
|
+
appendMemberBinding(memberBindings, {
|
|
186
401
|
dimensionId: dimension.dimensionId,
|
|
187
402
|
canonicalValues: [filter.value],
|
|
188
403
|
source: 'question',
|
|
189
404
|
confidence: 'exact',
|
|
190
405
|
});
|
|
191
406
|
}
|
|
192
|
-
const rankingRequested = meaningType === 'ranking' || queryIntent.limit !== undefined || /\b(top|bottom|highest|lowest|rank)\b/i.test(input.question);
|
|
193
407
|
const groupRequested = rankingRequested || /\b(by|per|for each|breakdown)\b/i.test(input.question);
|
|
194
408
|
const dimensions = [];
|
|
195
409
|
for (const dimension of resolvedDimensions.values()) {
|
|
@@ -200,7 +414,10 @@ export function buildDeterministicAnalyticalFrame(input) {
|
|
|
200
414
|
}
|
|
201
415
|
if (dimension.supportedRoles.includes('group_by')) {
|
|
202
416
|
dimensions.push({ dimensionId: dimension.dimensionId, role: 'group_by' });
|
|
203
|
-
|
|
417
|
+
const isExplicitEntityDisplay = entityDisplayDimensionIds.has(dimension.dimensionId);
|
|
418
|
+
if (rankingRequested
|
|
419
|
+
&& dimension.supportedRoles.includes('rank_entity')
|
|
420
|
+
&& (entityDisplayDimensionIds.size === 0 || isExplicitEntityDisplay)) {
|
|
204
421
|
dimensions.push({
|
|
205
422
|
dimensionId: dimension.dimensionId,
|
|
206
423
|
role: 'rank_entity',
|
|
@@ -211,10 +428,20 @@ export function buildDeterministicAnalyticalFrame(input) {
|
|
|
211
428
|
dimensions.push({ dimensionId: dimension.dimensionId, role: 'filter' });
|
|
212
429
|
}
|
|
213
430
|
}
|
|
214
|
-
|
|
215
|
-
|
|
431
|
+
// `current` can be an intrinsic business qualifier ("current BCM run
|
|
432
|
+
// rate"), not a request for a date axis. Creating a timeContext for that
|
|
433
|
+
// bare modifier forces every metric through time-dimension compatibility and
|
|
434
|
+
// incorrectly blocks snapshot metrics that already encode their current/as-
|
|
435
|
+
// of semantics. Only require a declared time role for an actual time grain
|
|
436
|
+
// or temporal filter. The metric's own governed definition remains the
|
|
437
|
+
// authority for an unqualified current/latest snapshot value.
|
|
438
|
+
const requestedTimeGrain = explicitTimeGrain(queryIntent.timeGrain) ?? inferTimeGrain(input.question);
|
|
439
|
+
const timeRequested = Boolean(queryIntent.timeRange && !isSnapshotOnlyTimeRange(queryIntent.timeRange))
|
|
440
|
+
|| Boolean(requestedTimeGrain)
|
|
441
|
+
|| hasExplicitTemporalFilter(input.question);
|
|
216
442
|
const previousYearRequested = /\b(last year|previous year|year over year|yoy)\b/i.test(input.question);
|
|
217
|
-
const currentRequested = /\b(today|current
|
|
443
|
+
const currentRequested = /\b(?:today|yesterday)\b|\b(?:this|current)\s+(?:day|week|month|quarter|year)\b|\b(?:month|quarter|year)[ -]to[ -]date\b|\b(?:mtd|qtd|ytd)\b/i.test(input.question)
|
|
444
|
+
|| previousYearRequested;
|
|
218
445
|
const timeContext = timeRequested
|
|
219
446
|
? {
|
|
220
447
|
...(capability.timeDimensions.length === 1
|
|
@@ -223,7 +450,7 @@ export function buildDeterministicAnalyticalFrame(input) {
|
|
|
223
450
|
timeRole: capability.timeDimensions[0].role,
|
|
224
451
|
}
|
|
225
452
|
: {}),
|
|
226
|
-
grain:
|
|
453
|
+
...(requestedTimeGrain ? { grain: requestedTimeGrain } : {}),
|
|
227
454
|
...(capability.freshness?.defaultCompletenessPolicy
|
|
228
455
|
? {
|
|
229
456
|
completenessPolicy: capability.freshness.defaultCompletenessPolicy,
|
|
@@ -347,6 +574,68 @@ export function buildDeterministicAnalyticalFrame(input) {
|
|
|
347
574
|
ambiguity,
|
|
348
575
|
};
|
|
349
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* Resolve a selected semantic member through one metric's authored capability
|
|
579
|
+
* contract. Router validation uses this to compose a persisted metric frame
|
|
580
|
+
* with a selected display/grouping field without treating the field itself as
|
|
581
|
+
* a metric meaning.
|
|
582
|
+
*/
|
|
583
|
+
export function resolveMetricCapabilityDimension(metricCandidate, selectedDimensionId) {
|
|
584
|
+
const capability = normalizeMetricCapabilityContract(metricCandidate.analyticalCapability);
|
|
585
|
+
return capability ? resolveExactCapabilityDimension(selectedDimensionId, capability) : undefined;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Prove the narrowly-scoped same-snapshot MetricFlow extension contract.
|
|
589
|
+
*
|
|
590
|
+
* This is intentionally a pure capability check shared by the router,
|
|
591
|
+
* frozen-plan compiler gate, and Ask workspace admission. An extension is
|
|
592
|
+
* not a lexical synonym: it is valid only when the currently eligible metric
|
|
593
|
+
* declares the exact (or protocol-alias-equivalent) semantic member as a
|
|
594
|
+
* `group_by` dimension in its normalized capability. Keeping this one proof
|
|
595
|
+
* in front of every authority boundary prevents an old, mismatched, or
|
|
596
|
+
* hand-shaped card from entering the planner package and later looking like
|
|
597
|
+
* compiler-approved evidence.
|
|
598
|
+
*/
|
|
599
|
+
export function proveSameSnapshotMetricflowRoleExtensionV1(input) {
|
|
600
|
+
const { candidate, metricCandidate } = input;
|
|
601
|
+
const extension = candidate.sameSnapshotRoleExtension;
|
|
602
|
+
if (!extension
|
|
603
|
+
|| extension.version !== 1
|
|
604
|
+
|| extension.role !== 'categorical_dimension'
|
|
605
|
+
|| (extension.basis !== 'sole_metricflow_grouping_dimension'
|
|
606
|
+
&& extension.basis !== 'exact_metricflow_grouping_dimension')
|
|
607
|
+
|| !extension.requestedTerm.trim()
|
|
608
|
+
|| !extension.metricId.trim()
|
|
609
|
+
|| !extension.dimensionId.trim()
|
|
610
|
+
|| candidate.kind !== 'semantic_member'
|
|
611
|
+
|| (candidate.semanticObjectType !== undefined && candidate.semanticObjectType !== 'dimension')
|
|
612
|
+
|| candidate.eligible === false
|
|
613
|
+
|| candidate.compatibility === 'incompatible'
|
|
614
|
+
|| metricCandidate.kind !== 'semantic_metric'
|
|
615
|
+
|| metricCandidate.eligible === false
|
|
616
|
+
|| metricCandidate.compatibility === 'incompatible')
|
|
617
|
+
return undefined;
|
|
618
|
+
const capability = normalizeMetricCapabilityContract(metricCandidate.analyticalCapability);
|
|
619
|
+
if (!capability)
|
|
620
|
+
return undefined;
|
|
621
|
+
const metricAuthorityIds = new Set([
|
|
622
|
+
metricCandidate.id,
|
|
623
|
+
metricCandidate.qualifiedId,
|
|
624
|
+
capability.metricId,
|
|
625
|
+
...capability.measureIds,
|
|
626
|
+
].filter((id) => Boolean(id)));
|
|
627
|
+
if (!metricAuthorityIds.has(extension.metricId))
|
|
628
|
+
return undefined;
|
|
629
|
+
const candidateIdentity = candidate.qualifiedId ?? candidate.id;
|
|
630
|
+
if (!sameCapabilityDimensionIdentity(candidateIdentity, extension.dimensionId))
|
|
631
|
+
return undefined;
|
|
632
|
+
const dimension = resolveExactCapabilityDimension(extension.dimensionId, capability);
|
|
633
|
+
if (!dimension
|
|
634
|
+
|| !sameCapabilityDimensionIdentity(dimension.dimensionId, extension.dimensionId)
|
|
635
|
+
|| !dimension.supportedRoles.includes('group_by'))
|
|
636
|
+
return undefined;
|
|
637
|
+
return { capability, dimension };
|
|
638
|
+
}
|
|
350
639
|
function resolveDimensionTerm(requested, capability, candidates) {
|
|
351
640
|
const candidateIds = new Set();
|
|
352
641
|
for (const candidate of candidates) {
|
|
@@ -357,10 +646,153 @@ function resolveDimensionTerm(requested, capability, candidates) {
|
|
|
357
646
|
candidateIds.add(candidate.qualifiedId ?? candidate.id);
|
|
358
647
|
}
|
|
359
648
|
}
|
|
360
|
-
return capability.dimensions.filter((dimension) =>
|
|
649
|
+
return capability.dimensions.filter((dimension) => dimensionMatchesTerm(dimension, requested) || candidateIds.has(dimension.dimensionId));
|
|
361
650
|
}
|
|
362
651
|
function resolveCapabilityDimension(id, capability) {
|
|
363
|
-
|
|
652
|
+
// A server-issued clarification can render the capability child under the
|
|
653
|
+
// stable display namespace `semantic:uncategorized:dimension:` while the
|
|
654
|
+
// same snapshot's executable contract stores its canonical
|
|
655
|
+
// `semantic:dimension:` identity. This is an exact protocol alias, not a
|
|
656
|
+
// leaf-name fallback: normalize only that namespace and require equality
|
|
657
|
+
// with a dimension declared by the selected metric capability. It keeps a
|
|
658
|
+
// persisted display choice usable after reload without allowing a client to
|
|
659
|
+
// substitute a same-named field from another model.
|
|
660
|
+
const canonicalId = canonicalCapabilityDimensionId(id);
|
|
661
|
+
return capability.dimensions.find((dimension) => dimension.dimensionId === canonicalId
|
|
662
|
+
|| dimensionMatchesTerm(dimension, normalize(id)));
|
|
663
|
+
}
|
|
664
|
+
function resolveExactCapabilityDimension(id, capability) {
|
|
665
|
+
const exactId = id.trim();
|
|
666
|
+
const canonicalId = canonicalCapabilityDimensionId(exactId);
|
|
667
|
+
return capability.dimensions.find((dimension) => dimension.dimensionId === exactId
|
|
668
|
+
|| dimension.dimensionId === canonicalId
|
|
669
|
+
// The local manifest adapter may retain the display namespace on the
|
|
670
|
+
// capability itself while a persisted structured option carries the
|
|
671
|
+
// executable namespace (or vice versa). Both normalize to the same
|
|
672
|
+
// protocol identity; no leaf-name or cross-model fallback is allowed.
|
|
673
|
+
|| canonicalCapabilityDimensionId(dimension.dimensionId) === canonicalId);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* A same-snapshot MetricFlow extension is an exact server-side binding from
|
|
677
|
+
* a business term (for example `region`) to one declared capability child
|
|
678
|
+
* (for example `locations.location_name`). Once that child was selected and
|
|
679
|
+
* revalidated above, do not reopen the raw word match merely because the
|
|
680
|
+
* authored field label does not literally contain "region". This is limited
|
|
681
|
+
* to the persisted extension contract and exact capability identity; it is
|
|
682
|
+
* not a geographic synonym or a cross-model fallback.
|
|
683
|
+
*/
|
|
684
|
+
function selectedSameSnapshotDimensionBindsRequestedTerm(input) {
|
|
685
|
+
const requested = normalize(input.requested);
|
|
686
|
+
if (!requested)
|
|
687
|
+
return false;
|
|
688
|
+
return input.candidates.some((candidate) => {
|
|
689
|
+
const extension = candidate.sameSnapshotRoleExtension;
|
|
690
|
+
if (!extension
|
|
691
|
+
|| extension.version !== 1
|
|
692
|
+
|| extension.role !== 'categorical_dimension'
|
|
693
|
+
|| normalize(extension.requestedTerm) !== requested)
|
|
694
|
+
return false;
|
|
695
|
+
const candidateIdentity = candidate.qualifiedId ?? candidate.id;
|
|
696
|
+
return sameCapabilityDimensionIdentity(candidateIdentity, input.dimension.dimensionId)
|
|
697
|
+
&& sameCapabilityDimensionIdentity(extension.dimensionId, input.dimension.dimensionId);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Resolve a host-owned entity display requirement only through the selected
|
|
702
|
+
* metric's authored native capability. A bare entity card is not a display
|
|
703
|
+
* field, and an arbitrary categorical attribute such as `customer_type`
|
|
704
|
+
* cannot satisfy `top customers`. If two native display/rank fields match the
|
|
705
|
+
* exact display phrase, retain a genuine typed ambiguity instead of guessing.
|
|
706
|
+
*/
|
|
707
|
+
function resolveEntityDisplayTerm(input) {
|
|
708
|
+
// New semantic indexes expose `display` explicitly, while a compatible
|
|
709
|
+
// older MetricFlow capability may expose only `rank_entity`. Both describe
|
|
710
|
+
// a metric-native entity result role. Do not relax this to every group-by
|
|
711
|
+
// field: a customer type or owner grouping is still not an answer to
|
|
712
|
+
// “top customers”.
|
|
713
|
+
const nativeEntityDimensions = input.capability.dimensions.filter((dimension) => dimension.supportedRoles.includes('group_by')
|
|
714
|
+
&& (input.rankingRequested
|
|
715
|
+
? dimension.supportedRoles.includes('rank_entity')
|
|
716
|
+
: dimension.supportedRoles.includes('display'))
|
|
717
|
+
&& (input.entityTerms.length === 0 || input.entityTerms.some((entityTerm) => termsMatch(normalize(dimension.entityId), entityTerm))));
|
|
718
|
+
// A requirement seed can carry the full capability-qualified display
|
|
719
|
+
// identity after local retrieval (`customers.customer_name`). Prefer that
|
|
720
|
+
// exact identity before the friendly-label matcher below: an older local
|
|
721
|
+
// index may also expose the unscoped display alias (`customer_name`) as a
|
|
722
|
+
// second card, but it is not a second business meaning when the selected
|
|
723
|
+
// capability identifies one exact native child. This is intentionally an
|
|
724
|
+
// identifier comparison, never a leaf-name fallback across models.
|
|
725
|
+
const normalizedRequested = normalize(input.requested);
|
|
726
|
+
const exactQualifiedMatches = nativeEntityDimensions.filter((dimension) => normalize(dimension.dimensionId) === normalizedRequested
|
|
727
|
+
|| (dimension.aliases ?? []).some((alias) => normalize(alias) === normalizedRequested));
|
|
728
|
+
if (exactQualifiedMatches.length > 0)
|
|
729
|
+
return exactQualifiedMatches;
|
|
730
|
+
const exactDisplayMatches = nativeEntityDimensions.filter((dimension) => dimensionMatchesTerm(dimension, input.requested));
|
|
731
|
+
if (exactDisplayMatches.length > 0)
|
|
732
|
+
return exactDisplayMatches;
|
|
733
|
+
// `top customers` is host-normalized to the output requirement `customer
|
|
734
|
+
// name`. A legacy capability can still prove that requirement when it has a
|
|
735
|
+
// unique native entity field literally named `customer`. Do not let a
|
|
736
|
+
// broader lexical match turn `customer_type`, `customer_owner`, or
|
|
737
|
+
// `customer_order_number` into that label. If no exact native identity
|
|
738
|
+
// exists, preserve only non-attribute entity alternatives so two genuine
|
|
739
|
+
// authored account paths remain an identifier-bound ambiguity.
|
|
740
|
+
const entityNativeMatches = nativeEntityDimensions.filter((dimension) => input.entityTerms.some((entityTerm) => nativeEntityIdentity(dimension, entityTerm) === 'exact'));
|
|
741
|
+
if (entityNativeMatches.length > 0)
|
|
742
|
+
return entityNativeMatches;
|
|
743
|
+
return nativeEntityDimensions.filter((dimension) => input.entityTerms.some((entityTerm) => nativeEntityIdentity(dimension, entityTerm) === 'qualified'));
|
|
744
|
+
}
|
|
745
|
+
function nativeEntityIdentity(dimension, entityTerm) {
|
|
746
|
+
const normalizedEntity = normalize(entityTerm);
|
|
747
|
+
if (!normalizedEntity)
|
|
748
|
+
return undefined;
|
|
749
|
+
// Use the field's own label/qualified leaf for the entity-key fallback.
|
|
750
|
+
// Retrieval aliases are deliberately excluded here: an attribute card may
|
|
751
|
+
// carry the broad entity word as a discovery alias (`customer_type` ↔
|
|
752
|
+
// `customer`) but that does not make the attribute the entity display key.
|
|
753
|
+
const identities = [
|
|
754
|
+
dimension.label,
|
|
755
|
+
localId(dimension.dimensionId),
|
|
756
|
+
].map((value) => value ? normalize(value) : '').filter(Boolean);
|
|
757
|
+
if (identities.some((identity) => identity === normalizedEntity))
|
|
758
|
+
return 'exact';
|
|
759
|
+
// A field whose own identity begins with the generic entity name is an
|
|
760
|
+
// attribute of that entity, not its display/rank key. It must not satisfy a
|
|
761
|
+
// request for the entity itself merely because it is the only field left.
|
|
762
|
+
if (identities.some((identity) => identity.startsWith(`${normalizedEntity} `)))
|
|
763
|
+
return undefined;
|
|
764
|
+
// A qualified authored identity such as `billing account` or `service
|
|
765
|
+
// account` is a genuine alternative entity role. Return it only to create a
|
|
766
|
+
// typed ambiguity when there is no exact native entity key.
|
|
767
|
+
return identities.some((identity) => identity.endsWith(` ${normalizedEntity}`))
|
|
768
|
+
? 'qualified'
|
|
769
|
+
: undefined;
|
|
770
|
+
}
|
|
771
|
+
function canonicalCapabilityDimensionId(id) {
|
|
772
|
+
return id.trim().replace(/^semantic:uncategorized:dimension:/, 'semantic:dimension:');
|
|
773
|
+
}
|
|
774
|
+
function sameCapabilityDimensionIdentity(left, right) {
|
|
775
|
+
return canonicalCapabilityDimensionId(left) === canonicalCapabilityDimensionId(right);
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Match a requested business dimension against the dimension's own authored
|
|
779
|
+
* label/leaf alias, not its model-qualified namespace. A metric model named
|
|
780
|
+
* `account_revenue` prefixes every one of its dimensions; treating that
|
|
781
|
+
* prefix as a match for "account" made fiscal period and customer name look
|
|
782
|
+
* like equally valid account display keys.
|
|
783
|
+
*/
|
|
784
|
+
function dimensionMatchesTerm(dimension, requested) {
|
|
785
|
+
const normalizedRequested = normalize(requested);
|
|
786
|
+
if (!normalizedRequested)
|
|
787
|
+
return false;
|
|
788
|
+
if (normalize(dimension.dimensionId) === normalizedRequested)
|
|
789
|
+
return true;
|
|
790
|
+
const identities = [
|
|
791
|
+
dimension.label,
|
|
792
|
+
localId(dimension.dimensionId),
|
|
793
|
+
...(dimension.aliases ?? []).map(localId),
|
|
794
|
+
].map((value) => value ? normalize(value) : '').filter(Boolean);
|
|
795
|
+
return identities.some((identity) => termsMatch(identity, normalizedRequested));
|
|
364
796
|
}
|
|
365
797
|
function phraseAppears(question, phrase) {
|
|
366
798
|
const questionTokens = new Set(normalize(question).split(' ').map(singularize));
|
|
@@ -368,7 +800,12 @@ function phraseAppears(question, phrase) {
|
|
|
368
800
|
.split(' ')
|
|
369
801
|
.map(singularize)
|
|
370
802
|
.filter((token) => token.length >= 3 && !['name', 'dimension'].includes(token));
|
|
371
|
-
|
|
803
|
+
// Candidate aliases such as `customer name` must not be promoted into a
|
|
804
|
+
// separately requested dimension merely because the question says
|
|
805
|
+
// `customer`. The host seed already owns the display role. Requiring every
|
|
806
|
+
// meaningful token makes this an explicit phrase-presence check while
|
|
807
|
+
// retaining singular/plural normalization.
|
|
808
|
+
return significant.length > 0 && significant.every((token) => questionTokens.has(token));
|
|
372
809
|
}
|
|
373
810
|
function termsMatch(left, right) {
|
|
374
811
|
if (!left || !right)
|
|
@@ -405,13 +842,40 @@ function inferTimeGrain(question) {
|
|
|
405
842
|
if (/\b(week|weekly)\b/i.test(question))
|
|
406
843
|
return 'week';
|
|
407
844
|
if (/\b(month|monthly|mtd)\b/i.test(question))
|
|
408
|
-
return '
|
|
845
|
+
return 'month';
|
|
409
846
|
if (/\b(quarter|quarterly)\b/i.test(question))
|
|
410
847
|
return 'quarter';
|
|
411
848
|
if (/\b(year|yearly|annual|yoy)\b/i.test(question))
|
|
412
849
|
return 'year';
|
|
413
850
|
return undefined;
|
|
414
851
|
}
|
|
852
|
+
function explicitTimeGrain(value) {
|
|
853
|
+
const normalized = value?.trim().toLowerCase();
|
|
854
|
+
return normalized === 'day'
|
|
855
|
+
|| normalized === 'week'
|
|
856
|
+
|| normalized === 'month'
|
|
857
|
+
|| normalized === 'quarter'
|
|
858
|
+
|| normalized === 'year'
|
|
859
|
+
? normalized
|
|
860
|
+
: undefined;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* The parser can represent an explicit time range without preserving every
|
|
864
|
+
* source token. Keep snapshot-only modifiers out of the physical date-role
|
|
865
|
+
* contract: a bare "current" / "latest" metric should be answered by its
|
|
866
|
+
* authored snapshot semantics, not fabricated into a time-axis request.
|
|
867
|
+
*/
|
|
868
|
+
function isSnapshotOnlyTimeRange(value) {
|
|
869
|
+
return /^(?:current|latest|snapshot|as[ -]?of[ -]?(?:latest|current))$/i.test(value.trim());
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Detect temporal filters in the user's actual wording. `current` by itself
|
|
873
|
+
* deliberately does not match: `current BCM run rate` is a metric qualifier,
|
|
874
|
+
* whereas `current month` and `today` require a date role.
|
|
875
|
+
*/
|
|
876
|
+
function hasExplicitTemporalFilter(question) {
|
|
877
|
+
return /\b(?:today|yesterday)\b|\b(?:this|current|last|previous|past)\s+(?:(?:\d{1,3}|one|two|three|four|five|six|seven|eight|nine|ten|twelve)\s+)?(?:day|days|week|weeks|month|months|quarter|quarters|year|years)\b|\b(?:month|quarter|year)[ -]to[ -]date\b|\b(?:mtd|qtd|ytd|last year|previous year|year over year|yoy)\b|\bfy\s?\d{2,4}\b|\bfiscal\s+year\s+\d{2,4}\b/i.test(question);
|
|
878
|
+
}
|
|
415
879
|
function extractLimit(question) {
|
|
416
880
|
const match = /\b(?:top|bottom)\s+(\d{1,4})\b/i.exec(question);
|
|
417
881
|
return match ? Math.max(1, Number(match[1])) : undefined;
|