@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.
Files changed (174) hide show
  1. package/dist/agent-run-engine.d.ts +71 -2
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +1048 -31
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-gates.d.ts.map +1 -1
  6. package/dist/agent-run-gates.js +12 -0
  7. package/dist/agent-run-gates.js.map +1 -1
  8. package/dist/agent-run-planner.d.ts +6 -0
  9. package/dist/agent-run-planner.d.ts.map +1 -1
  10. package/dist/agent-run-planner.js +2 -0
  11. package/dist/agent-run-planner.js.map +1 -1
  12. package/dist/agent-run-store.d.ts.map +1 -1
  13. package/dist/agent-run-store.js +48 -3
  14. package/dist/agent-run-store.js.map +1 -1
  15. package/dist/agentic/analyst-loop.d.ts +7 -1
  16. package/dist/agentic/analyst-loop.d.ts.map +1 -1
  17. package/dist/agentic/analyst-loop.js +211 -28
  18. package/dist/agentic/analyst-loop.js.map +1 -1
  19. package/dist/agentic/assumptions.d.ts +1 -1
  20. package/dist/agentic/assumptions.d.ts.map +1 -1
  21. package/dist/agentic/assumptions.js.map +1 -1
  22. package/dist/agentic/research/synthesis.d.ts +4 -0
  23. package/dist/agentic/research/synthesis.d.ts.map +1 -1
  24. package/dist/agentic/research/synthesis.js +23 -8
  25. package/dist/agentic/research/synthesis.js.map +1 -1
  26. package/dist/agentic/research-agent.d.ts.map +1 -1
  27. package/dist/agentic/research-agent.js +13 -3
  28. package/dist/agentic/research-agent.js.map +1 -1
  29. package/dist/agentic/sql-authorization.d.ts +8 -0
  30. package/dist/agentic/sql-authorization.d.ts.map +1 -1
  31. package/dist/agentic/sql-authorization.js +233 -2
  32. package/dist/agentic/sql-authorization.js.map +1 -1
  33. package/dist/analytical-compatibility.d.ts.map +1 -1
  34. package/dist/analytical-compatibility.js +8 -1
  35. package/dist/analytical-compatibility.js.map +1 -1
  36. package/dist/analytical-failure-repair.d.ts.map +1 -1
  37. package/dist/analytical-failure-repair.js +6 -2
  38. package/dist/analytical-failure-repair.js.map +1 -1
  39. package/dist/analytical-frame.d.ts +23 -1
  40. package/dist/analytical-frame.d.ts.map +1 -1
  41. package/dist/analytical-frame.js +332 -17
  42. package/dist/analytical-frame.js.map +1 -1
  43. package/dist/analytical-orchestration.d.ts +613 -0
  44. package/dist/analytical-orchestration.d.ts.map +1 -1
  45. package/dist/analytical-orchestration.js +1007 -0
  46. package/dist/analytical-orchestration.js.map +1 -1
  47. package/dist/answer-loop.d.ts +110 -6
  48. package/dist/answer-loop.d.ts.map +1 -1
  49. package/dist/answer-loop.js +1180 -125
  50. package/dist/answer-loop.js.map +1 -1
  51. package/dist/answer-shape.d.ts +32 -1
  52. package/dist/answer-shape.d.ts.map +1 -1
  53. package/dist/answer-shape.js +49 -3
  54. package/dist/answer-shape.js.map +1 -1
  55. package/dist/ask-observability/index.d.ts +11 -0
  56. package/dist/ask-observability/index.d.ts.map +1 -0
  57. package/dist/ask-observability/index.js +7 -0
  58. package/dist/ask-observability/index.js.map +1 -0
  59. package/dist/ask-observability/instrumentation.d.ts +35 -0
  60. package/dist/ask-observability/instrumentation.d.ts.map +1 -0
  61. package/dist/ask-observability/instrumentation.js +472 -0
  62. package/dist/ask-observability/instrumentation.js.map +1 -0
  63. package/dist/ask-observability/observer.d.ts +62 -0
  64. package/dist/ask-observability/observer.d.ts.map +1 -0
  65. package/dist/ask-observability/observer.js +537 -0
  66. package/dist/ask-observability/observer.js.map +1 -0
  67. package/dist/ask-observability/portable.d.ts +98 -0
  68. package/dist/ask-observability/portable.d.ts.map +1 -0
  69. package/dist/ask-observability/portable.js +527 -0
  70. package/dist/ask-observability/portable.js.map +1 -0
  71. package/dist/ask-observability/store.d.ts +130 -0
  72. package/dist/ask-observability/store.d.ts.map +1 -0
  73. package/dist/ask-observability/store.js +1114 -0
  74. package/dist/ask-observability/store.js.map +1 -0
  75. package/dist/ask-observability/types.d.ts +357 -0
  76. package/dist/ask-observability/types.d.ts.map +1 -0
  77. package/dist/ask-observability/types.js +2 -0
  78. package/dist/ask-observability/types.js.map +1 -0
  79. package/dist/ask-observability/utils.d.ts +21 -0
  80. package/dist/ask-observability/utils.d.ts.map +1 -0
  81. package/dist/ask-observability/utils.js +102 -0
  82. package/dist/ask-observability/utils.js.map +1 -0
  83. package/dist/conversation/snapshot.d.ts +20 -0
  84. package/dist/conversation/snapshot.d.ts.map +1 -1
  85. package/dist/conversation/snapshot.js +30 -1
  86. package/dist/conversation/snapshot.js.map +1 -1
  87. package/dist/fixtures/ask-ai-office-shaped.d.ts +153 -0
  88. package/dist/fixtures/ask-ai-office-shaped.d.ts.map +1 -0
  89. package/dist/fixtures/ask-ai-office-shaped.js +94 -0
  90. package/dist/fixtures/ask-ai-office-shaped.js.map +1 -0
  91. package/dist/generated-analytical-proposal.d.ts +37 -1
  92. package/dist/generated-analytical-proposal.d.ts.map +1 -1
  93. package/dist/generated-analytical-proposal.js +211 -3
  94. package/dist/generated-analytical-proposal.js.map +1 -1
  95. package/dist/governed-relational-compiler.d.ts +15 -0
  96. package/dist/governed-relational-compiler.d.ts.map +1 -1
  97. package/dist/governed-relational-compiler.js +99 -1
  98. package/dist/governed-relational-compiler.js.map +1 -1
  99. package/dist/index.d.ts +11 -9
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +7 -5
  102. package/dist/index.js.map +1 -1
  103. package/dist/intent-controller.d.ts +38 -0
  104. package/dist/intent-controller.d.ts.map +1 -1
  105. package/dist/intent-controller.js +31 -1
  106. package/dist/intent-controller.js.map +1 -1
  107. package/dist/meaning-resolution.d.ts +141 -2
  108. package/dist/meaning-resolution.d.ts.map +1 -1
  109. package/dist/meaning-resolution.js +444 -30
  110. package/dist/meaning-resolution.js.map +1 -1
  111. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  112. package/dist/metadata/analysis-planner.js +144 -7
  113. package/dist/metadata/analysis-planner.js.map +1 -1
  114. package/dist/metadata/block-fit.d.ts +18 -0
  115. package/dist/metadata/block-fit.d.ts.map +1 -1
  116. package/dist/metadata/block-fit.js +250 -38
  117. package/dist/metadata/block-fit.js.map +1 -1
  118. package/dist/metadata/catalog.d.ts.map +1 -1
  119. package/dist/metadata/catalog.js +58 -12
  120. package/dist/metadata/catalog.js.map +1 -1
  121. package/dist/metadata/meaning-evidence.d.ts +19 -3
  122. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  123. package/dist/metadata/meaning-evidence.js +518 -54
  124. package/dist/metadata/meaning-evidence.js.map +1 -1
  125. package/dist/plan-execution-adapter.d.ts +9 -1
  126. package/dist/plan-execution-adapter.d.ts.map +1 -1
  127. package/dist/plan-execution-adapter.js +93 -13
  128. package/dist/plan-execution-adapter.js.map +1 -1
  129. package/dist/provider-egress.d.ts +24 -10
  130. package/dist/provider-egress.d.ts.map +1 -1
  131. package/dist/provider-egress.js +22 -17
  132. package/dist/provider-egress.js.map +1 -1
  133. package/dist/providers/claude.d.ts.map +1 -1
  134. package/dist/providers/claude.js +10 -9
  135. package/dist/providers/claude.js.map +1 -1
  136. package/dist/providers/dispatch.d.ts +27 -1
  137. package/dist/providers/dispatch.d.ts.map +1 -1
  138. package/dist/providers/dispatch.js +63 -1
  139. package/dist/providers/dispatch.js.map +1 -1
  140. package/dist/providers/gemini.js +10 -10
  141. package/dist/providers/gemini.js.map +1 -1
  142. package/dist/providers/index.d.ts +2 -2
  143. package/dist/providers/index.d.ts.map +1 -1
  144. package/dist/providers/index.js +1 -1
  145. package/dist/providers/index.js.map +1 -1
  146. package/dist/providers/ollama.js +15 -15
  147. package/dist/providers/ollama.js.map +1 -1
  148. package/dist/providers/openai.js +22 -22
  149. package/dist/providers/openai.js.map +1 -1
  150. package/dist/providers/types.d.ts +52 -0
  151. package/dist/providers/types.d.ts.map +1 -1
  152. package/dist/providers/types.js.map +1 -1
  153. package/dist/relationship-proof.d.ts +81 -0
  154. package/dist/relationship-proof.d.ts.map +1 -1
  155. package/dist/relationship-proof.js +228 -4
  156. package/dist/relationship-proof.js.map +1 -1
  157. package/dist/research-loop.d.ts.map +1 -1
  158. package/dist/research-loop.js +23 -1
  159. package/dist/research-loop.js.map +1 -1
  160. package/dist/resolved-analytical-plan.d.ts +22 -1
  161. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  162. package/dist/resolved-analytical-plan.js +773 -49
  163. package/dist/resolved-analytical-plan.js.map +1 -1
  164. package/dist/router.d.ts +14 -4
  165. package/dist/router.d.ts.map +1 -1
  166. package/dist/router.js +3668 -425
  167. package/dist/router.js.map +1 -1
  168. package/dist/semantic-bridge/compose.d.ts.map +1 -1
  169. package/dist/semantic-bridge/compose.js +23 -4
  170. package/dist/semantic-bridge/compose.js.map +1 -1
  171. package/dist/semantic-bridge/member-select.d.ts.map +1 -1
  172. package/dist/semantic-bridge/member-select.js +39 -21
  173. package/dist/semantic-bridge/member-select.js.map +1 -1
  174. 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* the identities. `compileGovernedRelationalPlan` still
118
- * enforces certified, fresh, automatic-join-safe (`REL-002`) before it emits a
119
- * join, so surfacing a draft relationship here cannot authorize one.
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 identitiesByEntity = new Map();
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
- for (const alias of entityAliasesForRelationshipMatch(endpoint)) {
136
- const existing = identitiesByEntity.get(alias) ?? new Set();
137
- for (const identity of identities)
138
- existing.add(identity);
139
- identitiesByEntity.set(alias, existing);
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 (identitiesByEntity.size === 0)
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
- for (const reference of [dimension.entityId, ...(dimension.nativeGroupingPath ?? [])]) {
149
- for (const alias of entityAliasesForRelationshipMatch(reference)) {
150
- for (const identity of identitiesByEntity.get(alias) ?? [])
151
- pathRelationships.add(identity);
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 capabilityPathEntities = candidate.analyticalCapability?.dimensions.flatMap((dimension) => [
159
- dimension.entityId,
160
- ...(dimension.nativeGroupingPath ?? []),
161
- ]) ?? [];
162
- for (const reference of [
163
- candidate.primaryEntity,
164
- candidate.analyticalCapability?.primaryEntityId,
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 maxRelevance = Math.max(1, ...evidence.candidates.map((candidate) => candidate.relevanceScore));
186
- const candidates = evidence.candidates.map((candidate) => agentCandidateFromMeaning(candidate, maxRelevance));
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 role = normalizeDimensionRole(object.name);
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
- || (isGeographicRole(term) && isGeographicRole(role)));
556
+ || candidateMatchesCategoricalDimensionRequirement(candidate, [term]));
361
557
  if (!relevant)
362
558
  return [];
363
- const card = candidateCard(object, 'semantic', 1, 1, aliasesFor(object), ['requested clarification capability'], []);
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) || (isGeographicRole(term) && isGeographicRole(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
- const unique = [...objectCards, ...declaredDimensionCards, ...rankingMeasureCards]
434
- .filter((candidate, index, all) => all.findIndex((other) => (other.qualifiedId ?? other.id) === (candidate.qualifiedId ?? candidate.id)) === index);
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
- || (isGeographicRole(lane.term) && candidateTerms.some(isGeographicRole));
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 completeCertifiedFit = fit?.action === 'certified_answer'
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: fit?.action === 'certified_answer'
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 qualifiedId = firstString(payload.qualifiedId, payload.uniqueId, object.fullName, object.objectKey) ?? object.objectKey;
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 [];