@duckcodeailabs/dql-agent 1.14.1 → 1.14.2

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 (84) hide show
  1. package/dist/agent-run-engine.d.ts +9 -1
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +199 -17
  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/agentic/analyst-loop.d.ts.map +1 -1
  9. package/dist/agentic/analyst-loop.js +48 -28
  10. package/dist/agentic/analyst-loop.js.map +1 -1
  11. package/dist/agentic/research/synthesis.d.ts +4 -0
  12. package/dist/agentic/research/synthesis.d.ts.map +1 -1
  13. package/dist/agentic/research/synthesis.js +23 -8
  14. package/dist/agentic/research/synthesis.js.map +1 -1
  15. package/dist/agentic/research-agent.d.ts.map +1 -1
  16. package/dist/agentic/research-agent.js +3 -2
  17. package/dist/agentic/research-agent.js.map +1 -1
  18. package/dist/agentic/sql-authorization.d.ts.map +1 -1
  19. package/dist/agentic/sql-authorization.js +230 -2
  20. package/dist/agentic/sql-authorization.js.map +1 -1
  21. package/dist/analytical-frame.d.ts.map +1 -1
  22. package/dist/analytical-frame.js +10 -1
  23. package/dist/analytical-frame.js.map +1 -1
  24. package/dist/analytical-orchestration.d.ts +302 -0
  25. package/dist/analytical-orchestration.d.ts.map +1 -1
  26. package/dist/analytical-orchestration.js +564 -0
  27. package/dist/analytical-orchestration.js.map +1 -1
  28. package/dist/answer-loop.d.ts +48 -0
  29. package/dist/answer-loop.d.ts.map +1 -1
  30. package/dist/answer-loop.js +679 -85
  31. package/dist/answer-loop.js.map +1 -1
  32. package/dist/answer-shape.d.ts +32 -1
  33. package/dist/answer-shape.d.ts.map +1 -1
  34. package/dist/answer-shape.js +49 -3
  35. package/dist/answer-shape.js.map +1 -1
  36. package/dist/conversation/snapshot.d.ts +20 -0
  37. package/dist/conversation/snapshot.d.ts.map +1 -1
  38. package/dist/conversation/snapshot.js +30 -1
  39. package/dist/conversation/snapshot.js.map +1 -1
  40. package/dist/fixtures/ask-ai-office-shaped.d.ts +153 -0
  41. package/dist/fixtures/ask-ai-office-shaped.d.ts.map +1 -0
  42. package/dist/fixtures/ask-ai-office-shaped.js +94 -0
  43. package/dist/fixtures/ask-ai-office-shaped.js.map +1 -0
  44. package/dist/index.d.ts +4 -4
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -2
  47. package/dist/index.js.map +1 -1
  48. package/dist/intent-controller.d.ts +28 -0
  49. package/dist/intent-controller.d.ts.map +1 -1
  50. package/dist/intent-controller.js +30 -0
  51. package/dist/intent-controller.js.map +1 -1
  52. package/dist/meaning-resolution.d.ts +69 -2
  53. package/dist/meaning-resolution.d.ts.map +1 -1
  54. package/dist/meaning-resolution.js +128 -7
  55. package/dist/meaning-resolution.js.map +1 -1
  56. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  57. package/dist/metadata/analysis-planner.js +124 -7
  58. package/dist/metadata/analysis-planner.js.map +1 -1
  59. package/dist/metadata/block-fit.d.ts +18 -0
  60. package/dist/metadata/block-fit.d.ts.map +1 -1
  61. package/dist/metadata/block-fit.js +250 -38
  62. package/dist/metadata/block-fit.js.map +1 -1
  63. package/dist/metadata/catalog.d.ts.map +1 -1
  64. package/dist/metadata/catalog.js +57 -11
  65. package/dist/metadata/catalog.js.map +1 -1
  66. package/dist/metadata/meaning-evidence.d.ts +10 -1
  67. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  68. package/dist/metadata/meaning-evidence.js +261 -47
  69. package/dist/metadata/meaning-evidence.js.map +1 -1
  70. package/dist/research-loop.d.ts.map +1 -1
  71. package/dist/research-loop.js +23 -1
  72. package/dist/research-loop.js.map +1 -1
  73. package/dist/resolved-analytical-plan.d.ts +7 -0
  74. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  75. package/dist/resolved-analytical-plan.js +11 -4
  76. package/dist/resolved-analytical-plan.js.map +1 -1
  77. package/dist/router.d.ts +4 -3
  78. package/dist/router.d.ts.map +1 -1
  79. package/dist/router.js +1796 -125
  80. package/dist/router.js.map +1 -1
  81. package/dist/semantic-bridge/member-select.d.ts.map +1 -1
  82. package/dist/semantic-bridge/member-select.js +39 -21
  83. package/dist/semantic-bridge/member-select.js.map +1 -1
  84. package/package.json +5 -5
@@ -1,7 +1,7 @@
1
1
  import { type AnalysisQuestionPlan } from "./analysis-planner.js";
2
2
  import { type AnalyticalPolicyContract, type MetricCapabilityContract } from "@duckcodeailabs/dql-core";
3
3
  import type { LocalContextPack, MetadataObject } from "./catalog.js";
4
- import type { AgentRetrievalEvidence } from '../meaning-resolution.js';
4
+ import { type AgentRetrievalEvidence, type AgentRelationshipSafetyEvidence } from '../meaning-resolution.js';
5
5
  import type { KnowledgeLens } from '../domain-context.js';
6
6
  export type MetadataEvidenceClass = 'certified' | 'semantic' | 'sql';
7
7
  export type MetadataEvidenceTrust = 'certified' | 'semantic' | 'governed_sql' | 'exploratory';
@@ -35,11 +35,20 @@ export interface MetadataMeaningCandidate {
35
35
  };
36
36
  /** Snapshot-normalized execution truth; absent means capability is incomplete. */
37
37
  analyticalCapability?: MetricCapabilityContract;
38
+ /** Structured relationship facts when this card itself is a relationship. */
39
+ relationshipSafety?: AgentRelationshipSafetyEvidence[];
38
40
  provenance?: string;
39
41
  ambiguityPeerIds: string[];
40
42
  }
41
43
  export interface MetadataMeaningEvidencePackage {
42
44
  candidates: MetadataMeaningCandidate[];
45
+ /**
46
+ * Snapshot-qualified retrieval retained for host-side role admission. The
47
+ * historic per-class cards remain for compact UI diagnostics, but must not
48
+ * erase an explicit metric/entity before the one bounded meaning package is
49
+ * role-balanced.
50
+ */
51
+ qualifiedCandidates?: MetadataMeaningCandidate[];
43
52
  byEvidenceClass: Record<MetadataEvidenceClass, MetadataMeaningCandidate[]>;
44
53
  ambiguousGroups: Array<{
45
54
  candidateIds: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"meaning-evidence.d.ts","sourceRoot":"","sources":["../../src/metadata/meaning-evidence.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"meaning-evidence.d.ts","sourceRoot":"","sources":["../../src/metadata/meaning-evidence.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,13 @@
1
1
  import { scoreMetadataObjectWithAnalysisPlan, } from "./analysis-planner.js";
2
2
  import { normalizeMetricCapabilityContract, } from "@duckcodeailabs/dql-core";
3
+ import { candidateMatchesCategoricalDimensionRequirement } 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
@@ -40,6 +43,15 @@ export function buildMeaningEvidencePackage(question, questionPlan, ranked) {
40
43
  eligible.sort((left, right) => right.relevanceScore - left.relevanceScore
41
44
  || left.item.rank - right.item.rank
42
45
  || left.item.row.objectKey.localeCompare(right.item.row.objectKey));
46
+ const qualifiedPeerMap = buildAmbiguityPeers(eligible.slice(0, MAX_QUALIFIED_CANDIDATES).map((candidate) => ({
47
+ objectKey: candidate.item.row.objectKey,
48
+ aliases: candidate.aliases,
49
+ })));
50
+ const qualifiedCounts = { certified: 0, semantic: 0, sql: 0 };
51
+ const qualifiedCandidates = eligible.slice(0, MAX_QUALIFIED_CANDIDATES).map((candidate) => {
52
+ qualifiedCounts[candidate.evidenceClass] += 1;
53
+ return candidateCard(candidate.item.row, candidate.evidenceClass, qualifiedCounts[candidate.evidenceClass], candidate.relevanceScore, candidate.aliases, candidate.relevanceReasons, qualifiedPeerMap.get(candidate.item.row.objectKey) ?? []);
54
+ });
43
55
  const requiredSemanticMetricKey = questionPlan.requestedShape.measures.length > 0
44
56
  ? eligible.find((candidate) => candidate.evidenceClass === 'semantic'
45
57
  && candidate.item.row.objectType === 'semantic_metric')?.item.row.objectKey
@@ -96,6 +108,7 @@ export function buildMeaningEvidencePackage(question, questionPlan, ranked) {
96
108
  return {
97
109
  candidates: ['certified', 'semantic', 'sql']
98
110
  .flatMap((evidenceClass) => byEvidenceClass[evidenceClass]),
111
+ qualifiedCandidates,
99
112
  byEvidenceClass,
100
113
  ambiguousGroups: ambiguityGroups(peerMap),
101
114
  };
@@ -114,17 +127,141 @@ export function buildMeaningEvidencePackage(question, questionPlan, ranked) {
114
127
  * gates joins on that list, so every multi-relation governed compile returned
115
128
  * `RELATIONSHIP_PROOF_REQUIRED` and the path was unreachable in production.
116
129
  *
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.
130
+ * This only *offers* identities and compact source facts.
131
+ * `compileGovernedRelationalPlan` still enforces certified, fresh,
132
+ * automatic-join-safe (`REL-002`) before it emits a join. The router also
133
+ * requires the structured safety record for exploratory composition, so a
134
+ * draft relationship with a neutral-looking identity cannot suppress a
135
+ * clarification.
136
+ */
137
+ function relationshipSafetyFromObject(object) {
138
+ if (object.objectType !== 'relationship')
139
+ return undefined;
140
+ const payload = object.payload ?? {};
141
+ const id = firstString(payload.qualifiedId, payload.id, object.fullName, object.objectKey);
142
+ if (!id)
143
+ return undefined;
144
+ const validationPayload = recordValue(payload.validation);
145
+ const keys = Array.isArray(payload.keys)
146
+ ? payload.keys.flatMap((value) => {
147
+ const key = recordValue(value);
148
+ const from = firstString(key?.from);
149
+ const to = firstString(key?.to);
150
+ return from && to ? [{ from, to }] : [];
151
+ })
152
+ : [];
153
+ const validation = validationPayload
154
+ ? {
155
+ status: firstString(validationPayload.status),
156
+ checkedAt: firstString(validationPayload.checkedAt, validationPayload.checked_at),
157
+ queryFingerprint: firstString(validationPayload.queryFingerprint, validationPayload.query_fingerprint),
158
+ proofFingerprint: firstString(validationPayload.proofFingerprint, validationPayload.proof_fingerprint),
159
+ }
160
+ : undefined;
161
+ return {
162
+ id,
163
+ aliases: uniqueStrings([
164
+ firstString(payload.qualifiedId) ?? '',
165
+ firstString(payload.id) ?? '',
166
+ firstString(payload.localId) ?? '',
167
+ object.objectKey,
168
+ object.fullName ?? '',
169
+ object.name,
170
+ ]).filter((identity) => identity !== id),
171
+ from: firstString(payload.from),
172
+ to: firstString(payload.to),
173
+ keys,
174
+ status: firstString(payload.status, object.status),
175
+ cardinality: firstString(payload.cardinality),
176
+ fanout: firstString(payload.fanout),
177
+ ...(typeof payload.staleCertification === 'boolean'
178
+ ? { staleCertification: payload.staleCertification }
179
+ : {}),
180
+ ...(typeof payload.automaticJoinAllowed === 'boolean'
181
+ ? { automaticJoinAllowed: payload.automaticJoinAllowed }
182
+ : {}),
183
+ certificationFingerprint: firstString(payload.certificationFingerprint),
184
+ evidenceExpiresAt: firstString(payload.evidenceExpiresAt, payload.evidence_expires_at),
185
+ ...(validation ? { validation } : {}),
186
+ };
187
+ }
188
+ function normalizedRelationshipReference(value) {
189
+ return value.trim().toLowerCase();
190
+ }
191
+ function relationshipReferenceLeaf(value) {
192
+ const normalized = normalizedRelationshipReference(value).replace(/["`\[\]]/g, '');
193
+ return normalized.split(/::|:|\./).filter(Boolean).at(-1) ?? normalized;
194
+ }
195
+ function candidateRelationshipReferences(candidate) {
196
+ return uniqueStrings([
197
+ candidate.qualifiedId ?? '',
198
+ candidate.id,
199
+ candidate.primaryEntity ?? '',
200
+ candidate.analyticalCapability?.primaryEntityId ?? '',
201
+ ...(candidate.sourceObjects ?? []),
202
+ ]);
203
+ }
204
+ function isRawRelationCandidate(candidate) {
205
+ return candidate.kind === 'dbt_model'
206
+ || candidate.kind === 'dbt_source'
207
+ || candidate.kind === 'sql_table';
208
+ }
209
+ /**
210
+ * Bind relationship proof only through a canonical entity/relation identity.
211
+ * A leaf fallback is deliberately allowed only when both the endpoint and the
212
+ * raw relation leaf occur exactly once in the retrieval snapshot. This keeps
213
+ * two domains with an `orders` model from inheriting each other's proof.
120
214
  */
121
215
  function attachRelationshipEvidence(candidates, contextObjects) {
122
216
  const relationships = contextObjects.filter((object) => object.objectType === 'relationship');
123
217
  if (relationships.length === 0)
124
218
  return;
125
- const identitiesByEntity = new Map();
219
+ const endpointIdsByReference = new Map();
220
+ const endpointIdsByLeaf = new Map();
221
+ const identitiesByEndpoint = new Map();
222
+ const safetyByEndpoint = new Map();
223
+ const registerEndpointReference = (endpoint, reference) => {
224
+ if (!reference?.trim())
225
+ return;
226
+ const normalizedReference = normalizedRelationshipReference(reference);
227
+ const existing = endpointIdsByReference.get(normalizedReference) ?? new Set();
228
+ existing.add(endpoint);
229
+ endpointIdsByReference.set(normalizedReference, existing);
230
+ };
231
+ const registerEndpoint = (endpoint) => {
232
+ const normalizedEndpoint = normalizedRelationshipReference(endpoint);
233
+ const existing = endpointIdsByLeaf.get(relationshipReferenceLeaf(endpoint)) ?? new Set();
234
+ existing.add(endpoint);
235
+ endpointIdsByLeaf.set(relationshipReferenceLeaf(endpoint), existing);
236
+ registerEndpointReference(endpoint, endpoint);
237
+ // Keep the canonical normalized key explicit even when endpoint casing
238
+ // differs from the relationship payload or a physical dbt reference.
239
+ registerEndpointReference(endpoint, normalizedEndpoint);
240
+ };
241
+ // DQL entity metadata is the authoritative bridge from a canonical modeling
242
+ // endpoint to its dbt/runtime relation. Preserve that bridge rather than
243
+ // reducing it to a leaf name.
244
+ for (const object of contextObjects.filter((item) => item.objectType === 'dql_entity')) {
245
+ const payload = object.payload ?? {};
246
+ const endpoint = firstString(payload.qualifiedId, payload.id, object.fullName, object.objectKey);
247
+ if (!endpoint)
248
+ continue;
249
+ registerEndpoint(endpoint);
250
+ for (const reference of [
251
+ payload.qualifiedId,
252
+ payload.id,
253
+ payload.localId,
254
+ payload.dbtUniqueId,
255
+ payload.relation,
256
+ payload.table,
257
+ object.objectKey,
258
+ object.fullName,
259
+ ])
260
+ registerEndpointReference(endpoint, firstString(reference));
261
+ }
126
262
  for (const object of relationships) {
127
263
  const payload = (object.payload ?? {});
264
+ const safety = relationshipSafetyFromObject(object);
128
265
  const identities = [payload.qualifiedId, payload.id, payload.localId, object.objectKey, object.fullName, object.name]
129
266
  .filter((value) => Boolean(value));
130
267
  if (identities.length === 0)
@@ -132,58 +269,113 @@ function attachRelationshipEvidence(candidates, contextObjects) {
132
269
  for (const endpoint of [payload.from, payload.to]) {
133
270
  if (!endpoint)
134
271
  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);
272
+ registerEndpoint(endpoint);
273
+ const normalizedEndpoint = normalizedRelationshipReference(endpoint);
274
+ const endpointIdentities = identitiesByEndpoint.get(normalizedEndpoint) ?? new Set();
275
+ for (const identity of identities)
276
+ endpointIdentities.add(identity);
277
+ identitiesByEndpoint.set(normalizedEndpoint, endpointIdentities);
278
+ if (safety) {
279
+ const safetyForEndpoint = safetyByEndpoint.get(normalizedEndpoint) ?? new Map();
280
+ safetyForEndpoint.set(safety.id, safety);
281
+ safetyByEndpoint.set(normalizedEndpoint, safetyForEndpoint);
140
282
  }
141
283
  }
142
284
  }
143
- if (identitiesByEntity.size === 0)
285
+ if (identitiesByEndpoint.size === 0)
144
286
  return;
287
+ const rawRelationIdsByLeaf = new Map();
288
+ const registerRawRelation = (relationId, references) => {
289
+ for (const leaf of new Set(references.map(relationshipReferenceLeaf))) {
290
+ const ids = rawRelationIdsByLeaf.get(leaf) ?? new Set();
291
+ ids.add(relationId);
292
+ rawRelationIdsByLeaf.set(leaf, ids);
293
+ }
294
+ };
295
+ for (const candidate of candidates.filter(isRawRelationCandidate)) {
296
+ registerRawRelation(normalizedRelationshipReference(candidate.qualifiedId ?? candidate.id), candidateRelationshipReferences(candidate));
297
+ }
298
+ // Candidate cards are bounded, but the immutable context snapshot can retain
299
+ // another raw relation with the same leaf outside that compact package. Count
300
+ // it too before permitting a leaf fallback.
301
+ for (const object of contextObjects.filter((item) => item.objectType === 'dbt_model'
302
+ || item.objectType === 'dbt_source'
303
+ || item.objectType === 'warehouse_table'
304
+ || item.objectType === 'runtime_table')) {
305
+ const payload = object.payload ?? {};
306
+ const relationId = firstString(payload.qualifiedId, payload.uniqueId, object.fullName, object.objectKey) ?? object.objectKey;
307
+ registerRawRelation(normalizedRelationshipReference(relationId), uniqueStrings([
308
+ relationId,
309
+ firstString(payload.qualifiedId) ?? '',
310
+ firstString(payload.uniqueId) ?? '',
311
+ firstString(payload.relation) ?? '',
312
+ firstString(payload.table) ?? '',
313
+ object.name,
314
+ object.fullName ?? '',
315
+ ]));
316
+ }
317
+ const endpointsForReferences = (references) => {
318
+ const direct = new Set();
319
+ for (const reference of references) {
320
+ for (const endpoint of endpointIdsByReference.get(normalizedRelationshipReference(reference)) ?? []) {
321
+ direct.add(endpoint);
322
+ }
323
+ }
324
+ if (direct.size > 0)
325
+ return direct;
326
+ const fallback = new Set();
327
+ for (const leaf of new Set(references.map(relationshipReferenceLeaf))) {
328
+ const endpoints = endpointIdsByLeaf.get(leaf);
329
+ if (endpoints?.size !== 1 || rawRelationIdsByLeaf.get(leaf)?.size !== 1)
330
+ continue;
331
+ for (const endpoint of endpoints)
332
+ fallback.add(endpoint);
333
+ }
334
+ return fallback;
335
+ };
145
336
  for (const candidate of candidates) {
146
337
  for (const dimension of candidate.analyticalCapability?.dimensions ?? []) {
147
338
  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);
339
+ const endpoints = endpointsForReferences([dimension.entityId, ...(dimension.nativeGroupingPath ?? [])]);
340
+ for (const endpoint of endpoints) {
341
+ for (const identity of identitiesByEndpoint.get(normalizedRelationshipReference(endpoint)) ?? []) {
342
+ pathRelationships.add(identity);
152
343
  }
153
344
  }
154
345
  if (pathRelationships.size > 0)
155
346
  dimension.relationshipPathIds = [...pathRelationships].sort();
156
347
  }
348
+ const endpoints = endpointsForReferences(candidateRelationshipReferences(candidate));
157
349
  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);
350
+ const touchedSafety = new Map((candidate.relationshipSafety ?? []).map((safety) => [safety.id, safety]));
351
+ for (const endpoint of endpoints) {
352
+ const normalizedEndpoint = normalizedRelationshipReference(endpoint);
353
+ for (const identity of identitiesByEndpoint.get(normalizedEndpoint) ?? [])
354
+ touched.add(identity);
355
+ for (const safety of safetyByEndpoint.get(normalizedEndpoint)?.values() ?? []) {
356
+ touchedSafety.set(safety.id, safety);
173
357
  }
174
358
  }
359
+ if (endpoints.size > 0) {
360
+ candidate.relationshipEndpointIds = [...new Set([
361
+ ...(candidate.relationshipEndpointIds ?? []),
362
+ ...endpoints,
363
+ ])].sort();
364
+ }
175
365
  if (touched.size > 0)
176
366
  candidate.relationshipEvidence = [...touched].sort();
367
+ if (touchedSafety.size > 0) {
368
+ candidate.relationshipSafety = [...touchedSafety.values()]
369
+ .sort((left, right) => left.id.localeCompare(right.id));
370
+ }
177
371
  }
178
372
  }
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
373
  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));
374
+ const sourceCandidates = evidence.qualifiedCandidates?.length
375
+ ? evidence.qualifiedCandidates
376
+ : evidence.candidates;
377
+ const maxRelevance = Math.max(1, ...sourceCandidates.map((candidate) => candidate.relevanceScore));
378
+ const candidates = sourceCandidates.map((candidate) => agentCandidateFromMeaning(candidate, maxRelevance));
187
379
  const trustedBindings = trustedMemberBindingEvidence(questionPlan, candidates, options.contextObjects ?? []);
188
380
  for (const member of trustedBindings.candidates) {
189
381
  if (!candidates.some((candidate) => candidate.id === member.id))
@@ -265,6 +457,7 @@ function agentCandidateFromMeaning(candidate, maxRelevance = 1) {
265
457
  timeGrains: candidate.businessShape.timeGrains,
266
458
  requiredParameters: candidate.businessShape.parameters,
267
459
  sourceObjects: candidate.businessShape.sourceRelations,
460
+ relationshipSafety: candidate.relationshipSafety,
268
461
  relevanceScore: Number((candidate.relevanceScore / maxRelevance).toFixed(6)),
269
462
  matchReasons: candidate.relevanceReasons,
270
463
  // Fit validation remains host-owned. Metadata only reports facts here and
@@ -353,21 +546,21 @@ function supplementalClarificationEvidence(plan, objects, existing) {
353
546
  if (!['semantic_metric', 'semantic_measure', 'semantic_dimension'].includes(object.objectType))
354
547
  return [];
355
548
  const names = aliasesFor(object).map(normalizeText);
356
- const role = normalizeDimensionRole(object.name);
549
+ const candidate = agentCandidateFromMeaning(candidateCard(object, 'semantic', 1, 1, aliasesFor(object), ['requested clarification capability'], []));
357
550
  const relevant = object.objectType === 'semantic_metric' || object.objectType === 'semantic_measure'
358
551
  ? requestedMeasures.some((term) => names.some((name) => phraseTermsMatch(term, name)))
359
552
  : requestedDimensions.some((term) => names.some((name) => phraseTermsMatch(term, name))
360
- || (isGeographicRole(term) && isGeographicRole(role)));
553
+ || candidateMatchesCategoricalDimensionRequirement(candidate, [term]));
361
554
  if (!relevant)
362
555
  return [];
363
- const card = candidateCard(object, 'semantic', 1, 1, aliasesFor(object), ['requested clarification capability'], []);
364
- return [agentCandidateFromMeaning(card)];
556
+ return [candidate];
365
557
  });
366
558
  const declaredDimensionCards = existing.flatMap((candidate) => (candidate.dimensions ?? []).flatMap((dimensionId) => {
367
559
  if (!/[:./]/.test(dimensionId) || existingIds.has(dimensionId))
368
560
  return [];
369
561
  const role = normalizeDimensionRole(dimensionId);
370
- const relevant = requestedDimensions.some((term) => phraseTermsMatch(term, role) || (isGeographicRole(term) && isGeographicRole(role)));
562
+ const relevant = requestedDimensions.some((term) => phraseTermsMatch(term, role)
563
+ || candidateMatchesCategoricalDimensionRequirement(candidate, [term]));
371
564
  if (!relevant)
372
565
  return [];
373
566
  return [{
@@ -380,6 +573,7 @@ function supplementalClarificationEvidence(plan, objects, existing) {
380
573
  definition: `Modeled ${role} dimension available through ${candidate.name}.`,
381
574
  primaryEntity: candidate.primaryEntity,
382
575
  dimensions: [dimensionId],
576
+ compatibilityFacts: candidate.compatibilityFacts,
383
577
  relevanceScore: candidate.relevanceScore,
384
578
  matchReasons: ['requested clarification capability role'],
385
579
  compatibility: 'partial',
@@ -454,7 +648,7 @@ function supplementalClarificationEvidence(plan, objects, existing) {
454
648
  if (candidate.kind !== 'semantic_member')
455
649
  return false;
456
650
  return candidateTerms.some((term) => phraseTermsMatch(lane.term, term))
457
- || (isGeographicRole(lane.term) && candidateTerms.some(isGeographicRole));
651
+ || candidateMatchesCategoricalDimensionRequirement(candidate, [lane.term]);
458
652
  }).sort((left, right) => right.relevanceScore - left.relevanceScore || left.id.localeCompare(right.id));
459
653
  for (const candidate of matches.slice(0, MAX_PER_REQUESTED_ROLE)) {
460
654
  if (!selected.some((item) => (item.qualifiedId ?? item.id) === (candidate.qualifiedId ?? candidate.id))) {
@@ -526,9 +720,6 @@ function dimensionLookupAliases(dimensions) {
526
720
  return role && role !== leaf ? [dimension, role] : [dimension];
527
721
  }));
528
722
  }
529
- function isGeographicRole(value) {
530
- return /^(?:region|location|geography|territory|market|site|store)$/.test(normalizeDimensionRole(value));
531
- }
532
723
  /**
533
724
  * Bind current-turn values only when an authorized runtime-value observation
534
725
  * and a semantic dimension's exact physical table/column identity agree. Text
@@ -640,9 +831,12 @@ export function applyContextPackCompatibility(evidence, pack, selectedEvidenceId
640
831
  // `monthly_revenue` artifact lose to a semantic metric for the ordinary
641
832
  // paraphrase "What is monthly revenue?". Do not promote lexical
642
833
  // relevance alone; require the catalog's executable fit verdict.
834
+ const requestedMeasures = pack.questionPlan.requestedShape.measures;
835
+ const blockDeclaresRequestedMeasures = certifiedCandidateExplicitlyCoversMeasures(candidate, requestedMeasures);
643
836
  const completeCertifiedFit = fit?.action === 'certified_answer'
644
837
  && (fit.fit.kind === 'exact' || fit.fit.kind === 'trim_safe')
645
- && fit.fit.confidence === 'high';
838
+ && fit.fit.confidence === 'high'
839
+ && blockDeclaresRequestedMeasures;
646
840
  return {
647
841
  ...candidate,
648
842
  // Lexical equality to a block name/alias is useful retrieval signal,
@@ -657,7 +851,7 @@ export function applyContextPackCompatibility(evidence, pack, selectedEvidenceId
657
851
  // broader question. Exclude it before both meaning resolution and
658
852
  // identifier-bound clarification; later fit rejection is too late.
659
853
  eligible: unrequestedStaticScope ? false : candidate.eligible,
660
- compatibility: fit?.action === 'certified_answer'
854
+ compatibility: completeCertifiedFit
661
855
  ? 'compatible'
662
856
  : fit?.action === 'rejected_for_fit'
663
857
  ? 'incompatible'
@@ -724,6 +918,7 @@ function trustTierFor(object, evidenceClass) {
724
918
  function candidateCard(object, evidenceClass, classRank, relevanceScore, aliases, relevanceReasons, ambiguityPeerIds) {
725
919
  const payload = object.payload ?? {};
726
920
  const analyticalCapability = normalizeMetricCapabilityContract(payload.analyticalCapability);
921
+ const relationshipSafety = relationshipSafetyFromObject(object);
727
922
  const qualifiedId = firstString(payload.qualifiedId, payload.uniqueId, object.fullName, object.objectKey) ?? object.objectKey;
728
923
  const parameters = arrayNames(payload.parameters ?? payload.parameterPolicy);
729
924
  const outputs = uniqueStrings([
@@ -731,7 +926,20 @@ function candidateCard(object, evidenceClass, classRank, relevanceScore, aliases
731
926
  ...arrayNames(payload.outputContract),
732
927
  ...arrayNames(payload.outputs),
733
928
  ]);
929
+ const declaredSemanticGeography = [
930
+ ...stringArray(payload.semanticRoles),
931
+ ...stringArray(payload.semantic_roles),
932
+ firstString(payload.semanticRole, payload.semantic_role) ?? '',
933
+ ].some((role) => {
934
+ const normalized = normalizeText(role);
935
+ return normalized === 'geography' || normalized === 'geographic';
936
+ });
734
937
  const compatibilityFacts = uniqueStrings([
938
+ // Carry only authored, snapshot-local compatibility facts. Consumers use
939
+ // these as typed declarations, never as fuzzy lexical aliases.
940
+ ...stringArray(payload.compatibilityFacts),
941
+ ...stringArray(payload.compatibility_facts),
942
+ ...(declaredSemanticGeography ? ['semantic-role: geography'] : []),
735
943
  firstString(payload.grain) ? `grain: ${firstString(payload.grain)}` : '',
736
944
  ...stringArray(payload.dimensions).slice(0, 8).map((value) => `dimension: ${value}`),
737
945
  ...parameters.slice(0, 8).map((value) => `parameter: ${value}`),
@@ -776,6 +984,7 @@ function candidateCard(object, evidenceClass, classRank, relevanceScore, aliases
776
984
  ]).slice(0, 12),
777
985
  },
778
986
  ...(analyticalCapability ? { analyticalCapability } : {}),
987
+ ...(relationshipSafety ? { relationshipSafety: [relationshipSafety] } : {}),
779
988
  provenance: firstString(payload.provenance, object.sourceSystem),
780
989
  ambiguityPeerIds,
781
990
  };
@@ -915,6 +1124,11 @@ function stringArray(value) {
915
1124
  ? value.filter((item) => typeof item === 'string' && Boolean(item.trim()))
916
1125
  : [];
917
1126
  }
1127
+ function recordValue(value) {
1128
+ return value && typeof value === 'object' && !Array.isArray(value)
1129
+ ? value
1130
+ : undefined;
1131
+ }
918
1132
  function arrayNames(value) {
919
1133
  if (!Array.isArray(value))
920
1134
  return [];