@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
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
* package and may reference only IDs contained in it. Execution remains owned by
|
|
6
6
|
* the host after it validates the selected candidates and route.
|
|
7
7
|
*/
|
|
8
|
-
import { buildAnalyticalRequirementSet, evidenceCandidateRoles, selectRoleBalancedMeaningCandidates, } from './analytical-orchestration.js';
|
|
8
|
+
import { buildAnalyticalRequirementSet, candidateConflictsWithExplicitRankingMeasure, evidenceCandidateRoles, hasEntityAttributeTerm, isEntityAttributeCandidate, selectRoleBalancedMeaningCandidates, } from './analytical-orchestration.js';
|
|
9
9
|
/**
|
|
10
10
|
* Return the authored output identity that proves a certified block can answer
|
|
11
|
-
* one requested measure.
|
|
12
|
-
* which are populated from declared/output-contract fields at indexing
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* one requested measure. This normally reads only the block's own `output:`
|
|
12
|
+
* facts, which are populated from declared/output-contract fields at indexing
|
|
13
|
+
* time. An exact block-title request may additionally carry a
|
|
14
|
+
* `catalog-proven-output:` fact from the snapshot-local catalog fit. That fact
|
|
15
|
+
* is minted only after the catalog has already bound this exact title to a
|
|
16
|
+
* high-confidence certified answer contract; names, tags, examples,
|
|
17
|
+
* definitions, and unrelated retrieved metrics are intentionally absent.
|
|
15
18
|
*/
|
|
16
19
|
export function certifiedCandidateDeclaredMeasureOutput(candidate, requested) {
|
|
17
20
|
if (candidate.kind !== 'certified_block')
|
|
@@ -20,11 +23,86 @@ export function certifiedCandidateDeclaredMeasureOutput(candidate, requested) {
|
|
|
20
23
|
if (!requestedIdentity)
|
|
21
24
|
return undefined;
|
|
22
25
|
const declared = (candidate.compatibilityFacts ?? [])
|
|
23
|
-
.flatMap((fact) => /^output:\s*(.+)$/i.exec(fact)?.[1] ?? [])
|
|
26
|
+
.flatMap((fact) => /^(?:output|catalog-proven-output):\s*(.+)$/i.exec(fact)?.[1] ?? [])
|
|
24
27
|
.map((output) => output.trim())
|
|
25
28
|
.filter(Boolean);
|
|
26
29
|
return declared.find((output) => canonicalCertifiedOutputMetricIdentity(output) === requestedIdentity);
|
|
27
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Return the block output that proves a requested display/grouping dimension.
|
|
33
|
+
* This is intentionally narrower than retrieval matching: only a declared
|
|
34
|
+
* output or the catalog-proven output bridge for an exact certified title may
|
|
35
|
+
* bind a dimension to the block executor. `customer_name` is a valid output
|
|
36
|
+
* for the business term `customer`; arbitrary attributes such as owner or
|
|
37
|
+
* sentiment are not.
|
|
38
|
+
*/
|
|
39
|
+
export function certifiedCandidateDeclaredDimensionOutput(candidate, requested) {
|
|
40
|
+
if (candidate.kind !== 'certified_block')
|
|
41
|
+
return undefined;
|
|
42
|
+
const requestedIdentity = canonicalCertifiedOutputDimensionIdentity(requested);
|
|
43
|
+
if (!requestedIdentity)
|
|
44
|
+
return undefined;
|
|
45
|
+
const declared = (candidate.compatibilityFacts ?? [])
|
|
46
|
+
.flatMap((fact) => /^(?:output|catalog-proven-output):\s*(.+)$/i.exec(fact)?.[1] ?? [])
|
|
47
|
+
.map((output) => output.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
return declared.find((output) => canonicalCertifiedOutputDimensionIdentity(output) === requestedIdentity);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Return only the declared block dimensions that define its authored result
|
|
53
|
+
* grain. A certified block can return descriptive attributes alongside the
|
|
54
|
+
* grain key without changing the number of result rows: a one-row-per-customer
|
|
55
|
+
* profile may legitimately include both `customer_name` and `customer_type`.
|
|
56
|
+
* Treating every returned attribute as a grouping key rejects that complete
|
|
57
|
+
* customer answer and unnecessarily sends a catalog-proven exact match to the
|
|
58
|
+
* provider planner.
|
|
59
|
+
*
|
|
60
|
+
* The proof is deliberately conservative. When an authored grain fact is
|
|
61
|
+
* present, an output is grain-driving only when its canonical identity is
|
|
62
|
+
* wholly represented by the grain wording. If no output can be tied back to
|
|
63
|
+
* the grain, retain the older all-dimensions behaviour rather than weakening
|
|
64
|
+
* a block with an opaque grain declaration. This keeps a customer-ranked
|
|
65
|
+
* block from answering the scalar request `show me revenue`: `customer_name`
|
|
66
|
+
* is still a grain-driving output that the scalar request did not ask for.
|
|
67
|
+
*/
|
|
68
|
+
export function certifiedCandidateGrainDimensionOutputs(candidate) {
|
|
69
|
+
if (candidate.kind !== 'certified_block')
|
|
70
|
+
return [];
|
|
71
|
+
const declared = [...new Set((candidate.dimensions ?? [])
|
|
72
|
+
.map((dimension) => certifiedCandidateDeclaredDimensionOutput(candidate, dimension))
|
|
73
|
+
.filter((output) => Boolean(output))
|
|
74
|
+
.map((output) => output.trim())
|
|
75
|
+
.filter(Boolean))];
|
|
76
|
+
if (declared.length === 0)
|
|
77
|
+
return [];
|
|
78
|
+
const grainFacts = (candidate.compatibilityFacts ?? [])
|
|
79
|
+
.flatMap((fact) => /^grain:\s*(.+)$/i.exec(fact)?.[1] ?? [])
|
|
80
|
+
.map((grain) => grain.trim())
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
if (grainFacts.length === 0)
|
|
83
|
+
return declared;
|
|
84
|
+
const ignoredGrainTokens = new Set([
|
|
85
|
+
'a', 'an', 'and', 'at', 'by', 'for', 'in', 'of', 'on', 'per', 'row',
|
|
86
|
+
'rows', 'the', 'to', 'with', 'result', 'results', 'record', 'records',
|
|
87
|
+
'ranking', 'rank', 'purchase', 'profile', 'value', 'values',
|
|
88
|
+
]);
|
|
89
|
+
const grainTokenSets = grainFacts.map((grain) => new Set(grain.toLowerCase()
|
|
90
|
+
.replace(/[_./:-]+/g, ' ')
|
|
91
|
+
.replace(/[^a-z0-9 ]+/g, ' ')
|
|
92
|
+
.split(/\s+/)
|
|
93
|
+
.map((token) => token.endsWith('s') && token.length > 3 ? token.slice(0, -1) : token)
|
|
94
|
+
.filter((token) => token.length > 0 && !ignoredGrainTokens.has(token))));
|
|
95
|
+
const grainDriving = declared.filter((output) => {
|
|
96
|
+
const outputTokens = canonicalCertifiedOutputDimensionIdentity(output)
|
|
97
|
+
.split('_')
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
return outputTokens.length > 0 && grainTokenSets.some((tokens) => outputTokens.every((token) => tokens.has(token)));
|
|
100
|
+
});
|
|
101
|
+
// An opaque/legacy grain fact is not enough to waive the existing
|
|
102
|
+
// no-extra-grouping protection. Fall back to every declared dimension if
|
|
103
|
+
// none can be proven as the block's grain key.
|
|
104
|
+
return grainDriving.length > 0 ? grainDriving : declared;
|
|
105
|
+
}
|
|
28
106
|
/**
|
|
29
107
|
* A certified tier can freeze only when the selected block itself declares
|
|
30
108
|
* every requested measure. This is deliberately stricter than retrieval
|
|
@@ -85,6 +163,19 @@ function canonicalCertifiedOutputMetricIdentity(value) {
|
|
|
85
163
|
tokens.pop();
|
|
86
164
|
return tokens.join('_');
|
|
87
165
|
}
|
|
166
|
+
function canonicalCertifiedOutputDimensionIdentity(value) {
|
|
167
|
+
const tokens = value
|
|
168
|
+
.toLowerCase()
|
|
169
|
+
.replace(/[_./:-]+/g, ' ')
|
|
170
|
+
.replace(/[^a-z0-9 ]+/g, ' ')
|
|
171
|
+
.split(/\s+/)
|
|
172
|
+
.map((token) => token.endsWith('s') && token.length > 3 ? token.slice(0, -1) : token)
|
|
173
|
+
.filter(Boolean);
|
|
174
|
+
const displaySuffixes = new Set(['display', 'id', 'key', 'label', 'name', 'title']);
|
|
175
|
+
while (tokens.length > 1 && displaySuffixes.has(tokens.at(-1)))
|
|
176
|
+
tokens.pop();
|
|
177
|
+
return tokens.join('_');
|
|
178
|
+
}
|
|
88
179
|
const TRUST_ORDER = {
|
|
89
180
|
certified: 4,
|
|
90
181
|
semantic: 3,
|
|
@@ -112,6 +203,11 @@ export function buildMeaningEvidencePackage(evidence, maxCandidates = 16, questi
|
|
|
112
203
|
// role-aware admission code ever saw them.
|
|
113
204
|
const canonicalEligible = canonicalizeMetricMeasureCandidates(evidence.candidates)
|
|
114
205
|
.filter((candidate) => candidate.eligible !== false)
|
|
206
|
+
// Preserve the retrieval result for lifecycle/diagnostics, but do not put
|
|
207
|
+
// a correlated non-requested metric in the bounded meaning package. An
|
|
208
|
+
// explicit ranking measure is a typed requirement, not a prompt hint the
|
|
209
|
+
// resolver may replace with a more highly scored BCM/run-rate card.
|
|
210
|
+
.filter((candidate) => !candidateConflictsWithExplicitRankingMeasure(candidate, requirements))
|
|
115
211
|
.sort(compareCandidates);
|
|
116
212
|
const rawPinned = selectRoleBalancedMeaningCandidates({
|
|
117
213
|
candidates: canonicalEligible,
|
|
@@ -124,10 +220,10 @@ export function buildMeaningEvidencePackage(evidence, maxCandidates = 16, questi
|
|
|
124
220
|
// categorical-dimension parser also saw the token "account". An explicit
|
|
125
221
|
// attribute request remains eligible.
|
|
126
222
|
const requestedEntityDisplay = requirements.entityDisplayTerms.length > 0;
|
|
127
|
-
const explicitlyRequestsAttribute =
|
|
223
|
+
const explicitlyRequestsAttribute = hasEntityAttributeTerm(question);
|
|
128
224
|
const hasRawPinnedEntityLabel = rawPinned.some((candidate) => evidenceCandidateRoles(candidate).includes('entity_label'));
|
|
129
225
|
const pinned = requestedEntityDisplay && hasRawPinnedEntityLabel && !explicitlyRequestsAttribute
|
|
130
|
-
? rawPinned.filter((candidate) =>
|
|
226
|
+
? rawPinned.filter((candidate) => !isEntityAttributeCandidate(candidate))
|
|
131
227
|
: rawPinned;
|
|
132
228
|
const kindCounts = new Map();
|
|
133
229
|
const perKindQualified = canonicalEligible
|
|
@@ -152,7 +248,7 @@ export function buildMeaningEvidencePackage(evidence, maxCandidates = 16, questi
|
|
|
152
248
|
// An account owner/e-mail/sentiment is an attribute, never a substitute
|
|
153
249
|
// for the requested account/customer display entity. Do not let it consume
|
|
154
250
|
// the remaining meaning cards after that display role was successfully pinned.
|
|
155
|
-
return !hasPinnedEntityLabel ||
|
|
251
|
+
return !hasPinnedEntityLabel || !isEntityAttributeCandidate(candidate);
|
|
156
252
|
});
|
|
157
253
|
return [...pinned, ...safeFill]
|
|
158
254
|
.slice(0, limit);
|
|
@@ -208,69 +304,102 @@ function normalizeRef(value) {
|
|
|
208
304
|
return value.trim().toLowerCase().replace(/[`"']/g, "");
|
|
209
305
|
}
|
|
210
306
|
/** Validate that a resolver cannot invent or select ineligible evidence. */
|
|
211
|
-
export function validateMeaningResolution(value, candidates, requestedMeasures = value.queryIntent.measures) {
|
|
212
|
-
|
|
307
|
+
export function validateMeaningResolution(value, candidates, requestedMeasures = value.queryIntent.measures, options = {}) {
|
|
308
|
+
// `id` is the compiler's local canonical identity while `qualifiedId` is
|
|
309
|
+
// the immutable source/semantic handle that the planner and frozen meaning
|
|
310
|
+
// receipt may intentionally preserve. Both are server-issued identities
|
|
311
|
+
// from this bounded snapshot; accepting the qualified handle here does not
|
|
312
|
+
// create a lexical alias surface or permit invented evidence.
|
|
313
|
+
const byId = new Map();
|
|
314
|
+
for (const candidate of candidates) {
|
|
315
|
+
byId.set(candidate.id, candidate);
|
|
316
|
+
if (candidate.qualifiedId)
|
|
317
|
+
byId.set(candidate.qualifiedId, candidate);
|
|
318
|
+
}
|
|
213
319
|
const selectedConceptIds = value.selectedConceptIds.length > 0
|
|
214
320
|
? value.selectedConceptIds
|
|
215
321
|
: value.recommendedExecutionId
|
|
216
322
|
? [value.recommendedExecutionId]
|
|
217
323
|
: [];
|
|
218
|
-
|
|
324
|
+
// Selected/recommended IDs are execution authority and must be present in
|
|
325
|
+
// the bounded package the resolver received. Rejections are explanatory
|
|
326
|
+
// only: hosts may retain a full-snapshot rejection ledger even when its
|
|
327
|
+
// card was correctly pruned from the 16-card meaning package. Do not let a
|
|
328
|
+
// non-authoritative rejected ID invalidate an otherwise valid frozen plan.
|
|
329
|
+
const authoritativeIds = [
|
|
219
330
|
...value.selectedConceptIds,
|
|
220
331
|
...(value.recommendedExecutionId ? [value.recommendedExecutionId] : []),
|
|
221
|
-
...value.rejectedCandidates.map((candidate) => candidate.id),
|
|
222
332
|
];
|
|
223
|
-
const invented =
|
|
333
|
+
const invented = authoritativeIds.find((id) => !byId.has(id));
|
|
224
334
|
if (invented)
|
|
225
335
|
return { ok: false, reason: `The resolver referenced evidence that was not retrieved: ${invented}` };
|
|
226
|
-
|
|
336
|
+
const packageRejectedCandidates = value.rejectedCandidates.filter((candidate) => byId.has(candidate.id));
|
|
337
|
+
const normalizedValue = packageRejectedCandidates.length === value.rejectedCandidates.length
|
|
338
|
+
? value
|
|
339
|
+
: { ...value, rejectedCandidates: packageRejectedCandidates };
|
|
340
|
+
if (normalizedValue.confidence !== "low" && selectedConceptIds.length === 0) {
|
|
227
341
|
return { ok: false, reason: "A medium/high-confidence resolution must select at least one retrieved concept." };
|
|
228
342
|
}
|
|
229
343
|
const selected = selectedConceptIds.map((id) => byId.get(id));
|
|
230
344
|
if (selected.some((candidate) => candidate.eligible === false || candidate.compatibility === "incompatible")) {
|
|
231
345
|
return { ok: false, reason: "The resolver selected ineligible or incompatible evidence." };
|
|
232
346
|
}
|
|
233
|
-
|
|
347
|
+
// The evidence package normally excludes a competing metric before the
|
|
348
|
+
// resolver sees it. Keep the same check at the model-output boundary so an
|
|
349
|
+
// extension, legacy caller, or malformed resolver response cannot turn an
|
|
350
|
+
// explicitly named ranking measure into a correlated alternative.
|
|
351
|
+
const explicitRankingConflict = options.requirements
|
|
352
|
+
? selected.find((candidate) => candidateConflictsWithExplicitRankingMeasure(candidate, options.requirements))
|
|
353
|
+
: undefined;
|
|
354
|
+
if (explicitRankingConflict) {
|
|
355
|
+
return {
|
|
356
|
+
ok: false,
|
|
357
|
+
reason: `The resolver selected a metric that conflicts with the explicit ranking measure: ${explicitRankingConflict.id}`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const rejectedIds = new Set(normalizedValue.rejectedCandidates.map((candidate) => candidate.id));
|
|
234
361
|
if (selectedConceptIds.some((id) => rejectedIds.has(id))) {
|
|
235
362
|
return { ok: false, reason: "The resolver both selected and rejected the same evidence." };
|
|
236
363
|
}
|
|
237
|
-
const executionId =
|
|
364
|
+
const executionId = normalizedValue.recommendedExecutionId ?? normalizedValue.selectedConceptIds[0];
|
|
238
365
|
if (executionId) {
|
|
239
366
|
const execution = byId.get(executionId);
|
|
240
|
-
if (
|
|
241
|
-
return { ok: false, reason: "The recommended execution evidence is ineligible or incompatible." };
|
|
242
|
-
}
|
|
243
|
-
if (value.recommendedRoute === "certified" && execution.kind !== "certified_block") {
|
|
244
|
-
return { ok: false, reason: "A certified route must reference a certified block." };
|
|
245
|
-
}
|
|
246
|
-
if (value.recommendedRoute === "certified" && execution.compatibility !== "compatible") {
|
|
247
|
-
return { ok: false, reason: "A certified route requires a deterministically compatible block fit." };
|
|
248
|
-
}
|
|
249
|
-
if (value.recommendedRoute === 'certified'
|
|
250
|
-
&& !certifiedCandidateExplicitlyCoversMeasures(execution, requestedMeasures)) {
|
|
367
|
+
if (options.requirements && candidateConflictsWithExplicitRankingMeasure(execution, options.requirements)) {
|
|
251
368
|
return {
|
|
252
369
|
ok: false,
|
|
253
|
-
reason:
|
|
370
|
+
reason: `The resolver selected a metric that conflicts with the explicit ranking measure: ${execution.id}`,
|
|
254
371
|
};
|
|
255
372
|
}
|
|
256
|
-
if (
|
|
257
|
-
return { ok: false, reason: "
|
|
258
|
-
}
|
|
259
|
-
if (value.recommendedRoute === "semantic" && execution.compatibility !== "compatible") {
|
|
260
|
-
return { ok: false, reason: "A semantic route requires deterministic measure, grain, and dimension compatibility." };
|
|
373
|
+
if (execution.eligible === false || execution.compatibility === "incompatible") {
|
|
374
|
+
return { ok: false, reason: "The recommended execution evidence is ineligible or incompatible." };
|
|
261
375
|
}
|
|
376
|
+
// `recommendedRoute` is deliberately not validated here. The bounded
|
|
377
|
+
// meaning call can nominate only supplied IDs; the authoritative cascade
|
|
378
|
+
// subsequently evaluates the full host-owned requirement seed against
|
|
379
|
+
// those IDs. Rejecting a partial selected semantic metric at this boundary
|
|
380
|
+
// used the model-adjacent nomination as route authority and stopped the
|
|
381
|
+
// valid same-snapshot metric + grouping extension before the semantic
|
|
382
|
+
// compatibility solver could prove it. The same rule applies to a
|
|
383
|
+
// selected certified block: its output contract is checked by the
|
|
384
|
+
// certified cascade before freeze, where an incomplete block advances to
|
|
385
|
+
// semantic/exploratory rather than becoming a false terminal failure.
|
|
386
|
+
//
|
|
387
|
+
// Package membership, eligibility, incompatible evidence, and explicit
|
|
388
|
+
// ranking conflicts above remain hard boundaries; this only defers tier
|
|
389
|
+
// fitness to the host-owned planner.
|
|
390
|
+
void execution;
|
|
262
391
|
}
|
|
263
|
-
if (
|
|
264
|
-
const invalidFrameReference = firstInvalidAnalyticalFrameReference(
|
|
392
|
+
if (normalizedValue.analyticalFrame) {
|
|
393
|
+
const invalidFrameReference = firstInvalidAnalyticalFrameReference(normalizedValue.analyticalFrame, candidates, executionId);
|
|
265
394
|
if (invalidFrameReference) {
|
|
266
395
|
return { ok: false, reason: `The analytical frame referenced evidence that was not retrieved: ${invalidFrameReference}` };
|
|
267
396
|
}
|
|
268
397
|
}
|
|
269
398
|
return {
|
|
270
399
|
ok: true,
|
|
271
|
-
resolution: selectedConceptIds ===
|
|
272
|
-
?
|
|
273
|
-
: { ...
|
|
400
|
+
resolution: selectedConceptIds === normalizedValue.selectedConceptIds
|
|
401
|
+
? normalizedValue
|
|
402
|
+
: { ...normalizedValue, selectedConceptIds },
|
|
274
403
|
};
|
|
275
404
|
}
|
|
276
405
|
function firstInvalidAnalyticalFrameReference(frame, candidates, executionId) {
|
|
@@ -282,6 +411,13 @@ function firstInvalidAnalyticalFrameReference(frame, candidates, executionId) {
|
|
|
282
411
|
evidenceIds.add(candidate.id);
|
|
283
412
|
if (candidate.qualifiedId)
|
|
284
413
|
evidenceIds.add(candidate.qualifiedId);
|
|
414
|
+
// Semantic adapters can expose a retrieval-stable member identity beside
|
|
415
|
+
// the canonical capability dimension ID. Those exact aliases are authored
|
|
416
|
+
// snapshot identities, not a lexical fallback. Keep them in the frame
|
|
417
|
+
// proof so a server-issued capability choice survives local index
|
|
418
|
+
// normalization without being treated as invented evidence.
|
|
419
|
+
for (const alias of candidate.aliases ?? [])
|
|
420
|
+
evidenceIds.add(alias);
|
|
285
421
|
if (candidate.kind === 'semantic_metric') {
|
|
286
422
|
metricIds.add(candidate.id);
|
|
287
423
|
if (candidate.qualifiedId)
|
|
@@ -303,6 +439,9 @@ function firstInvalidAnalyticalFrameReference(frame, candidates, executionId) {
|
|
|
303
439
|
entityIds.add(capability.primaryEntityId);
|
|
304
440
|
for (const grain of capability.resultGrainIds)
|
|
305
441
|
entityIds.add(grain);
|
|
442
|
+
for (const dimension of capability.dimensions) {
|
|
443
|
+
entityIds.add(dimension.entityId);
|
|
444
|
+
}
|
|
306
445
|
for (const dimension of capability.dimensions)
|
|
307
446
|
dimensionIds.add(dimension.dimensionId);
|
|
308
447
|
for (const dimension of capability.timeDimensions)
|
|
@@ -326,20 +465,38 @@ function firstInvalidAnalyticalFrameReference(frame, candidates, executionId) {
|
|
|
326
465
|
if (!allowed.has(id))
|
|
327
466
|
return `${kind} ${id}`;
|
|
328
467
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
]
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
468
|
+
// A canonical multi-metric request has one recommended execution metric but
|
|
469
|
+
// more than one explicit, retrieved capability. Validate membership against
|
|
470
|
+
// the complete selected metric set here; compatibility/additivity across
|
|
471
|
+
// that set is still proven by the immutable compiler below. Checking only
|
|
472
|
+
// the first metric converted a valid `metric_a + metric_b` identity binding
|
|
473
|
+
// into the false “not retrieved” gap before MetricFlow got a chance to
|
|
474
|
+
// evaluate it.
|
|
475
|
+
const selectedCapabilities = candidates
|
|
476
|
+
.filter((candidate) => {
|
|
477
|
+
const identities = [candidate.id, candidate.qualifiedId, candidate.analyticalCapability?.metricId]
|
|
478
|
+
.filter((id) => Boolean(id));
|
|
479
|
+
return identities.some((id) => frame.metricConceptIds.includes(id))
|
|
480
|
+
|| Boolean(executionId && identities.includes(executionId));
|
|
481
|
+
})
|
|
482
|
+
.flatMap((candidate) => candidate.analyticalCapability ? [candidate.analyticalCapability] : []);
|
|
483
|
+
if (selectedCapabilities.length > 0) {
|
|
484
|
+
const selectedMetricIds = new Set(selectedCapabilities.map((capability) => capability.metricId));
|
|
485
|
+
const selectedDimensionIds = new Set(selectedCapabilities.flatMap((capability) => [
|
|
486
|
+
...capability.dimensions.map((dimension) => dimension.dimensionId),
|
|
487
|
+
...capability.timeDimensions.map((dimension) => dimension.dimensionId),
|
|
488
|
+
]));
|
|
489
|
+
const selectedEntityIds = new Set(selectedCapabilities.flatMap((capability) => [
|
|
490
|
+
capability.primaryEntityId,
|
|
491
|
+
...capability.resultGrainIds,
|
|
492
|
+
// A MetricFlow capability may safely expose a governed product/order
|
|
493
|
+
// grouping dimension whose entity differs from its primary aggregate
|
|
494
|
+
// entity. The frame records that declared entity grain for the output;
|
|
495
|
+
// rejecting it merely because it was not the metric's primary entity
|
|
496
|
+
// turns a complete customer-by-product program into a false "not
|
|
497
|
+
// retrieved" gap before semantic compilation can evaluate it.
|
|
498
|
+
...capability.dimensions.map((dimension) => dimension.entityId),
|
|
499
|
+
]));
|
|
343
500
|
const selectedChecks = [
|
|
344
501
|
['selected capability metric', frame.metricConceptIds, selectedMetricIds],
|
|
345
502
|
['selected capability entity grain', frame.entityGrainIds, selectedEntityIds],
|
|
@@ -362,6 +519,235 @@ function firstInvalidAnalyticalFrameReference(frame, candidates, executionId) {
|
|
|
362
519
|
}
|
|
363
520
|
return undefined;
|
|
364
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* Merge the one model meaning response into the host-owned request seed.
|
|
524
|
+
*
|
|
525
|
+
* Candidate IDs are the only model-controlled execution-adjacent values, and
|
|
526
|
+
* they are still validated against the exact supplied package afterwards. The
|
|
527
|
+
* model's route, SQL-adjacent frame, query intent, and reworded question are
|
|
528
|
+
* presentation suggestions at most; accepting them as authority was how an
|
|
529
|
+
* omitted product category/order ID or an inherited prior filter silently
|
|
530
|
+
* changed the answer tuple after retrieval.
|
|
531
|
+
*/
|
|
532
|
+
export function mergeMeaningResolutionWithRequirementSeed(input) {
|
|
533
|
+
const { seed, resolution, candidates } = input;
|
|
534
|
+
const modelSelectedConceptIds = [...new Set([
|
|
535
|
+
...resolution.selectedConceptIds,
|
|
536
|
+
...(resolution.recommendedExecutionId ? [resolution.recommendedExecutionId] : []),
|
|
537
|
+
])];
|
|
538
|
+
// A model can bind only supplied IDs, but supplied is not synonymous with
|
|
539
|
+
// grounded in the current request. Discard a known semantic metric that
|
|
540
|
+
// would add a qualifier not present in the immutable seed. Keep unknown IDs
|
|
541
|
+
// in the result so `validateMeaningResolution` still rejects inventions at
|
|
542
|
+
// the boundary rather than silently laundering them away.
|
|
543
|
+
const omittedUngroundedMetricIds = modelSelectedConceptIds.filter((id) => {
|
|
544
|
+
const candidate = candidates.find((item) => item.id === id || item.qualifiedId === id);
|
|
545
|
+
return candidate?.kind === 'semantic_metric'
|
|
546
|
+
&& !metricCandidateExactlyMatchesSeed(candidate, seed);
|
|
547
|
+
});
|
|
548
|
+
const selectedConceptIds = modelSelectedConceptIds.filter((id) => !omittedUngroundedMetricIds.includes(id));
|
|
549
|
+
// A minimal candidate-ID-only response is intentionally allowed for the
|
|
550
|
+
// one bounded meaning call. Do not let the order of a role-targeted member
|
|
551
|
+
// extension turn that member into the execution target merely because the
|
|
552
|
+
// model omitted the optional recommendation. A selected metric is the
|
|
553
|
+
// host-safe default; a member only supplies a dimension binding beside it.
|
|
554
|
+
const selectedMetricId = selectedConceptIds.find((id) => {
|
|
555
|
+
const candidate = candidates.find((item) => item.id === id || item.qualifiedId === id);
|
|
556
|
+
return candidate?.kind === 'semantic_metric'
|
|
557
|
+
// A selected metric can supersede a legacy recommendation only when it
|
|
558
|
+
// proves the host-owned metric wording. Token-overlap retrieval is not
|
|
559
|
+
// enough here: otherwise `rollover amount` silently became
|
|
560
|
+
// `rollover_balance_amount` merely because a pooled semantic card had
|
|
561
|
+
// been selected alongside a certified nomination. The cascade can
|
|
562
|
+
// still evaluate a partial candidate later; this guard only prevents a
|
|
563
|
+
// model selection from adding an unspoken metric qualifier.
|
|
564
|
+
&& metricCandidateExactlyMatchesSeed(candidate, seed);
|
|
565
|
+
});
|
|
566
|
+
// A model may nominate a certified block in its legacy recommendation while
|
|
567
|
+
// selecting a supplied semantic metric. The recommendation is not route
|
|
568
|
+
// authority; when one selected metric exists, it is the only safe host
|
|
569
|
+
// execution nomination. This preserves the candidate-ID boundary without
|
|
570
|
+
// letting an unproved block displace the selected complete semantic tuple.
|
|
571
|
+
const recommendedExecutionId = selectedMetricId
|
|
572
|
+
?? resolution.recommendedExecutionId
|
|
573
|
+
?? selectedConceptIds[0];
|
|
574
|
+
const recommendedCandidate = recommendedExecutionId
|
|
575
|
+
? candidates.find((candidate) => candidate.id === recommendedExecutionId || candidate.qualifiedId === recommendedExecutionId)
|
|
576
|
+
: undefined;
|
|
577
|
+
const rejectedCandidates = resolution.rejectedCandidates.filter((candidate) => candidates.some((known) => known.id === candidate.id || known.qualifiedId === candidate.id));
|
|
578
|
+
const queryIntent = bindSelectedMemberValuesToSeed({
|
|
579
|
+
seed,
|
|
580
|
+
selectedConceptIds,
|
|
581
|
+
candidates,
|
|
582
|
+
});
|
|
583
|
+
const receipts = [];
|
|
584
|
+
const recordHostPreserved = (field, differs, reason) => {
|
|
585
|
+
if (differs)
|
|
586
|
+
receipts.push({ version: 1, field, action: 'host_preserved', reason });
|
|
587
|
+
};
|
|
588
|
+
recordHostPreserved('interpreted_question', normalizeMeaningText(resolution.interpretedQuestion) !== normalizeMeaningText(seed.sourceQuestion), 'The source question remains host-owned; model rephrasing cannot add context.');
|
|
589
|
+
recordHostPreserved('question_type', resolution.questionType !== questionTypeFromText(seed.sourceQuestion), 'The host classifies the request mode before meaning resolution.');
|
|
590
|
+
recordHostPreserved('query_intent', !sameQueryIntent(resolution.queryIntent, seed.queryIntent), 'Explicit measures, entity/grain, dimensions, filters, ranking, outputs, and time requirements remain host-owned.');
|
|
591
|
+
recordHostPreserved('recommended_route', resolution.recommendedRoute !== (recommendedCandidate ? cascadeNominationForCandidate(recommendedCandidate) : 'clarify'), 'Tier selection is deterministic cascade authority, not a model recommendation.');
|
|
592
|
+
recordHostPreserved('analytical_frame', resolution.analyticalFrame !== undefined, 'A model frame cannot introduce identifiers, SQL semantics, trust, or a replacement route.');
|
|
593
|
+
if (selectedConceptIds.length > 0) {
|
|
594
|
+
receipts.push({
|
|
595
|
+
version: 1,
|
|
596
|
+
field: 'candidate_selection',
|
|
597
|
+
action: 'selection_accepted',
|
|
598
|
+
reason: 'The model selected supplied candidate IDs; host validation still verifies package membership and compatibility.',
|
|
599
|
+
candidateIds: selectedConceptIds,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
if (omittedUngroundedMetricIds.length > 0) {
|
|
603
|
+
receipts.push({
|
|
604
|
+
version: 1,
|
|
605
|
+
field: 'candidate_selection',
|
|
606
|
+
action: 'host_preserved',
|
|
607
|
+
reason: 'A selected semantic metric would add an unspoken qualifier to the host-owned measure requirement.',
|
|
608
|
+
candidateIds: omittedUngroundedMetricIds,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
if (!sameQueryIntent(queryIntent, seed.queryIntent)) {
|
|
612
|
+
receipts.push({
|
|
613
|
+
version: 1,
|
|
614
|
+
field: 'member_binding',
|
|
615
|
+
action: 'selection_accepted',
|
|
616
|
+
reason: 'A selected, supplied semantic member canonically bound an existing host filter value; no filter field or scope was added.',
|
|
617
|
+
candidateIds: selectedConceptIds,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
interpretedQuestion: seed.sourceQuestion,
|
|
622
|
+
questionType: questionTypeFromText(seed.sourceQuestion),
|
|
623
|
+
selectedConceptIds,
|
|
624
|
+
...(recommendedExecutionId ? { recommendedExecutionId } : {}),
|
|
625
|
+
queryIntent,
|
|
626
|
+
rejectedCandidates,
|
|
627
|
+
confidence: resolution.confidence,
|
|
628
|
+
missingInformation: [...new Set(resolution.missingInformation)],
|
|
629
|
+
recommendedRoute: recommendedCandidate ? cascadeNominationForCandidate(recommendedCandidate) : 'clarify',
|
|
630
|
+
...(resolution.clarifyingQuestion ? { clarifyingQuestion: resolution.clarifyingQuestion } : {}),
|
|
631
|
+
hostRequirementSeed: seed,
|
|
632
|
+
...(receipts.length > 0 ? { overrideReceipts: receipts } : {}),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Keep the host's literal metric requirement authoritative when choosing a
|
|
637
|
+
* primary nominated metric. This intentionally uses exact canonical identity
|
|
638
|
+
* rather than retrieval-style token matching: aliases are normalized into the
|
|
639
|
+
* requirement seed before this boundary, so a legitimate `sales` ->
|
|
640
|
+
* `revenue` binding still matches while a related `rollover balance amount`
|
|
641
|
+
* card cannot add `balance` to an unspoken request for `rollover amount`.
|
|
642
|
+
*/
|
|
643
|
+
function metricCandidateExactlyMatchesSeed(candidate, seed) {
|
|
644
|
+
if (seed.requirements.measures.length === 0)
|
|
645
|
+
return true;
|
|
646
|
+
const identities = [candidate.name, ...(candidate.aliases ?? []), candidate.qualifiedId ?? candidate.id]
|
|
647
|
+
.map((value) => canonicalMetricIdentity(value))
|
|
648
|
+
.filter(Boolean);
|
|
649
|
+
// A multi-metric tuple legitimately selects one candidate per requested
|
|
650
|
+
// measure. Each candidate must prove at least one host metric; requiring a
|
|
651
|
+
// single candidate to prove the entire tuple would drop `beverage revenue`
|
|
652
|
+
// beside `total revenue` before the semantic compiler can combine them.
|
|
653
|
+
return seed.requirements.measures.some((measure) => {
|
|
654
|
+
const requested = canonicalMetricIdentity(measure);
|
|
655
|
+
const genericBareTerm = /^(?:amount|value|count|number|total|rate|percentage|percent)$/.test(requested);
|
|
656
|
+
return identities.some((identity) => identity === requested
|
|
657
|
+
// A governed semantic metric may carry a leading accounting qualifier
|
|
658
|
+
// (`net revenue`) for the reader's base term (`revenue`). Keep that
|
|
659
|
+
// narrow suffix form for the established semantic contract, but never
|
|
660
|
+
// permit an inserted qualifier: `rollover balance amount` must not
|
|
661
|
+
// satisfy the distinct request `rollover amount`.
|
|
662
|
+
|| (!genericBareTerm && identity.endsWith(` ${requested}`)));
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
function canonicalMetricIdentity(value) {
|
|
666
|
+
return value
|
|
667
|
+
.trim()
|
|
668
|
+
.toLowerCase()
|
|
669
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
670
|
+
.replace(/[._:/-]+/g, ' ')
|
|
671
|
+
.replace(/\s+/g, ' ')
|
|
672
|
+
.trim();
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* A selected semantic member can canonically correct an already-host-owned
|
|
676
|
+
* filter value (for example a retrieved alias/typo), but it cannot introduce
|
|
677
|
+
* a new filter field, member, or prior-result scope. Ambiguous member cards
|
|
678
|
+
* deliberately leave the seed value untouched for later clarification.
|
|
679
|
+
*/
|
|
680
|
+
function bindSelectedMemberValuesToSeed(input) {
|
|
681
|
+
const selectedMembers = input.selectedConceptIds.flatMap((id) => {
|
|
682
|
+
const candidate = input.candidates.find((item) => item.id === id || item.qualifiedId === id);
|
|
683
|
+
return candidate?.kind === 'semantic_member' ? [candidate] : [];
|
|
684
|
+
});
|
|
685
|
+
const queryIntent = cloneSeedQueryIntent(input.seed);
|
|
686
|
+
return {
|
|
687
|
+
...queryIntent,
|
|
688
|
+
filters: queryIntent.filters.map((filter) => {
|
|
689
|
+
const matches = selectedMembers.filter((candidate) => [candidate.name, ...(candidate.aliases ?? [])]
|
|
690
|
+
.map(normalizeMemberBindingText)
|
|
691
|
+
.includes(normalizeMemberBindingText(filter.value)));
|
|
692
|
+
return matches.length === 1 ? { ...filter, value: matches[0].name } : filter;
|
|
693
|
+
}),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function normalizeMemberBindingText(value) {
|
|
697
|
+
return value.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* The model's selected candidate is a nomination for the cascade, not the
|
|
701
|
+
* final eligibility verdict. A partial certified card must therefore enter
|
|
702
|
+
* the certified tier so the deterministic output-contract check can continue
|
|
703
|
+
* to semantic/exploratory evidence instead of pretending it is raw SQL.
|
|
704
|
+
*/
|
|
705
|
+
function cascadeNominationForCandidate(candidate) {
|
|
706
|
+
if (candidate.kind === 'certified_block')
|
|
707
|
+
return 'certified';
|
|
708
|
+
if (candidate.kind === 'semantic_metric' || candidate.kind === 'semantic_member')
|
|
709
|
+
return 'semantic';
|
|
710
|
+
return candidate.trustTier === 'governed_sql' ? 'governed_sql' : 'exploratory';
|
|
711
|
+
}
|
|
712
|
+
function cloneSeedQueryIntent(seed) {
|
|
713
|
+
return {
|
|
714
|
+
measures: [...seed.queryIntent.measures],
|
|
715
|
+
dimensions: [...seed.queryIntent.dimensions],
|
|
716
|
+
filters: seed.queryIntent.filters.map((filter) => ({ field: filter.field, value: filter.value })),
|
|
717
|
+
...(seed.queryIntent.timeRange ? { timeRange: seed.queryIntent.timeRange } : {}),
|
|
718
|
+
...(seed.queryIntent.timeGrain ? { timeGrain: seed.queryIntent.timeGrain } : {}),
|
|
719
|
+
...(seed.queryIntent.order ? { order: seed.queryIntent.order } : {}),
|
|
720
|
+
...(seed.queryIntent.limit !== undefined ? { limit: seed.queryIntent.limit } : {}),
|
|
721
|
+
...(seed.queryIntent.fiscalCalendarId ? { fiscalCalendarId: seed.queryIntent.fiscalCalendarId } : {}),
|
|
722
|
+
...(seed.queryIntent.fiscalDateRoleId ? { fiscalDateRoleId: seed.queryIntent.fiscalDateRoleId } : {}),
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function sameQueryIntent(left, right) {
|
|
726
|
+
return JSON.stringify({
|
|
727
|
+
measures: left.measures,
|
|
728
|
+
dimensions: left.dimensions,
|
|
729
|
+
filters: left.filters,
|
|
730
|
+
timeRange: left.timeRange,
|
|
731
|
+
timeGrain: left.timeGrain,
|
|
732
|
+
order: left.order,
|
|
733
|
+
limit: left.limit,
|
|
734
|
+
fiscalCalendarId: left.fiscalCalendarId,
|
|
735
|
+
fiscalDateRoleId: left.fiscalDateRoleId,
|
|
736
|
+
}) === JSON.stringify({
|
|
737
|
+
measures: right.measures,
|
|
738
|
+
dimensions: right.dimensions,
|
|
739
|
+
filters: right.filters,
|
|
740
|
+
timeRange: right.timeRange,
|
|
741
|
+
timeGrain: right.timeGrain,
|
|
742
|
+
order: right.order,
|
|
743
|
+
limit: right.limit,
|
|
744
|
+
fiscalCalendarId: right.fiscalCalendarId,
|
|
745
|
+
fiscalDateRoleId: right.fiscalDateRoleId,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
function normalizeMeaningText(value) {
|
|
749
|
+
return value.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
750
|
+
}
|
|
365
751
|
export function routeForEvidenceCandidate(candidate) {
|
|
366
752
|
if (candidate.kind === "certified_block" && candidate.compatibility === "compatible")
|
|
367
753
|
return "certified";
|