@duckcodeailabs/dql-agent 1.14.1 → 1.14.3-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-run-engine.d.ts +71 -2
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +1048 -31
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +12 -0
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-planner.d.ts +6 -0
- package/dist/agent-run-planner.d.ts.map +1 -1
- package/dist/agent-run-planner.js +2 -0
- package/dist/agent-run-planner.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +48 -3
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +7 -1
- package/dist/agentic/analyst-loop.d.ts.map +1 -1
- package/dist/agentic/analyst-loop.js +211 -28
- package/dist/agentic/analyst-loop.js.map +1 -1
- package/dist/agentic/assumptions.d.ts +1 -1
- package/dist/agentic/assumptions.d.ts.map +1 -1
- package/dist/agentic/assumptions.js.map +1 -1
- package/dist/agentic/research/synthesis.d.ts +4 -0
- package/dist/agentic/research/synthesis.d.ts.map +1 -1
- package/dist/agentic/research/synthesis.js +23 -8
- package/dist/agentic/research/synthesis.js.map +1 -1
- package/dist/agentic/research-agent.d.ts.map +1 -1
- package/dist/agentic/research-agent.js +13 -3
- package/dist/agentic/research-agent.js.map +1 -1
- package/dist/agentic/sql-authorization.d.ts +8 -0
- package/dist/agentic/sql-authorization.d.ts.map +1 -1
- package/dist/agentic/sql-authorization.js +233 -2
- package/dist/agentic/sql-authorization.js.map +1 -1
- package/dist/analytical-compatibility.d.ts.map +1 -1
- package/dist/analytical-compatibility.js +8 -1
- package/dist/analytical-compatibility.js.map +1 -1
- package/dist/analytical-failure-repair.d.ts.map +1 -1
- package/dist/analytical-failure-repair.js +6 -2
- package/dist/analytical-failure-repair.js.map +1 -1
- package/dist/analytical-frame.d.ts +23 -1
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +332 -17
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +613 -0
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +1007 -0
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +110 -6
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +1180 -125
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +32 -1
- package/dist/answer-shape.d.ts.map +1 -1
- package/dist/answer-shape.js +49 -3
- package/dist/answer-shape.js.map +1 -1
- package/dist/ask-observability/index.d.ts +11 -0
- package/dist/ask-observability/index.d.ts.map +1 -0
- package/dist/ask-observability/index.js +7 -0
- package/dist/ask-observability/index.js.map +1 -0
- package/dist/ask-observability/instrumentation.d.ts +35 -0
- package/dist/ask-observability/instrumentation.d.ts.map +1 -0
- package/dist/ask-observability/instrumentation.js +472 -0
- package/dist/ask-observability/instrumentation.js.map +1 -0
- package/dist/ask-observability/observer.d.ts +62 -0
- package/dist/ask-observability/observer.d.ts.map +1 -0
- package/dist/ask-observability/observer.js +537 -0
- package/dist/ask-observability/observer.js.map +1 -0
- package/dist/ask-observability/portable.d.ts +98 -0
- package/dist/ask-observability/portable.d.ts.map +1 -0
- package/dist/ask-observability/portable.js +527 -0
- package/dist/ask-observability/portable.js.map +1 -0
- package/dist/ask-observability/store.d.ts +130 -0
- package/dist/ask-observability/store.d.ts.map +1 -0
- package/dist/ask-observability/store.js +1114 -0
- package/dist/ask-observability/store.js.map +1 -0
- package/dist/ask-observability/types.d.ts +357 -0
- package/dist/ask-observability/types.d.ts.map +1 -0
- package/dist/ask-observability/types.js +2 -0
- package/dist/ask-observability/types.js.map +1 -0
- package/dist/ask-observability/utils.d.ts +21 -0
- package/dist/ask-observability/utils.d.ts.map +1 -0
- package/dist/ask-observability/utils.js +102 -0
- package/dist/ask-observability/utils.js.map +1 -0
- package/dist/conversation/snapshot.d.ts +20 -0
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +30 -1
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/fixtures/ask-ai-office-shaped.d.ts +153 -0
- package/dist/fixtures/ask-ai-office-shaped.d.ts.map +1 -0
- package/dist/fixtures/ask-ai-office-shaped.js +94 -0
- package/dist/fixtures/ask-ai-office-shaped.js.map +1 -0
- package/dist/generated-analytical-proposal.d.ts +37 -1
- package/dist/generated-analytical-proposal.d.ts.map +1 -1
- package/dist/generated-analytical-proposal.js +211 -3
- package/dist/generated-analytical-proposal.js.map +1 -1
- package/dist/governed-relational-compiler.d.ts +15 -0
- package/dist/governed-relational-compiler.d.ts.map +1 -1
- package/dist/governed-relational-compiler.js +99 -1
- package/dist/governed-relational-compiler.js.map +1 -1
- package/dist/index.d.ts +11 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +38 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +31 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/meaning-resolution.d.ts +141 -2
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +444 -30
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +144 -7
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/block-fit.d.ts +18 -0
- package/dist/metadata/block-fit.d.ts.map +1 -1
- package/dist/metadata/block-fit.js +250 -38
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +58 -12
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +19 -3
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +518 -54
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/plan-execution-adapter.d.ts +9 -1
- package/dist/plan-execution-adapter.d.ts.map +1 -1
- package/dist/plan-execution-adapter.js +93 -13
- package/dist/plan-execution-adapter.js.map +1 -1
- package/dist/provider-egress.d.ts +24 -10
- package/dist/provider-egress.d.ts.map +1 -1
- package/dist/provider-egress.js +22 -17
- package/dist/provider-egress.js.map +1 -1
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +10 -9
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/dispatch.d.ts +27 -1
- package/dist/providers/dispatch.d.ts.map +1 -1
- package/dist/providers/dispatch.js +63 -1
- package/dist/providers/dispatch.js.map +1 -1
- package/dist/providers/gemini.js +10 -10
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.js +15 -15
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.js +22 -22
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +52 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/relationship-proof.d.ts +81 -0
- package/dist/relationship-proof.d.ts.map +1 -1
- package/dist/relationship-proof.js +228 -4
- package/dist/relationship-proof.js.map +1 -1
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +23 -1
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +22 -1
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +773 -49
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +14 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +3668 -425
- package/dist/router.js.map +1 -1
- package/dist/semantic-bridge/compose.d.ts.map +1 -1
- package/dist/semantic-bridge/compose.js +23 -4
- package/dist/semantic-bridge/compose.js.map +1 -1
- package/dist/semantic-bridge/member-select.d.ts.map +1 -1
- package/dist/semantic-bridge/member-select.js +39 -21
- package/dist/semantic-bridge/member-select.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { scoreMetadataObjectWithAnalysisPlan, } from "./analysis-planner.js";
|
|
2
2
|
import { normalizeMetricCapabilityContract, } from "@duckcodeailabs/dql-core";
|
|
3
|
+
import { candidateMatchesCategoricalDimensionRequirement, categoricalDimensionTermsMatch, } from '../analytical-orchestration.js';
|
|
4
|
+
import { certifiedCandidateExplicitlyCoversMeasures, } from '../meaning-resolution.js';
|
|
3
5
|
const GENERIC_MEANING_TOKENS = new Set([
|
|
4
6
|
'amount', 'value', 'metric', 'measure', 'total', 'overall', 'monthly', 'daily',
|
|
5
7
|
'weekly', 'quarterly', 'yearly', 'annual', 'count', 'number', 'rate', 'ratio',
|
|
6
8
|
]);
|
|
7
9
|
const MAX_CANDIDATES_PER_CLASS = 4;
|
|
10
|
+
const MAX_QUALIFIED_CANDIDATES = 32;
|
|
8
11
|
const ANALYTICAL_TIME_GRAINS = new Set(['day', 'week', 'month', 'quarter', 'year', 'season', 'period']);
|
|
9
12
|
/**
|
|
10
13
|
* Produce compact meaning-rich cards for an AI resolver. Relevance and trust
|
|
11
14
|
* intentionally remain separate: certification labels an execution lane but
|
|
12
15
|
* never makes an unrelated candidate more relevant to the question.
|
|
13
16
|
*/
|
|
14
|
-
export function buildMeaningEvidencePackage(question, questionPlan, ranked) {
|
|
17
|
+
export function buildMeaningEvidencePackage(question, questionPlan, ranked, retrievalLanes = []) {
|
|
18
|
+
const laneMembership = laneMembershipByObjectKey(retrievalLanes);
|
|
15
19
|
const questionText = normalizeText(question);
|
|
16
20
|
const questionTokens = tokenSet(questionText);
|
|
17
21
|
const eligible = ranked.flatMap((item) => {
|
|
@@ -40,6 +44,15 @@ export function buildMeaningEvidencePackage(question, questionPlan, ranked) {
|
|
|
40
44
|
eligible.sort((left, right) => right.relevanceScore - left.relevanceScore
|
|
41
45
|
|| left.item.rank - right.item.rank
|
|
42
46
|
|| left.item.row.objectKey.localeCompare(right.item.row.objectKey));
|
|
47
|
+
const qualifiedPeerMap = buildAmbiguityPeers(eligible.slice(0, MAX_QUALIFIED_CANDIDATES).map((candidate) => ({
|
|
48
|
+
objectKey: candidate.item.row.objectKey,
|
|
49
|
+
aliases: candidate.aliases,
|
|
50
|
+
})));
|
|
51
|
+
const qualifiedCounts = { certified: 0, semantic: 0, sql: 0 };
|
|
52
|
+
const qualifiedCandidates = eligible.slice(0, MAX_QUALIFIED_CANDIDATES).map((candidate) => {
|
|
53
|
+
qualifiedCounts[candidate.evidenceClass] += 1;
|
|
54
|
+
return candidateCard(candidate.item.row, candidate.evidenceClass, qualifiedCounts[candidate.evidenceClass], candidate.relevanceScore, candidate.aliases, candidate.relevanceReasons, qualifiedPeerMap.get(candidate.item.row.objectKey) ?? [], laneMembership.get(candidate.item.row.objectKey));
|
|
55
|
+
});
|
|
43
56
|
const requiredSemanticMetricKey = questionPlan.requestedShape.measures.length > 0
|
|
44
57
|
? eligible.find((candidate) => candidate.evidenceClass === 'semantic'
|
|
45
58
|
&& candidate.item.row.objectType === 'semantic_metric')?.item.row.objectKey
|
|
@@ -91,11 +104,12 @@ export function buildMeaningEvidencePackage(question, questionPlan, ranked) {
|
|
|
91
104
|
};
|
|
92
105
|
for (const candidate of selected) {
|
|
93
106
|
const lane = byEvidenceClass[candidate.evidenceClass];
|
|
94
|
-
lane.push(candidateCard(candidate.item.row, candidate.evidenceClass, lane.length + 1, candidate.relevanceScore, candidate.aliases, candidate.relevanceReasons, peerMap.get(candidate.item.row.objectKey) ?? []));
|
|
107
|
+
lane.push(candidateCard(candidate.item.row, candidate.evidenceClass, lane.length + 1, candidate.relevanceScore, candidate.aliases, candidate.relevanceReasons, peerMap.get(candidate.item.row.objectKey) ?? [], laneMembership.get(candidate.item.row.objectKey)));
|
|
95
108
|
}
|
|
96
109
|
return {
|
|
97
110
|
candidates: ['certified', 'semantic', 'sql']
|
|
98
111
|
.flatMap((evidenceClass) => byEvidenceClass[evidenceClass]),
|
|
112
|
+
qualifiedCandidates,
|
|
99
113
|
byEvidenceClass,
|
|
100
114
|
ambiguousGroups: ambiguityGroups(peerMap),
|
|
101
115
|
};
|
|
@@ -114,17 +128,141 @@ export function buildMeaningEvidencePackage(question, questionPlan, ranked) {
|
|
|
114
128
|
* gates joins on that list, so every multi-relation governed compile returned
|
|
115
129
|
* `RELATIONSHIP_PROOF_REQUIRED` and the path was unreachable in production.
|
|
116
130
|
*
|
|
117
|
-
* This only *offers*
|
|
118
|
-
* enforces certified, fresh,
|
|
119
|
-
* join
|
|
131
|
+
* This only *offers* identities and compact source facts.
|
|
132
|
+
* `compileGovernedRelationalPlan` still enforces certified, fresh,
|
|
133
|
+
* automatic-join-safe (`REL-002`) before it emits a join. The router also
|
|
134
|
+
* requires the structured safety record for exploratory composition, so a
|
|
135
|
+
* draft relationship with a neutral-looking identity cannot suppress a
|
|
136
|
+
* clarification.
|
|
137
|
+
*/
|
|
138
|
+
function relationshipSafetyFromObject(object) {
|
|
139
|
+
if (object.objectType !== 'relationship')
|
|
140
|
+
return undefined;
|
|
141
|
+
const payload = object.payload ?? {};
|
|
142
|
+
const id = firstString(payload.qualifiedId, payload.id, object.fullName, object.objectKey);
|
|
143
|
+
if (!id)
|
|
144
|
+
return undefined;
|
|
145
|
+
const validationPayload = recordValue(payload.validation);
|
|
146
|
+
const keys = Array.isArray(payload.keys)
|
|
147
|
+
? payload.keys.flatMap((value) => {
|
|
148
|
+
const key = recordValue(value);
|
|
149
|
+
const from = firstString(key?.from);
|
|
150
|
+
const to = firstString(key?.to);
|
|
151
|
+
return from && to ? [{ from, to }] : [];
|
|
152
|
+
})
|
|
153
|
+
: [];
|
|
154
|
+
const validation = validationPayload
|
|
155
|
+
? {
|
|
156
|
+
status: firstString(validationPayload.status),
|
|
157
|
+
checkedAt: firstString(validationPayload.checkedAt, validationPayload.checked_at),
|
|
158
|
+
queryFingerprint: firstString(validationPayload.queryFingerprint, validationPayload.query_fingerprint),
|
|
159
|
+
proofFingerprint: firstString(validationPayload.proofFingerprint, validationPayload.proof_fingerprint),
|
|
160
|
+
}
|
|
161
|
+
: undefined;
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
aliases: uniqueStrings([
|
|
165
|
+
firstString(payload.qualifiedId) ?? '',
|
|
166
|
+
firstString(payload.id) ?? '',
|
|
167
|
+
firstString(payload.localId) ?? '',
|
|
168
|
+
object.objectKey,
|
|
169
|
+
object.fullName ?? '',
|
|
170
|
+
object.name,
|
|
171
|
+
]).filter((identity) => identity !== id),
|
|
172
|
+
from: firstString(payload.from),
|
|
173
|
+
to: firstString(payload.to),
|
|
174
|
+
keys,
|
|
175
|
+
status: firstString(payload.status, object.status),
|
|
176
|
+
cardinality: firstString(payload.cardinality),
|
|
177
|
+
fanout: firstString(payload.fanout),
|
|
178
|
+
...(typeof payload.staleCertification === 'boolean'
|
|
179
|
+
? { staleCertification: payload.staleCertification }
|
|
180
|
+
: {}),
|
|
181
|
+
...(typeof payload.automaticJoinAllowed === 'boolean'
|
|
182
|
+
? { automaticJoinAllowed: payload.automaticJoinAllowed }
|
|
183
|
+
: {}),
|
|
184
|
+
certificationFingerprint: firstString(payload.certificationFingerprint),
|
|
185
|
+
evidenceExpiresAt: firstString(payload.evidenceExpiresAt, payload.evidence_expires_at),
|
|
186
|
+
...(validation ? { validation } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function normalizedRelationshipReference(value) {
|
|
190
|
+
return value.trim().toLowerCase();
|
|
191
|
+
}
|
|
192
|
+
function relationshipReferenceLeaf(value) {
|
|
193
|
+
const normalized = normalizedRelationshipReference(value).replace(/["`\[\]]/g, '');
|
|
194
|
+
return normalized.split(/::|:|\./).filter(Boolean).at(-1) ?? normalized;
|
|
195
|
+
}
|
|
196
|
+
function candidateRelationshipReferences(candidate) {
|
|
197
|
+
return uniqueStrings([
|
|
198
|
+
candidate.qualifiedId ?? '',
|
|
199
|
+
candidate.id,
|
|
200
|
+
candidate.primaryEntity ?? '',
|
|
201
|
+
candidate.analyticalCapability?.primaryEntityId ?? '',
|
|
202
|
+
...(candidate.sourceObjects ?? []),
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
function isRawRelationCandidate(candidate) {
|
|
206
|
+
return candidate.kind === 'dbt_model'
|
|
207
|
+
|| candidate.kind === 'dbt_source'
|
|
208
|
+
|| candidate.kind === 'sql_table';
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Bind relationship proof only through a canonical entity/relation identity.
|
|
212
|
+
* A leaf fallback is deliberately allowed only when both the endpoint and the
|
|
213
|
+
* raw relation leaf occur exactly once in the retrieval snapshot. This keeps
|
|
214
|
+
* two domains with an `orders` model from inheriting each other's proof.
|
|
120
215
|
*/
|
|
121
216
|
function attachRelationshipEvidence(candidates, contextObjects) {
|
|
122
217
|
const relationships = contextObjects.filter((object) => object.objectType === 'relationship');
|
|
123
218
|
if (relationships.length === 0)
|
|
124
219
|
return;
|
|
125
|
-
const
|
|
220
|
+
const endpointIdsByReference = new Map();
|
|
221
|
+
const endpointIdsByLeaf = new Map();
|
|
222
|
+
const identitiesByEndpoint = new Map();
|
|
223
|
+
const safetyByEndpoint = new Map();
|
|
224
|
+
const registerEndpointReference = (endpoint, reference) => {
|
|
225
|
+
if (!reference?.trim())
|
|
226
|
+
return;
|
|
227
|
+
const normalizedReference = normalizedRelationshipReference(reference);
|
|
228
|
+
const existing = endpointIdsByReference.get(normalizedReference) ?? new Set();
|
|
229
|
+
existing.add(endpoint);
|
|
230
|
+
endpointIdsByReference.set(normalizedReference, existing);
|
|
231
|
+
};
|
|
232
|
+
const registerEndpoint = (endpoint) => {
|
|
233
|
+
const normalizedEndpoint = normalizedRelationshipReference(endpoint);
|
|
234
|
+
const existing = endpointIdsByLeaf.get(relationshipReferenceLeaf(endpoint)) ?? new Set();
|
|
235
|
+
existing.add(endpoint);
|
|
236
|
+
endpointIdsByLeaf.set(relationshipReferenceLeaf(endpoint), existing);
|
|
237
|
+
registerEndpointReference(endpoint, endpoint);
|
|
238
|
+
// Keep the canonical normalized key explicit even when endpoint casing
|
|
239
|
+
// differs from the relationship payload or a physical dbt reference.
|
|
240
|
+
registerEndpointReference(endpoint, normalizedEndpoint);
|
|
241
|
+
};
|
|
242
|
+
// DQL entity metadata is the authoritative bridge from a canonical modeling
|
|
243
|
+
// endpoint to its dbt/runtime relation. Preserve that bridge rather than
|
|
244
|
+
// reducing it to a leaf name.
|
|
245
|
+
for (const object of contextObjects.filter((item) => item.objectType === 'dql_entity')) {
|
|
246
|
+
const payload = object.payload ?? {};
|
|
247
|
+
const endpoint = firstString(payload.qualifiedId, payload.id, object.fullName, object.objectKey);
|
|
248
|
+
if (!endpoint)
|
|
249
|
+
continue;
|
|
250
|
+
registerEndpoint(endpoint);
|
|
251
|
+
for (const reference of [
|
|
252
|
+
payload.qualifiedId,
|
|
253
|
+
payload.id,
|
|
254
|
+
payload.localId,
|
|
255
|
+
payload.dbtUniqueId,
|
|
256
|
+
payload.relation,
|
|
257
|
+
payload.table,
|
|
258
|
+
object.objectKey,
|
|
259
|
+
object.fullName,
|
|
260
|
+
])
|
|
261
|
+
registerEndpointReference(endpoint, firstString(reference));
|
|
262
|
+
}
|
|
126
263
|
for (const object of relationships) {
|
|
127
264
|
const payload = (object.payload ?? {});
|
|
265
|
+
const safety = relationshipSafetyFromObject(object);
|
|
128
266
|
const identities = [payload.qualifiedId, payload.id, payload.localId, object.objectKey, object.fullName, object.name]
|
|
129
267
|
.filter((value) => Boolean(value));
|
|
130
268
|
if (identities.length === 0)
|
|
@@ -132,58 +270,114 @@ function attachRelationshipEvidence(candidates, contextObjects) {
|
|
|
132
270
|
for (const endpoint of [payload.from, payload.to]) {
|
|
133
271
|
if (!endpoint)
|
|
134
272
|
continue;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
273
|
+
registerEndpoint(endpoint);
|
|
274
|
+
const normalizedEndpoint = normalizedRelationshipReference(endpoint);
|
|
275
|
+
const endpointIdentities = identitiesByEndpoint.get(normalizedEndpoint) ?? new Set();
|
|
276
|
+
for (const identity of identities)
|
|
277
|
+
endpointIdentities.add(identity);
|
|
278
|
+
identitiesByEndpoint.set(normalizedEndpoint, endpointIdentities);
|
|
279
|
+
if (safety) {
|
|
280
|
+
const safetyForEndpoint = safetyByEndpoint.get(normalizedEndpoint) ?? new Map();
|
|
281
|
+
safetyForEndpoint.set(safety.id, safety);
|
|
282
|
+
safetyByEndpoint.set(normalizedEndpoint, safetyForEndpoint);
|
|
140
283
|
}
|
|
141
284
|
}
|
|
142
285
|
}
|
|
143
|
-
if (
|
|
286
|
+
if (identitiesByEndpoint.size === 0)
|
|
144
287
|
return;
|
|
288
|
+
const rawRelationIdsByLeaf = new Map();
|
|
289
|
+
const registerRawRelation = (relationId, references) => {
|
|
290
|
+
for (const leaf of new Set(references.map(relationshipReferenceLeaf))) {
|
|
291
|
+
const ids = rawRelationIdsByLeaf.get(leaf) ?? new Set();
|
|
292
|
+
ids.add(relationId);
|
|
293
|
+
rawRelationIdsByLeaf.set(leaf, ids);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
for (const candidate of candidates.filter(isRawRelationCandidate)) {
|
|
297
|
+
registerRawRelation(normalizedRelationshipReference(candidate.qualifiedId ?? candidate.id), candidateRelationshipReferences(candidate));
|
|
298
|
+
}
|
|
299
|
+
// Candidate cards are bounded, but the immutable context snapshot can retain
|
|
300
|
+
// another raw relation with the same leaf outside that compact package. Count
|
|
301
|
+
// it too before permitting a leaf fallback.
|
|
302
|
+
for (const object of contextObjects.filter((item) => item.objectType === 'dbt_model'
|
|
303
|
+
|| item.objectType === 'dbt_source'
|
|
304
|
+
|| item.objectType === 'warehouse_table'
|
|
305
|
+
|| item.objectType === 'runtime_table')) {
|
|
306
|
+
const payload = object.payload ?? {};
|
|
307
|
+
const relationId = firstString(payload.qualifiedId, payload.uniqueId, object.fullName, object.objectKey) ?? object.objectKey;
|
|
308
|
+
registerRawRelation(normalizedRelationshipReference(relationId), uniqueStrings([
|
|
309
|
+
relationId,
|
|
310
|
+
firstString(payload.qualifiedId) ?? '',
|
|
311
|
+
firstString(payload.uniqueId) ?? '',
|
|
312
|
+
firstString(payload.relation) ?? '',
|
|
313
|
+
firstString(payload.table) ?? '',
|
|
314
|
+
object.name,
|
|
315
|
+
object.fullName ?? '',
|
|
316
|
+
]));
|
|
317
|
+
}
|
|
318
|
+
const endpointsForReferences = (references) => {
|
|
319
|
+
const direct = new Set();
|
|
320
|
+
for (const reference of references) {
|
|
321
|
+
for (const endpoint of endpointIdsByReference.get(normalizedRelationshipReference(reference)) ?? []) {
|
|
322
|
+
direct.add(endpoint);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (direct.size > 0)
|
|
326
|
+
return direct;
|
|
327
|
+
const fallback = new Set();
|
|
328
|
+
for (const leaf of new Set(references.map(relationshipReferenceLeaf))) {
|
|
329
|
+
const endpoints = endpointIdsByLeaf.get(leaf);
|
|
330
|
+
if (endpoints?.size !== 1 || rawRelationIdsByLeaf.get(leaf)?.size !== 1)
|
|
331
|
+
continue;
|
|
332
|
+
for (const endpoint of endpoints)
|
|
333
|
+
fallback.add(endpoint);
|
|
334
|
+
}
|
|
335
|
+
return fallback;
|
|
336
|
+
};
|
|
145
337
|
for (const candidate of candidates) {
|
|
146
338
|
for (const dimension of candidate.analyticalCapability?.dimensions ?? []) {
|
|
147
339
|
const pathRelationships = new Set(dimension.relationshipPathIds ?? []);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
340
|
+
const endpoints = endpointsForReferences([dimension.entityId, ...(dimension.nativeGroupingPath ?? [])]);
|
|
341
|
+
for (const endpoint of endpoints) {
|
|
342
|
+
for (const identity of identitiesByEndpoint.get(normalizedRelationshipReference(endpoint)) ?? []) {
|
|
343
|
+
pathRelationships.add(identity);
|
|
152
344
|
}
|
|
153
345
|
}
|
|
154
346
|
if (pathRelationships.size > 0)
|
|
155
347
|
dimension.relationshipPathIds = [...pathRelationships].sort();
|
|
156
348
|
}
|
|
349
|
+
const endpoints = endpointsForReferences(candidateRelationshipReferences(candidate));
|
|
157
350
|
const touched = new Set();
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
...capabilityPathEntities,
|
|
166
|
-
...(candidate.sourceObjects ?? []),
|
|
167
|
-
]) {
|
|
168
|
-
if (!reference)
|
|
169
|
-
continue;
|
|
170
|
-
for (const alias of entityAliasesForRelationshipMatch(reference)) {
|
|
171
|
-
for (const identity of identitiesByEntity.get(alias) ?? [])
|
|
172
|
-
touched.add(identity);
|
|
351
|
+
const touchedSafety = new Map((candidate.relationshipSafety ?? []).map((safety) => [safety.id, safety]));
|
|
352
|
+
for (const endpoint of endpoints) {
|
|
353
|
+
const normalizedEndpoint = normalizedRelationshipReference(endpoint);
|
|
354
|
+
for (const identity of identitiesByEndpoint.get(normalizedEndpoint) ?? [])
|
|
355
|
+
touched.add(identity);
|
|
356
|
+
for (const safety of safetyByEndpoint.get(normalizedEndpoint)?.values() ?? []) {
|
|
357
|
+
touchedSafety.set(safety.id, safety);
|
|
173
358
|
}
|
|
174
359
|
}
|
|
360
|
+
if (endpoints.size > 0) {
|
|
361
|
+
candidate.relationshipEndpointIds = [...new Set([
|
|
362
|
+
...(candidate.relationshipEndpointIds ?? []),
|
|
363
|
+
...endpoints,
|
|
364
|
+
])].sort();
|
|
365
|
+
}
|
|
175
366
|
if (touched.size > 0)
|
|
176
367
|
candidate.relationshipEvidence = [...touched].sort();
|
|
368
|
+
if (touchedSafety.size > 0) {
|
|
369
|
+
candidate.relationshipSafety = [...touchedSafety.values()]
|
|
370
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
371
|
+
}
|
|
177
372
|
}
|
|
178
373
|
}
|
|
179
|
-
/** Match `commerce::entity::order`, `order`, and `entity:order` as one entity. */
|
|
180
|
-
function entityAliasesForRelationshipMatch(reference) {
|
|
181
|
-
const normalized = reference.toLowerCase().replace(/^entity:/, '');
|
|
182
|
-
return [...new Set([normalized, normalized.split('::').at(-1) ?? normalized])];
|
|
183
|
-
}
|
|
184
374
|
export function toAgentRetrievalEvidence(evidence, questionPlan, options = {}) {
|
|
185
|
-
const
|
|
186
|
-
|
|
375
|
+
const sourceCandidates = evidence.qualifiedCandidates?.length
|
|
376
|
+
? evidence.qualifiedCandidates
|
|
377
|
+
: evidence.candidates;
|
|
378
|
+
const maxRelevance = Math.max(1, ...sourceCandidates.map((candidate) => candidate.relevanceScore));
|
|
379
|
+
const optionLaneMembership = laneMembershipByObjectKey(options.retrievalLanes ?? []);
|
|
380
|
+
const candidates = sourceCandidates.map((candidate) => agentCandidateFromMeaning(candidate, maxRelevance, candidate.retrievalLanes ?? optionLaneMembership.get(candidate.objectKey)));
|
|
187
381
|
const trustedBindings = trustedMemberBindingEvidence(questionPlan, candidates, options.contextObjects ?? []);
|
|
188
382
|
for (const member of trustedBindings.candidates) {
|
|
189
383
|
if (!candidates.some((candidate) => candidate.id === member.id))
|
|
@@ -243,7 +437,7 @@ export function toAgentRetrievalEvidence(evidence, questionPlan, options = {}) {
|
|
|
243
437
|
},
|
|
244
438
|
};
|
|
245
439
|
}
|
|
246
|
-
function agentCandidateFromMeaning(candidate, maxRelevance = 1) {
|
|
440
|
+
function agentCandidateFromMeaning(candidate, maxRelevance = 1, retrievalLanes) {
|
|
247
441
|
return {
|
|
248
442
|
id: candidate.objectKey,
|
|
249
443
|
qualifiedId: candidate.qualifiedId,
|
|
@@ -265,6 +459,7 @@ function agentCandidateFromMeaning(candidate, maxRelevance = 1) {
|
|
|
265
459
|
timeGrains: candidate.businessShape.timeGrains,
|
|
266
460
|
requiredParameters: candidate.businessShape.parameters,
|
|
267
461
|
sourceObjects: candidate.businessShape.sourceRelations,
|
|
462
|
+
relationshipSafety: candidate.relationshipSafety,
|
|
268
463
|
relevanceScore: Number((candidate.relevanceScore / maxRelevance).toFixed(6)),
|
|
269
464
|
matchReasons: candidate.relevanceReasons,
|
|
270
465
|
// Fit validation remains host-owned. Metadata only reports facts here and
|
|
@@ -274,6 +469,7 @@ function agentCandidateFromMeaning(candidate, maxRelevance = 1) {
|
|
|
274
469
|
analyticalCapability: candidate.analyticalCapability,
|
|
275
470
|
eligible: true,
|
|
276
471
|
exactMatch: candidate.relevanceReasons.includes("exact name or alias"),
|
|
472
|
+
...(retrievalLanes?.length ? { retrievalLanes } : {}),
|
|
277
473
|
};
|
|
278
474
|
}
|
|
279
475
|
function trustedMemberBindingEvidence(plan, candidates, objects) {
|
|
@@ -353,21 +549,21 @@ function supplementalClarificationEvidence(plan, objects, existing) {
|
|
|
353
549
|
if (!['semantic_metric', 'semantic_measure', 'semantic_dimension'].includes(object.objectType))
|
|
354
550
|
return [];
|
|
355
551
|
const names = aliasesFor(object).map(normalizeText);
|
|
356
|
-
const
|
|
552
|
+
const candidate = agentCandidateFromMeaning(candidateCard(object, 'semantic', 1, 1, aliasesFor(object), ['requested clarification capability'], []));
|
|
357
553
|
const relevant = object.objectType === 'semantic_metric' || object.objectType === 'semantic_measure'
|
|
358
554
|
? requestedMeasures.some((term) => names.some((name) => phraseTermsMatch(term, name)))
|
|
359
555
|
: requestedDimensions.some((term) => names.some((name) => phraseTermsMatch(term, name))
|
|
360
|
-
|| (
|
|
556
|
+
|| candidateMatchesCategoricalDimensionRequirement(candidate, [term]));
|
|
361
557
|
if (!relevant)
|
|
362
558
|
return [];
|
|
363
|
-
|
|
364
|
-
return [agentCandidateFromMeaning(card)];
|
|
559
|
+
return [candidate];
|
|
365
560
|
});
|
|
366
561
|
const declaredDimensionCards = existing.flatMap((candidate) => (candidate.dimensions ?? []).flatMap((dimensionId) => {
|
|
367
562
|
if (!/[:./]/.test(dimensionId) || existingIds.has(dimensionId))
|
|
368
563
|
return [];
|
|
369
564
|
const role = normalizeDimensionRole(dimensionId);
|
|
370
|
-
const relevant = requestedDimensions.some((term) => phraseTermsMatch(term, role)
|
|
565
|
+
const relevant = requestedDimensions.some((term) => phraseTermsMatch(term, role)
|
|
566
|
+
|| candidateMatchesCategoricalDimensionRequirement(candidate, [term]));
|
|
371
567
|
if (!relevant)
|
|
372
568
|
return [];
|
|
373
569
|
return [{
|
|
@@ -380,6 +576,7 @@ function supplementalClarificationEvidence(plan, objects, existing) {
|
|
|
380
576
|
definition: `Modeled ${role} dimension available through ${candidate.name}.`,
|
|
381
577
|
primaryEntity: candidate.primaryEntity,
|
|
382
578
|
dimensions: [dimensionId],
|
|
579
|
+
compatibilityFacts: candidate.compatibilityFacts,
|
|
383
580
|
relevanceScore: candidate.relevanceScore,
|
|
384
581
|
matchReasons: ['requested clarification capability role'],
|
|
385
582
|
compatibility: 'partial',
|
|
@@ -387,6 +584,87 @@ function supplementalClarificationEvidence(plan, objects, existing) {
|
|
|
387
584
|
exactMatch: false,
|
|
388
585
|
}];
|
|
389
586
|
}));
|
|
587
|
+
// A compact retrieval package can contain the selected semantic metric but
|
|
588
|
+
// not a separately indexed card for one of that metric's declared grouping
|
|
589
|
+
// dimensions. Retain one exact, same-snapshot extension for that field when
|
|
590
|
+
// the request names its categorical role. This does not infer a join or a
|
|
591
|
+
// business alias: the metric capability owns both the qualified dimension
|
|
592
|
+
// id and the MetricFlow-native grouping path, and the router later admits it
|
|
593
|
+
// only beside this exact metric. It is intentionally separate from the
|
|
594
|
+
// geography-only recovery below, which may bind a location-like field for
|
|
595
|
+
// the distinct business term `region` under a stricter sole-field rule.
|
|
596
|
+
const exactCapabilityDimensionCards = existing.flatMap((metric) => {
|
|
597
|
+
if (metric.kind !== 'semantic_metric')
|
|
598
|
+
return [];
|
|
599
|
+
const capability = normalizeMetricCapabilityContract(metric.analyticalCapability);
|
|
600
|
+
if (!capability || !capability.executionCapabilities.some((execution) => execution.route === 'semantic'))
|
|
601
|
+
return [];
|
|
602
|
+
const metricId = metric.qualifiedId ?? metric.id;
|
|
603
|
+
return requestedDimensions.flatMap((requestedTerm) => capability.dimensions.flatMap((dimension) => {
|
|
604
|
+
if (!dimension.supportedRoles.includes('group_by'))
|
|
605
|
+
return [];
|
|
606
|
+
// `existing` is the unbounded retrieval snapshot, not the compact
|
|
607
|
+
// meaning package. A separately indexed semantic dimension can be
|
|
608
|
+
// present here yet later lose its role-balanced admission to a raw/dbt
|
|
609
|
+
// column with the same business words. Preserve the capability-backed
|
|
610
|
+
// extension in that case so the router can admit it beside its exact
|
|
611
|
+
// metric. The final de-duplication deliberately retains this extension
|
|
612
|
+
// (with its metric/role proof) over a plain dimension card.
|
|
613
|
+
const identities = [
|
|
614
|
+
dimension.dimensionId,
|
|
615
|
+
dimension.label ?? '',
|
|
616
|
+
...(dimension.aliases ?? []),
|
|
617
|
+
];
|
|
618
|
+
if (!identities.some((identity) => categoricalDimensionTermsMatch(requestedTerm, identity)))
|
|
619
|
+
return [];
|
|
620
|
+
return [{
|
|
621
|
+
id: dimension.dimensionId,
|
|
622
|
+
qualifiedId: dimension.dimensionId,
|
|
623
|
+
kind: 'semantic_member',
|
|
624
|
+
semanticObjectType: 'dimension',
|
|
625
|
+
trustTier: 'semantic',
|
|
626
|
+
name: dimension.label ?? dimension.dimensionId.split(/[.:/]/).at(-1) ?? dimension.dimensionId,
|
|
627
|
+
aliases: uniqueStrings([
|
|
628
|
+
dimension.label ?? '',
|
|
629
|
+
...(dimension.aliases ?? []),
|
|
630
|
+
requestedTerm,
|
|
631
|
+
]),
|
|
632
|
+
definition: `Same-snapshot MetricFlow grouping field for ${requestedTerm}.`,
|
|
633
|
+
primaryEntity: dimension.entityId,
|
|
634
|
+
dimensions: [dimension.dimensionId],
|
|
635
|
+
sourceObjects: metric.sourceObjects,
|
|
636
|
+
relationshipEvidence: metric.relationshipEvidence,
|
|
637
|
+
relationshipSafety: metric.relationshipSafety,
|
|
638
|
+
relevanceScore: metric.relevanceScore,
|
|
639
|
+
matchReasons: ['same snapshot exact MetricFlow grouping extension'],
|
|
640
|
+
compatibility: 'compatible',
|
|
641
|
+
eligible: true,
|
|
642
|
+
exactMatch: false,
|
|
643
|
+
sameSnapshotRoleExtension: {
|
|
644
|
+
version: 1,
|
|
645
|
+
role: 'categorical_dimension',
|
|
646
|
+
requestedTerm,
|
|
647
|
+
metricId,
|
|
648
|
+
dimensionId: dimension.dimensionId,
|
|
649
|
+
basis: 'exact_metricflow_grouping_dimension',
|
|
650
|
+
},
|
|
651
|
+
}];
|
|
652
|
+
}));
|
|
653
|
+
});
|
|
654
|
+
// A location-like semantic dimension is not generally a synonym for
|
|
655
|
+
// "region". There is one deliberately narrow exception: when every
|
|
656
|
+
// current-turn requested metric proves the same one MetricFlow grouping
|
|
657
|
+
// dimension in this immutable snapshot. Surface that card as a typed,
|
|
658
|
+
// host-authored extension so the bounded meaning call can bind it; the
|
|
659
|
+
// selected metric capability still verifies the native grouping path and
|
|
660
|
+
// remains the only execution authority. This recovers the real Jaffle
|
|
661
|
+
// `revenue by ... region` path without guessing a raw join or widening
|
|
662
|
+
// lexical geography matching.
|
|
663
|
+
const sameSnapshotRoleExtensions = sameSnapshotGeographyRoleExtensions({
|
|
664
|
+
requestedMeasures,
|
|
665
|
+
requestedDimensions,
|
|
666
|
+
existing,
|
|
667
|
+
});
|
|
390
668
|
// A bare ranking ("who are the top customers") names an ENTITY and no
|
|
391
669
|
// measure, so every lane above — each keyed on a REQUESTED measure or
|
|
392
670
|
// dimension term — returns nothing, and the clarification has no choices to
|
|
@@ -430,8 +708,32 @@ function supplementalClarificationEvidence(plan, objects, existing) {
|
|
|
430
708
|
return [card];
|
|
431
709
|
}).sort((left, right) => right.relevanceScore - left.relevanceScore || left.id.localeCompare(right.id));
|
|
432
710
|
})();
|
|
433
|
-
|
|
434
|
-
|
|
711
|
+
// The index may yield a generic semantic-dimension card and then this
|
|
712
|
+
// function may enrich that exact qualified ID with a metric-declared,
|
|
713
|
+
// same-snapshot role proof. Retain the enriched card: it is not a second
|
|
714
|
+
// candidate or a guessed relationship, but the only metadata authority that
|
|
715
|
+
// says this field is queryable with the selected MetricFlow metric for the
|
|
716
|
+
// current requested role. First-seen de-duplication discarded that proof in
|
|
717
|
+
// real local indexes and manufactured a false coverage gap for
|
|
718
|
+
// `product category` / `product_type`.
|
|
719
|
+
const uniqueByQualifiedId = new Map();
|
|
720
|
+
for (const candidate of [
|
|
721
|
+
...objectCards,
|
|
722
|
+
...exactCapabilityDimensionCards,
|
|
723
|
+
...declaredDimensionCards,
|
|
724
|
+
...sameSnapshotRoleExtensions,
|
|
725
|
+
...rankingMeasureCards,
|
|
726
|
+
]) {
|
|
727
|
+
const key = candidate.qualifiedId ?? candidate.id;
|
|
728
|
+
const existingCandidate = uniqueByQualifiedId.get(key);
|
|
729
|
+
if (!existingCandidate
|
|
730
|
+
|| (!existingCandidate.sameSnapshotRoleExtension && candidate.sameSnapshotRoleExtension)) {
|
|
731
|
+
// Map replacement preserves stable first-seen ordering while retaining
|
|
732
|
+
// the stricter extension provenance for this one qualified object.
|
|
733
|
+
uniqueByQualifiedId.set(key, candidate);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const unique = [...uniqueByQualifiedId.values()];
|
|
435
737
|
const roleLanes = [
|
|
436
738
|
...requestedDimensions.map((term) => ({ kind: 'dimension', term })),
|
|
437
739
|
...requestedMeasures.map((term) => ({ kind: 'metric', term })),
|
|
@@ -454,7 +756,7 @@ function supplementalClarificationEvidence(plan, objects, existing) {
|
|
|
454
756
|
if (candidate.kind !== 'semantic_member')
|
|
455
757
|
return false;
|
|
456
758
|
return candidateTerms.some((term) => phraseTermsMatch(lane.term, term))
|
|
457
|
-
|| (
|
|
759
|
+
|| candidateMatchesCategoricalDimensionRequirement(candidate, [lane.term]);
|
|
458
760
|
}).sort((left, right) => right.relevanceScore - left.relevanceScore || left.id.localeCompare(right.id));
|
|
459
761
|
for (const candidate of matches.slice(0, MAX_PER_REQUESTED_ROLE)) {
|
|
460
762
|
if (!selected.some((item) => (item.qualifiedId ?? item.id) === (candidate.qualifiedId ?? candidate.id))) {
|
|
@@ -464,6 +766,95 @@ function supplementalClarificationEvidence(plan, objects, existing) {
|
|
|
464
766
|
}
|
|
465
767
|
return selected.slice(0, MAX_SUPPLEMENTAL_CANDIDATES);
|
|
466
768
|
}
|
|
769
|
+
function sameSnapshotGeographyRoleExtensions(input) {
|
|
770
|
+
const geographyTerms = [...new Set(input.requestedDimensions
|
|
771
|
+
.map(normalizeText)
|
|
772
|
+
.filter((term) => term === 'region' || term === 'geography' || term === 'geographic'))];
|
|
773
|
+
if (geographyTerms.length === 0 || input.requestedMeasures.length === 0)
|
|
774
|
+
return [];
|
|
775
|
+
const existingGeography = input.existing.some((candidate) => geographyTerms.some((term) => candidateMatchesCategoricalDimensionRequirement(candidate, [term])));
|
|
776
|
+
if (existingGeography)
|
|
777
|
+
return [];
|
|
778
|
+
const canonicalMetricTerm = (value) => {
|
|
779
|
+
const normalized = normalizeText(value);
|
|
780
|
+
return normalized === 'sale' || normalized === 'sales' ? 'revenue' : normalized;
|
|
781
|
+
};
|
|
782
|
+
const metricMatches = (candidate, requested) => {
|
|
783
|
+
if (candidate.kind !== 'semantic_metric' || !candidate.analyticalCapability)
|
|
784
|
+
return false;
|
|
785
|
+
const expected = canonicalMetricTerm(requested);
|
|
786
|
+
return [candidate.name, ...(candidate.aliases ?? []), candidate.analyticalCapability.metricId]
|
|
787
|
+
.map(canonicalMetricTerm)
|
|
788
|
+
.some((identity) => phraseTermsMatch(expected, identity));
|
|
789
|
+
};
|
|
790
|
+
const geographicDimension = (dimension) => {
|
|
791
|
+
const identity = [dimension.dimensionId, dimension.entityId, dimension.label ?? '', ...(dimension.aliases ?? [])]
|
|
792
|
+
.map(normalizeText)
|
|
793
|
+
.join(' ');
|
|
794
|
+
return /\b(?:region|geograph(?:y|ic)|location|country|state|province|city|territory)\b/.test(identity);
|
|
795
|
+
};
|
|
796
|
+
const candidatesForMeasure = (requested) => input.existing.flatMap((candidate) => {
|
|
797
|
+
if (!metricMatches(candidate, requested))
|
|
798
|
+
return [];
|
|
799
|
+
const capability = candidate.analyticalCapability;
|
|
800
|
+
if (!capability.executionCapabilities.some((execution) => execution.route === 'semantic'))
|
|
801
|
+
return [];
|
|
802
|
+
return capability.dimensions
|
|
803
|
+
.filter((dimension) => dimension.supportedRoles.includes('group_by') && geographicDimension(dimension))
|
|
804
|
+
.map((dimension) => ({ candidate, dimension }));
|
|
805
|
+
});
|
|
806
|
+
const matchesByMeasure = input.requestedMeasures.map((requested) => ({
|
|
807
|
+
requested,
|
|
808
|
+
matches: candidatesForMeasure(requested),
|
|
809
|
+
}));
|
|
810
|
+
if (matchesByMeasure.some(({ matches }) => matches.length === 0))
|
|
811
|
+
return [];
|
|
812
|
+
const sharedDimensionIds = matchesByMeasure
|
|
813
|
+
.map(({ matches }) => new Set(matches.map(({ dimension }) => dimension.dimensionId)))
|
|
814
|
+
.reduce((shared, ids) => shared === undefined
|
|
815
|
+
? ids
|
|
816
|
+
: new Set([...shared].filter((id) => ids.has(id))), undefined);
|
|
817
|
+
if (!sharedDimensionIds || sharedDimensionIds.size !== 1)
|
|
818
|
+
return [];
|
|
819
|
+
const dimensionId = [...sharedDimensionIds][0];
|
|
820
|
+
const witness = matchesByMeasure[0].matches.find((match) => match.dimension.dimensionId === dimensionId);
|
|
821
|
+
if (!witness)
|
|
822
|
+
return [];
|
|
823
|
+
const { candidate: metricCandidate, dimension } = witness;
|
|
824
|
+
const requestedTerm = geographyTerms[0];
|
|
825
|
+
return [{
|
|
826
|
+
id: dimensionId,
|
|
827
|
+
qualifiedId: dimensionId,
|
|
828
|
+
kind: 'semantic_member',
|
|
829
|
+
semanticObjectType: 'dimension',
|
|
830
|
+
trustTier: 'semantic',
|
|
831
|
+
name: dimension.label ?? dimension.dimensionId.split(/[.:/]/).at(-1) ?? dimension.dimensionId,
|
|
832
|
+
aliases: uniqueStrings([
|
|
833
|
+
dimension.label ?? '',
|
|
834
|
+
...(dimension.aliases ?? []),
|
|
835
|
+
requestedTerm,
|
|
836
|
+
]),
|
|
837
|
+
definition: `Same-snapshot MetricFlow grouping field for ${requestedTerm}.`,
|
|
838
|
+
primaryEntity: dimension.entityId,
|
|
839
|
+
dimensions: [dimensionId],
|
|
840
|
+
sourceObjects: metricCandidate.sourceObjects,
|
|
841
|
+
relationshipEvidence: metricCandidate.relationshipEvidence,
|
|
842
|
+
relationshipSafety: metricCandidate.relationshipSafety,
|
|
843
|
+
relevanceScore: metricCandidate.relevanceScore,
|
|
844
|
+
matchReasons: ['same snapshot role-targeted MetricFlow grouping extension'],
|
|
845
|
+
compatibility: 'compatible',
|
|
846
|
+
eligible: true,
|
|
847
|
+
exactMatch: false,
|
|
848
|
+
sameSnapshotRoleExtension: {
|
|
849
|
+
version: 1,
|
|
850
|
+
role: 'categorical_dimension',
|
|
851
|
+
requestedTerm,
|
|
852
|
+
metricId: metricCandidate.qualifiedId ?? metricCandidate.id,
|
|
853
|
+
dimensionId,
|
|
854
|
+
basis: 'sole_metricflow_grouping_dimension',
|
|
855
|
+
},
|
|
856
|
+
}];
|
|
857
|
+
}
|
|
467
858
|
function expandedRequestedTerms(terms, objects) {
|
|
468
859
|
const vocabulary = new Map();
|
|
469
860
|
for (const object of objects) {
|
|
@@ -526,9 +917,6 @@ function dimensionLookupAliases(dimensions) {
|
|
|
526
917
|
return role && role !== leaf ? [dimension, role] : [dimension];
|
|
527
918
|
}));
|
|
528
919
|
}
|
|
529
|
-
function isGeographicRole(value) {
|
|
530
|
-
return /^(?:region|location|geography|territory|market|site|store)$/.test(normalizeDimensionRole(value));
|
|
531
|
-
}
|
|
532
920
|
/**
|
|
533
921
|
* Bind current-turn values only when an authorized runtime-value observation
|
|
534
922
|
* and a semantic dimension's exact physical table/column identity agree. Text
|
|
@@ -640,11 +1028,46 @@ export function applyContextPackCompatibility(evidence, pack, selectedEvidenceId
|
|
|
640
1028
|
// `monthly_revenue` artifact lose to a semantic metric for the ordinary
|
|
641
1029
|
// paraphrase "What is monthly revenue?". Do not promote lexical
|
|
642
1030
|
// relevance alone; require the catalog's executable fit verdict.
|
|
643
|
-
const
|
|
1031
|
+
const requestedMeasures = pack.questionPlan.requestedShape.measures;
|
|
1032
|
+
// `declaredOutputs` is preferable because it remains independently
|
|
1033
|
+
// visible in the block source. Some older-but-valid certified blocks
|
|
1034
|
+
// only expose their projection through the catalog's immutable SQL
|
|
1035
|
+
// contract, however. Preserve that proof *only* when the reader typed
|
|
1036
|
+
// the exact block title and the same snapshot's catalog has already
|
|
1037
|
+
// selected this precise block as a high-confidence certified answer.
|
|
1038
|
+
// A fuzzy alias, paraphrase, or ordinary semantic question cannot use
|
|
1039
|
+
// this bridge, so non-exact role and output constraints remain intact.
|
|
1040
|
+
const exactCertifiedTitle = typeof pack.questionPlan.question === 'string'
|
|
1041
|
+
&& normalizeText(pack.questionPlan.question) === normalizeText(candidate.name)
|
|
1042
|
+
&& pack.routeDecision.route === 'certified'
|
|
1043
|
+
&& pack.routeDecision.exactObjectKey === candidate.id
|
|
1044
|
+
&& fit?.action === 'certified_answer'
|
|
644
1045
|
&& (fit.fit.kind === 'exact' || fit.fit.kind === 'trim_safe')
|
|
645
1046
|
&& fit.fit.confidence === 'high';
|
|
1047
|
+
const compatibilityFacts = exactCertifiedTitle
|
|
1048
|
+
? uniqueStrings([
|
|
1049
|
+
...(candidate.compatibilityFacts ?? []),
|
|
1050
|
+
...requestedMeasures.map((measure) => `catalog-proven-output: ${measure}`),
|
|
1051
|
+
// The catalog fit also proves this block's declared result
|
|
1052
|
+
// grain. Preserve it as an output only on the exact-title
|
|
1053
|
+
// bridge, so the immutable plan can bind `customer` to its
|
|
1054
|
+
// actual `customer_name` result without making a fuzzy block
|
|
1055
|
+
// title a display-key authority.
|
|
1056
|
+
...(candidate.compatibilityFacts ?? []).flatMap((fact) => /^grain:\s*(.+)$/i.exec(fact)?.[1]
|
|
1057
|
+
?.split(',')
|
|
1058
|
+
.map((value) => value.trim())
|
|
1059
|
+
.filter(Boolean)
|
|
1060
|
+
.map((value) => `catalog-proven-output: ${value}`) ?? []),
|
|
1061
|
+
])
|
|
1062
|
+
: candidate.compatibilityFacts;
|
|
1063
|
+
const blockDeclaresRequestedMeasures = certifiedCandidateExplicitlyCoversMeasures({ ...candidate, compatibilityFacts }, requestedMeasures);
|
|
1064
|
+
const completeCertifiedFit = fit?.action === 'certified_answer'
|
|
1065
|
+
&& (fit.fit.kind === 'exact' || fit.fit.kind === 'trim_safe')
|
|
1066
|
+
&& fit.fit.confidence === 'high'
|
|
1067
|
+
&& blockDeclaresRequestedMeasures;
|
|
646
1068
|
return {
|
|
647
1069
|
...candidate,
|
|
1070
|
+
...(compatibilityFacts ? { compatibilityFacts } : {}),
|
|
648
1071
|
// Lexical equality to a block name/alias is useful retrieval signal,
|
|
649
1072
|
// but it is not exact analytical authority. The catalog's complete
|
|
650
1073
|
// certified fit is the one host-owned exception and is still checked
|
|
@@ -657,7 +1080,7 @@ export function applyContextPackCompatibility(evidence, pack, selectedEvidenceId
|
|
|
657
1080
|
// broader question. Exclude it before both meaning resolution and
|
|
658
1081
|
// identifier-bound clarification; later fit rejection is too late.
|
|
659
1082
|
eligible: unrequestedStaticScope ? false : candidate.eligible,
|
|
660
|
-
compatibility:
|
|
1083
|
+
compatibility: completeCertifiedFit
|
|
661
1084
|
? 'compatible'
|
|
662
1085
|
: fit?.action === 'rejected_for_fit'
|
|
663
1086
|
? 'incompatible'
|
|
@@ -721,17 +1144,38 @@ function trustTierFor(object, evidenceClass) {
|
|
|
721
1144
|
return 'governed_sql';
|
|
722
1145
|
return 'exploratory';
|
|
723
1146
|
}
|
|
724
|
-
function candidateCard(object, evidenceClass, classRank, relevanceScore, aliases, relevanceReasons, ambiguityPeerIds) {
|
|
1147
|
+
function candidateCard(object, evidenceClass, classRank, relevanceScore, aliases, relevanceReasons, ambiguityPeerIds, retrievalLanes) {
|
|
725
1148
|
const payload = object.payload ?? {};
|
|
726
1149
|
const analyticalCapability = normalizeMetricCapabilityContract(payload.analyticalCapability);
|
|
727
|
-
const
|
|
1150
|
+
const relationshipSafety = relationshipSafetyFromObject(object);
|
|
1151
|
+
// A dbt/runtime column inherits its parent node's `uniqueId`. That value
|
|
1152
|
+
// is useful lineage metadata, but it is not a column identity: using it as
|
|
1153
|
+
// the candidate's qualified ID collapsed every selected column on a model
|
|
1154
|
+
// into the model itself. The physical cascade then appeared safe but could
|
|
1155
|
+
// not freeze an exact output contract. Keep the parent relation in
|
|
1156
|
+
// `sourceRelations`; give a column its source-qualified field identity.
|
|
1157
|
+
const columnObject = object.objectType.endsWith('_column');
|
|
1158
|
+
const qualifiedId = firstString(payload.qualifiedId, ...(columnObject ? [object.fullName] : []), payload.uniqueId, object.fullName, object.objectKey) ?? object.objectKey;
|
|
728
1159
|
const parameters = arrayNames(payload.parameters ?? payload.parameterPolicy);
|
|
729
1160
|
const outputs = uniqueStrings([
|
|
730
1161
|
...stringArray(payload.declaredOutputs),
|
|
731
1162
|
...arrayNames(payload.outputContract),
|
|
732
1163
|
...arrayNames(payload.outputs),
|
|
733
1164
|
]);
|
|
1165
|
+
const declaredSemanticGeography = [
|
|
1166
|
+
...stringArray(payload.semanticRoles),
|
|
1167
|
+
...stringArray(payload.semantic_roles),
|
|
1168
|
+
firstString(payload.semanticRole, payload.semantic_role) ?? '',
|
|
1169
|
+
].some((role) => {
|
|
1170
|
+
const normalized = normalizeText(role);
|
|
1171
|
+
return normalized === 'geography' || normalized === 'geographic';
|
|
1172
|
+
});
|
|
734
1173
|
const compatibilityFacts = uniqueStrings([
|
|
1174
|
+
// Carry only authored, snapshot-local compatibility facts. Consumers use
|
|
1175
|
+
// these as typed declarations, never as fuzzy lexical aliases.
|
|
1176
|
+
...stringArray(payload.compatibilityFacts),
|
|
1177
|
+
...stringArray(payload.compatibility_facts),
|
|
1178
|
+
...(declaredSemanticGeography ? ['semantic-role: geography'] : []),
|
|
735
1179
|
firstString(payload.grain) ? `grain: ${firstString(payload.grain)}` : '',
|
|
736
1180
|
...stringArray(payload.dimensions).slice(0, 8).map((value) => `dimension: ${value}`),
|
|
737
1181
|
...parameters.slice(0, 8).map((value) => `parameter: ${value}`),
|
|
@@ -776,10 +1220,25 @@ function candidateCard(object, evidenceClass, classRank, relevanceScore, aliases
|
|
|
776
1220
|
]).slice(0, 12),
|
|
777
1221
|
},
|
|
778
1222
|
...(analyticalCapability ? { analyticalCapability } : {}),
|
|
1223
|
+
...(relationshipSafety ? { relationshipSafety: [relationshipSafety] } : {}),
|
|
779
1224
|
provenance: firstString(payload.provenance, object.sourceSystem),
|
|
1225
|
+
...(retrievalLanes?.length ? { retrievalLanes } : {}),
|
|
780
1226
|
ambiguityPeerIds,
|
|
781
1227
|
};
|
|
782
1228
|
}
|
|
1229
|
+
function laneMembershipByObjectKey(lanes) {
|
|
1230
|
+
const memberships = new Map();
|
|
1231
|
+
for (const lane of lanes) {
|
|
1232
|
+
for (const candidate of lane.candidates) {
|
|
1233
|
+
const next = memberships.get(candidate.objectKey) ?? [];
|
|
1234
|
+
if (!next.some((entry) => entry.lane === lane.lane && entry.rank === candidate.rank)) {
|
|
1235
|
+
next.push({ lane: lane.lane, rank: candidate.rank });
|
|
1236
|
+
}
|
|
1237
|
+
memberships.set(candidate.objectKey, next);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return memberships;
|
|
1241
|
+
}
|
|
783
1242
|
function agentEvidenceKind(candidate) {
|
|
784
1243
|
if (candidate.evidenceClass === 'certified')
|
|
785
1244
|
return 'certified_block';
|
|
@@ -915,6 +1374,11 @@ function stringArray(value) {
|
|
|
915
1374
|
? value.filter((item) => typeof item === 'string' && Boolean(item.trim()))
|
|
916
1375
|
: [];
|
|
917
1376
|
}
|
|
1377
|
+
function recordValue(value) {
|
|
1378
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
1379
|
+
? value
|
|
1380
|
+
: undefined;
|
|
1381
|
+
}
|
|
918
1382
|
function arrayNames(value) {
|
|
919
1383
|
if (!Array.isArray(value))
|
|
920
1384
|
return [];
|