@duckcodeailabs/dql-agent 1.13.4 → 1.14.0

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 (137) hide show
  1. package/dist/agent-run-engine.d.ts +87 -7
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +559 -40
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-store.d.ts.map +1 -1
  6. package/dist/agent-run-store.js +42 -25
  7. package/dist/agent-run-store.js.map +1 -1
  8. package/dist/agentic/tool-loop.d.ts.map +1 -1
  9. package/dist/agentic/tool-loop.js +100 -32
  10. package/dist/agentic/tool-loop.js.map +1 -1
  11. package/dist/aggregation-safety-proof.d.ts +41 -0
  12. package/dist/aggregation-safety-proof.d.ts.map +1 -0
  13. package/dist/aggregation-safety-proof.js +511 -0
  14. package/dist/aggregation-safety-proof.js.map +1 -0
  15. package/dist/analytical-compatibility.d.ts.map +1 -1
  16. package/dist/analytical-compatibility.js +42 -25
  17. package/dist/analytical-compatibility.js.map +1 -1
  18. package/dist/analytical-execution-graph.d.ts +4 -21
  19. package/dist/analytical-execution-graph.d.ts.map +1 -1
  20. package/dist/analytical-execution-graph.js +27 -0
  21. package/dist/analytical-execution-graph.js.map +1 -1
  22. package/dist/analytical-frame.d.ts +12 -1
  23. package/dist/analytical-frame.d.ts.map +1 -1
  24. package/dist/analytical-frame.js +102 -2
  25. package/dist/analytical-frame.js.map +1 -1
  26. package/dist/analytical-orchestration.d.ts +256 -0
  27. package/dist/analytical-orchestration.d.ts.map +1 -0
  28. package/dist/analytical-orchestration.js +419 -0
  29. package/dist/analytical-orchestration.js.map +1 -0
  30. package/dist/analytical-result-facts.d.ts +54 -1
  31. package/dist/analytical-result-facts.d.ts.map +1 -1
  32. package/dist/analytical-result-facts.js +183 -0
  33. package/dist/analytical-result-facts.js.map +1 -1
  34. package/dist/answer-loop.d.ts +44 -3
  35. package/dist/answer-loop.d.ts.map +1 -1
  36. package/dist/answer-loop.js +771 -38
  37. package/dist/answer-loop.js.map +1 -1
  38. package/dist/cascade/budgets.js +1 -1
  39. package/dist/cascade/route-policy.d.ts +1 -1
  40. package/dist/cascade/route-policy.d.ts.map +1 -1
  41. package/dist/cascade/route-policy.js +2 -0
  42. package/dist/cascade/route-policy.js.map +1 -1
  43. package/dist/conversation/snapshot.d.ts.map +1 -1
  44. package/dist/conversation/snapshot.js +3 -0
  45. package/dist/conversation/snapshot.js.map +1 -1
  46. package/dist/conversation/turn-trust.d.ts.map +1 -1
  47. package/dist/conversation/turn-trust.js +6 -0
  48. package/dist/conversation/turn-trust.js.map +1 -1
  49. package/dist/generated-analytical-proposal.d.ts +37 -0
  50. package/dist/generated-analytical-proposal.d.ts.map +1 -0
  51. package/dist/generated-analytical-proposal.js +219 -0
  52. package/dist/generated-analytical-proposal.js.map +1 -0
  53. package/dist/index.d.ts +11 -5
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +7 -3
  56. package/dist/index.js.map +1 -1
  57. package/dist/intent-controller.d.ts +8 -1
  58. package/dist/intent-controller.d.ts.map +1 -1
  59. package/dist/intent-controller.js.map +1 -1
  60. package/dist/kg/build.js +102 -6
  61. package/dist/kg/build.js.map +1 -1
  62. package/dist/meaning-resolution.d.ts +30 -0
  63. package/dist/meaning-resolution.d.ts.map +1 -1
  64. package/dist/meaning-resolution.js +61 -8
  65. package/dist/meaning-resolution.js.map +1 -1
  66. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  67. package/dist/metadata/analysis-planner.js +46 -2
  68. package/dist/metadata/analysis-planner.js.map +1 -1
  69. package/dist/metadata/block-fit.d.ts.map +1 -1
  70. package/dist/metadata/block-fit.js +55 -2
  71. package/dist/metadata/block-fit.js.map +1 -1
  72. package/dist/metadata/catalog.d.ts +24 -0
  73. package/dist/metadata/catalog.d.ts.map +1 -1
  74. package/dist/metadata/catalog.js +227 -42
  75. package/dist/metadata/catalog.js.map +1 -1
  76. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  77. package/dist/metadata/meaning-evidence.js +408 -36
  78. package/dist/metadata/meaning-evidence.js.map +1 -1
  79. package/dist/metadata/sql-context-validation.d.ts +3 -0
  80. package/dist/metadata/sql-context-validation.d.ts.map +1 -1
  81. package/dist/metadata/sql-context-validation.js +16 -5
  82. package/dist/metadata/sql-context-validation.js.map +1 -1
  83. package/dist/plan-execution-adapter.d.ts +9 -1
  84. package/dist/plan-execution-adapter.d.ts.map +1 -1
  85. package/dist/plan-execution-adapter.js +81 -32
  86. package/dist/plan-execution-adapter.js.map +1 -1
  87. package/dist/provider-egress.d.ts +122 -0
  88. package/dist/provider-egress.d.ts.map +1 -0
  89. package/dist/provider-egress.js +504 -0
  90. package/dist/provider-egress.js.map +1 -0
  91. package/dist/providers/claude.d.ts.map +1 -1
  92. package/dist/providers/claude.js +76 -33
  93. package/dist/providers/claude.js.map +1 -1
  94. package/dist/providers/dispatch.d.ts +15 -0
  95. package/dist/providers/dispatch.d.ts.map +1 -0
  96. package/dist/providers/dispatch.js +25 -0
  97. package/dist/providers/dispatch.js.map +1 -0
  98. package/dist/providers/gemini.d.ts.map +1 -1
  99. package/dist/providers/gemini.js +16 -8
  100. package/dist/providers/gemini.js.map +1 -1
  101. package/dist/providers/index.d.ts +2 -1
  102. package/dist/providers/index.d.ts.map +1 -1
  103. package/dist/providers/index.js +1 -0
  104. package/dist/providers/index.js.map +1 -1
  105. package/dist/providers/ollama.d.ts.map +1 -1
  106. package/dist/providers/ollama.js +29 -10
  107. package/dist/providers/ollama.js.map +1 -1
  108. package/dist/providers/openai.d.ts.map +1 -1
  109. package/dist/providers/openai.js +92 -36
  110. package/dist/providers/openai.js.map +1 -1
  111. package/dist/providers/types.d.ts +43 -0
  112. package/dist/providers/types.d.ts.map +1 -1
  113. package/dist/providers/types.js.map +1 -1
  114. package/dist/relationship-proof.d.ts +19 -0
  115. package/dist/relationship-proof.d.ts.map +1 -0
  116. package/dist/relationship-proof.js +75 -0
  117. package/dist/relationship-proof.js.map +1 -0
  118. package/dist/research-loop.d.ts.map +1 -1
  119. package/dist/research-loop.js +17 -11
  120. package/dist/research-loop.js.map +1 -1
  121. package/dist/resolved-analytical-plan.d.ts +25 -3
  122. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  123. package/dist/resolved-analytical-plan.js +460 -31
  124. package/dist/resolved-analytical-plan.js.map +1 -1
  125. package/dist/router.d.ts +38 -1
  126. package/dist/router.d.ts.map +1 -1
  127. package/dist/router.js +1060 -53
  128. package/dist/router.js.map +1 -1
  129. package/dist/semantic-identities.d.ts +18 -0
  130. package/dist/semantic-identities.d.ts.map +1 -0
  131. package/dist/semantic-identities.js +60 -0
  132. package/dist/semantic-identities.js.map +1 -0
  133. package/dist/semantic-proof-authority.d.ts +29 -0
  134. package/dist/semantic-proof-authority.d.ts.map +1 -0
  135. package/dist/semantic-proof-authority.js +34 -0
  136. package/dist/semantic-proof-authority.js.map +1 -0
  137. package/package.json +4 -4
@@ -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":"AAAA,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAGV,sBAAsB,EACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG,UAAU,GAAG,KAAK,CAAC;AACrE,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG,UAAU,GAAG,cAAc,GAAG,aAAa,CAAC;AAE9F,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,qBAAqB,CAAC;IACrC,SAAS,EAAE,qBAAqB,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,aAAa,EAAE;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,UAAU,EAAE,MAAM,EAAE,CAAC;QACrB,UAAU,EAAE,MAAM,EAAE,CAAC;QACrB,UAAU,EAAE,MAAM,EAAE,CAAC;QACrB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,eAAe,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC;IACF,kFAAkF;IAClF,oBAAoB,CAAC,EAAE,wBAAwB,CAAC;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,8BAA8B;IAC7C,UAAU,EAAE,wBAAwB,EAAE,CAAC;IACvC,eAAe,EAAE,MAAM,CAAC,qBAAqB,EAAE,wBAAwB,EAAE,CAAC,CAAC;IAC3E,eAAe,EAAE,KAAK,CAAC;QAAE,YAAY,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpE;AAED,MAAM,WAAW,oCAAoC;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,wBAAwB,EAAE,CAAC;IAChD,+EAA+E;IAC/E,cAAc,CAAC,EAAE,cAAc,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,6BAA6B;IAC5C,GAAG,EAAE,cAAc,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB;AASD;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,oBAAoB,EAClC,MAAM,EAAE,6BAA6B,EAAE,GACtC,8BAA8B,CAsGhC;AAgFD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,8BAA8B,EACxC,YAAY,EAAE,oBAAoB,EAClC,OAAO,GAAE,oCAAyC,GACjD,sBAAsB,CA4ExB;AA0ZD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,sBAAsB,EAChC,IAAI,EAAE,gBAAgB,EACtB,kBAAkB,CAAC,EAAE,MAAM,GAC1B,sBAAsB,CAuDxB"}
@@ -40,6 +40,10 @@ export function buildMeaningEvidencePackage(question, questionPlan, ranked) {
40
40
  eligible.sort((left, right) => right.relevanceScore - left.relevanceScore
41
41
  || left.item.rank - right.item.rank
42
42
  || left.item.row.objectKey.localeCompare(right.item.row.objectKey));
43
+ const requiredSemanticMetricKey = questionPlan.requestedShape.measures.length > 0
44
+ ? eligible.find((candidate) => candidate.evidenceClass === 'semantic'
45
+ && candidate.item.row.objectType === 'semantic_metric')?.item.row.objectKey
46
+ : undefined;
43
47
  const selectedByClass = {
44
48
  certified: [], semantic: [], sql: [],
45
49
  };
@@ -47,6 +51,15 @@ export function buildMeaningEvidencePackage(question, questionPlan, ranked) {
47
51
  for (const candidate of eligible) {
48
52
  const lane = selectedByClass[candidate.evidenceClass];
49
53
  if (candidate.evidenceClass === 'semantic') {
54
+ // Dimensions and metrics share the semantic trust class. Preserve one
55
+ // retrieved metric for a measure-bearing question before member matches
56
+ // consume the bounded lane; deterministic compatibility still decides
57
+ // whether that metric is executable.
58
+ if (requiredSemanticMetricKey
59
+ && candidate.item.row.objectKey !== requiredSemanticMetricKey
60
+ && lane.length >= MAX_CANDIDATES_PER_CLASS - 1
61
+ && !lane.some((selectedCandidate) => selectedCandidate.item.row.objectKey === requiredSemanticMetricKey))
62
+ continue;
50
63
  const definitionKey = normalizeText(candidate.item.row.description ?? '');
51
64
  const meaningKey = definitionKey ? `${candidate.item.row.domain ?? ''}|${definitionKey}` : '';
52
65
  const existingIndex = meaningKey ? semanticMeaningIndexes.get(meaningKey) : undefined;
@@ -130,8 +143,28 @@ function attachRelationshipEvidence(candidates, contextObjects) {
130
143
  if (identitiesByEntity.size === 0)
131
144
  return;
132
145
  for (const candidate of candidates) {
146
+ for (const dimension of candidate.analyticalCapability?.dimensions ?? []) {
147
+ 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);
152
+ }
153
+ }
154
+ if (pathRelationships.size > 0)
155
+ dimension.relationshipPathIds = [...pathRelationships].sort();
156
+ }
133
157
  const touched = new Set();
134
- for (const reference of [candidate.primaryEntity, ...(candidate.sourceObjects ?? [])]) {
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
+ ]) {
135
168
  if (!reference)
136
169
  continue;
137
170
  for (const alias of entityAliasesForRelationshipMatch(reference)) {
@@ -150,10 +183,74 @@ function entityAliasesForRelationshipMatch(reference) {
150
183
  }
151
184
  export function toAgentRetrievalEvidence(evidence, questionPlan, options = {}) {
152
185
  const maxRelevance = Math.max(1, ...evidence.candidates.map((candidate) => candidate.relevanceScore));
153
- const candidates = evidence.candidates.map((candidate) => ({
186
+ const candidates = evidence.candidates.map((candidate) => agentCandidateFromMeaning(candidate, maxRelevance));
187
+ const trustedBindings = trustedMemberBindingEvidence(questionPlan, candidates, options.contextObjects ?? []);
188
+ for (const member of trustedBindings.candidates) {
189
+ if (!candidates.some((candidate) => candidate.id === member.id))
190
+ candidates.push(member);
191
+ }
192
+ const clarificationCandidates = supplementalClarificationEvidence(questionPlan, options.contextObjects ?? [], candidates);
193
+ attachRelationshipEvidence(candidates, options.contextObjects ?? []);
194
+ attachRelationshipEvidence(clarificationCandidates, options.contextObjects ?? []);
195
+ // A member the reader named that survived NO grounding lane. Live value
196
+ // lookup is opt-in per project (`agent.runtimeValueGrounding`), so with it
197
+ // disabled "What customer type is Wesley Jenkins?" produced zero grounded
198
+ // filters and the run quietly returned every customer instead of saying it
199
+ // could not bind him.
200
+ const groundedFilters = [
201
+ ...trustedBindings.filters,
202
+ ...groundedMemberFilters(questionPlan, candidates, options.contextObjects ?? []),
203
+ ].filter((filter, index, all) => all.findIndex((other) => other.field === filter.field && other.value === filter.value) === index);
204
+ const groundedValues = new Set(groundedFilters.map((filter) => normalizeText(String(filter.value))));
205
+ const unboundCandidates = uniqueStrings((questionPlan.requestedShape.filters ?? [])
206
+ .filter((term) => term.trim().length > 0 && !groundedValues.has(normalizeText(term))));
207
+ // The planner emits stemmed variants beside the literal ("Wesley Jenkins" and
208
+ // "wesley jenkin"). Naming both back to the reader reads like two different
209
+ // missing values, so keep only the longest form of each.
210
+ const unboundMemberTerms = unboundCandidates.filter((term) => {
211
+ const normalized = normalizeText(term);
212
+ return !unboundCandidates.some((other) => {
213
+ const otherNormalized = normalizeText(other);
214
+ return otherNormalized !== normalized
215
+ && otherNormalized.length > normalized.length
216
+ && otherNormalized.startsWith(normalized);
217
+ });
218
+ });
219
+ return {
220
+ snapshotId: options.snapshotId,
221
+ sourceFingerprint: options.sourceFingerprint,
222
+ knowledgeLens: options.knowledgeLens,
223
+ analyticalPolicies: options.analyticalPolicies,
224
+ candidates,
225
+ ...(clarificationCandidates.length > 0 ? { clarificationCandidates } : {}),
226
+ ...(unboundMemberTerms.length > 0 ? { unboundMemberTerms } : {}),
227
+ parsedIntent: {
228
+ measures: questionPlan.requestedShape.measures,
229
+ dimensions: questionPlan.requestedShape.dimensions,
230
+ filters: groundedFilters,
231
+ ...(analysisTimeGrain(questionPlan)
232
+ ? { timeGrain: analysisTimeGrain(questionPlan) }
233
+ : {}),
234
+ ...(questionPlan.requestedShape.rankingDirection
235
+ ? { order: questionPlan.requestedShape.rankingDirection === 'top' ? 'desc' : 'asc' }
236
+ : {}),
237
+ ...(questionPlan.requestedShape.topN ? { limit: questionPlan.requestedShape.topN.n } : {}),
238
+ },
239
+ diagnostics: {
240
+ searchedKinds: [...new Set([...candidates, ...clarificationCandidates].map((candidate) => candidate.kind))],
241
+ durationMs: options.durationMs,
242
+ truncated: options.truncated ?? false,
243
+ },
244
+ };
245
+ }
246
+ function agentCandidateFromMeaning(candidate, maxRelevance = 1) {
247
+ return {
154
248
  id: candidate.objectKey,
155
249
  qualifiedId: candidate.qualifiedId,
156
250
  kind: agentEvidenceKind(candidate),
251
+ ...(semanticObjectType(candidate.objectType)
252
+ ? { semanticObjectType: semanticObjectType(candidate.objectType) }
253
+ : {}),
157
254
  trustTier: candidate.trustTier,
158
255
  name: candidate.name,
159
256
  aliases: candidate.aliases,
@@ -164,7 +261,7 @@ export function toAgentRetrievalEvidence(evidence, questionPlan, options = {}) {
164
261
  domain: candidate.domain,
165
262
  semanticModel: candidate.semanticModel,
166
263
  primaryEntity: candidate.businessShape.entities[0],
167
- dimensions: candidate.businessShape.dimensions,
264
+ dimensions: dimensionLookupAliases(candidate.businessShape.dimensions),
168
265
  timeGrains: candidate.businessShape.timeGrains,
169
266
  requiredParameters: candidate.businessShape.parameters,
170
267
  sourceObjects: candidate.businessShape.sourceRelations,
@@ -177,34 +274,261 @@ export function toAgentRetrievalEvidence(evidence, questionPlan, options = {}) {
177
274
  analyticalCapability: candidate.analyticalCapability,
178
275
  eligible: true,
179
276
  exactMatch: candidate.relevanceReasons.includes("exact name or alias"),
180
- }));
181
- attachRelationshipEvidence(candidates, options.contextObjects ?? []);
182
- const groundedFilters = groundedMemberFilters(questionPlan, candidates, options.contextObjects ?? []);
183
- return {
184
- snapshotId: options.snapshotId,
185
- sourceFingerprint: options.sourceFingerprint,
186
- knowledgeLens: options.knowledgeLens,
187
- analyticalPolicies: options.analyticalPolicies,
188
- candidates,
189
- parsedIntent: {
190
- measures: questionPlan.requestedShape.measures,
191
- dimensions: questionPlan.requestedShape.dimensions,
192
- filters: groundedFilters,
193
- ...(analysisTimeGrain(questionPlan)
194
- ? { timeGrain: analysisTimeGrain(questionPlan) }
195
- : {}),
196
- ...(questionPlan.requestedShape.rankingDirection
197
- ? { order: questionPlan.requestedShape.rankingDirection === 'top' ? 'desc' : 'asc' }
198
- : {}),
199
- ...(questionPlan.requestedShape.topN ? { limit: questionPlan.requestedShape.topN.n } : {}),
200
- },
201
- diagnostics: {
202
- searchedKinds: [...new Set(candidates.map((candidate) => candidate.kind))],
203
- durationMs: options.durationMs,
204
- truncated: options.truncated ?? false,
205
- },
206
277
  };
207
278
  }
279
+ function trustedMemberBindingEvidence(plan, candidates, objects) {
280
+ const dimensions = new Map();
281
+ for (const object of objects) {
282
+ if (object.objectType !== 'semantic_dimension')
283
+ continue;
284
+ const id = firstString(object.payload?.qualifiedId, object.fullName);
285
+ if (!id)
286
+ continue;
287
+ dimensions.set(normalizeText(object.name), { id, definition: object.description });
288
+ dimensions.set(normalizeDimensionRole(object.name), { id, definition: object.description });
289
+ }
290
+ for (const candidate of candidates) {
291
+ const capability = normalizeMetricCapabilityContract(candidate.analyticalCapability);
292
+ for (const dimension of capability?.dimensions ?? []) {
293
+ const value = { id: dimension.dimensionId, entityId: dimension.entityId };
294
+ dimensions.set(normalizeText(dimension.dimensionId), value);
295
+ dimensions.set(normalizeDimensionRole(dimension.dimensionId), value);
296
+ }
297
+ for (const dimensionId of candidate.dimensions ?? []) {
298
+ if (!/[:./]/.test(dimensionId))
299
+ continue;
300
+ const value = { id: dimensionId, entityId: normalizeDimensionRole(dimensionId) };
301
+ dimensions.set(normalizeText(dimensionId), value);
302
+ dimensions.set(normalizeDimensionRole(dimensionId), value);
303
+ }
304
+ }
305
+ const cards = [];
306
+ const filters = [];
307
+ for (const binding of plan.requestedShape.memberBindings ?? []) {
308
+ if (binding.source !== 'prior_result' && binding.source !== 'clarification')
309
+ continue;
310
+ // A deictic binding used to be discarded outright, which threw away the
311
+ // referent of every "this customer" follow-up and left the turn ungrounded.
312
+ // Cardinality is what matters: exactly one candidate is unambiguous and
313
+ // safe to bind; several must not be fanned into an OR'd filter, and are
314
+ // surfaced as a clarification instead.
315
+ if (binding.confidence === 'deictic' && binding.values.length !== 1)
316
+ continue;
317
+ const dimension = dimensions.get(normalizeText(binding.dimension))
318
+ ?? dimensions.get(normalizeDimensionRole(binding.dimension));
319
+ if (!dimension)
320
+ continue;
321
+ for (const value of binding.values) {
322
+ filters.push({ field: dimension.id, value });
323
+ const id = `semantic:member:${encodeURIComponent(dimension.id)}:${encodeURIComponent(normalizeText(value))}`;
324
+ cards.push({
325
+ id,
326
+ qualifiedId: id,
327
+ kind: 'semantic_member',
328
+ trustTier: 'semantic',
329
+ name: value,
330
+ aliases: [value],
331
+ definition: `Trusted ${binding.source.replace('_', ' ')} member for ${dimension.id}.`,
332
+ primaryEntity: dimension.entityId,
333
+ dimensions: [dimension.id],
334
+ relevanceScore: 1,
335
+ matchReasons: [`typed ${binding.source} member binding`],
336
+ compatibility: 'compatible',
337
+ eligible: true,
338
+ exactMatch: true,
339
+ });
340
+ }
341
+ }
342
+ return { candidates: cards, filters };
343
+ }
344
+ function supplementalClarificationEvidence(plan, objects, existing) {
345
+ const MAX_SUPPLEMENTAL_CANDIDATES = 12;
346
+ const MAX_PER_REQUESTED_ROLE = 3;
347
+ const existingIds = new Set(existing.map((candidate) => candidate.id));
348
+ const requestedMeasures = expandedRequestedTerms(plan.requestedShape.measures, objects);
349
+ const requestedDimensions = expandedRequestedTerms(plan.requestedShape.dimensions, objects);
350
+ const objectCards = objects.flatMap((object) => {
351
+ if (existingIds.has(object.objectKey))
352
+ return [];
353
+ if (!['semantic_metric', 'semantic_measure', 'semantic_dimension'].includes(object.objectType))
354
+ return [];
355
+ const names = aliasesFor(object).map(normalizeText);
356
+ const role = normalizeDimensionRole(object.name);
357
+ const relevant = object.objectType === 'semantic_metric' || object.objectType === 'semantic_measure'
358
+ ? requestedMeasures.some((term) => names.some((name) => phraseTermsMatch(term, name)))
359
+ : requestedDimensions.some((term) => names.some((name) => phraseTermsMatch(term, name))
360
+ || (isGeographicRole(term) && isGeographicRole(role)));
361
+ if (!relevant)
362
+ return [];
363
+ const card = candidateCard(object, 'semantic', 1, 1, aliasesFor(object), ['requested clarification capability'], []);
364
+ return [agentCandidateFromMeaning(card)];
365
+ });
366
+ const declaredDimensionCards = existing.flatMap((candidate) => (candidate.dimensions ?? []).flatMap((dimensionId) => {
367
+ if (!/[:./]/.test(dimensionId) || existingIds.has(dimensionId))
368
+ return [];
369
+ const role = normalizeDimensionRole(dimensionId);
370
+ const relevant = requestedDimensions.some((term) => phraseTermsMatch(term, role) || (isGeographicRole(term) && isGeographicRole(role)));
371
+ if (!relevant)
372
+ return [];
373
+ return [{
374
+ id: dimensionId,
375
+ qualifiedId: dimensionId,
376
+ kind: 'semantic_member',
377
+ trustTier: 'semantic',
378
+ name: dimensionId.split(/[.:/]/).at(-1) ?? dimensionId,
379
+ aliases: [role],
380
+ definition: `Modeled ${role} dimension available through ${candidate.name}.`,
381
+ primaryEntity: candidate.primaryEntity,
382
+ dimensions: [dimensionId],
383
+ relevanceScore: candidate.relevanceScore,
384
+ matchReasons: ['requested clarification capability role'],
385
+ compatibility: 'partial',
386
+ eligible: true,
387
+ exactMatch: false,
388
+ }];
389
+ }));
390
+ // A bare ranking ("who are the top customers") names an ENTITY and no
391
+ // measure, so every lane above — each keyed on a REQUESTED measure or
392
+ // dimension term — returns nothing, and the clarification has no choices to
393
+ // offer. Surface the governed measures that can actually order that entity.
394
+ const rankingMeasureCards = (() => {
395
+ if (!plan.requestedShape.rankingDirection)
396
+ return [];
397
+ if (requestedMeasures.length > 0)
398
+ return [];
399
+ const entityTerms = requestedDimensions;
400
+ if (entityTerms.length === 0)
401
+ return [];
402
+ return objects.flatMap((object) => {
403
+ if (existingIds.has(object.objectKey))
404
+ return [];
405
+ if (!['semantic_metric', 'semantic_measure'].includes(object.objectType))
406
+ return [];
407
+ const card = agentCandidateFromMeaning(candidateCard(object, 'semantic', 1, 1, aliasesFor(object), ['ranking measure for the requested entity'], []));
408
+ // It must be reportable at the ranked entity's grain. `semanticModel`
409
+ // covers a measure that declares its model but no dimension list.
410
+ const grainTerms = [
411
+ card.primaryEntity ?? '',
412
+ card.semanticModel ?? '',
413
+ ...(card.dimensions ?? []),
414
+ ].map(normalizeText).filter(Boolean);
415
+ if (!entityTerms.some((term) => grainTerms.some((grain) => grainTermsMatch(term, grain))))
416
+ return [];
417
+ // A count of the ranked entity gives every row the same score.
418
+ const aggregation = normalizeText(card.aggregation ?? '');
419
+ const candidateNames = [card.name, ...(card.aliases ?? [])].map(normalizeText);
420
+ if (/count/.test(aggregation)
421
+ && entityTerms.some((term) => candidateNames.some((name) => grainTermsMatch(term, name))))
422
+ return [];
423
+ // The registry emits a bare alias node beside the entity-qualified metric
424
+ // (`semantic:metric:revenue` next to `semantic:metric:order_item.revenue`).
425
+ // The alias carries no aggregation, grain, or prose — only a raw dbt
426
+ // unique_id as its description — so it can never be proven compatible and
427
+ // must not take a choice slot from the node that can.
428
+ if (!card.aggregation)
429
+ return [];
430
+ return [card];
431
+ }).sort((left, right) => right.relevanceScore - left.relevanceScore || left.id.localeCompare(right.id));
432
+ })();
433
+ const unique = [...objectCards, ...declaredDimensionCards, ...rankingMeasureCards]
434
+ .filter((candidate, index, all) => all.findIndex((other) => (other.qualifiedId ?? other.id) === (candidate.qualifiedId ?? candidate.id)) === index);
435
+ const roleLanes = [
436
+ ...requestedDimensions.map((term) => ({ kind: 'dimension', term })),
437
+ ...requestedMeasures.map((term) => ({ kind: 'metric', term })),
438
+ ];
439
+ const selected = [];
440
+ // The ranking lane has no requested term to match on — that absence is the
441
+ // whole reason it exists — so it seeds the selection directly.
442
+ for (const candidate of rankingMeasureCards.slice(0, MAX_PER_REQUESTED_ROLE)) {
443
+ if (!selected.some((item) => (item.qualifiedId ?? item.id) === (candidate.qualifiedId ?? candidate.id))) {
444
+ selected.push(candidate);
445
+ }
446
+ }
447
+ for (const lane of roleLanes) {
448
+ const matches = unique.filter((candidate) => {
449
+ const candidateTerms = [candidate.name, ...(candidate.aliases ?? [])].map(normalizeText);
450
+ if (lane.kind === 'metric') {
451
+ return candidate.kind === 'semantic_metric'
452
+ && candidateTerms.some((term) => phraseTermsMatch(lane.term, term));
453
+ }
454
+ if (candidate.kind !== 'semantic_member')
455
+ return false;
456
+ return candidateTerms.some((term) => phraseTermsMatch(lane.term, term))
457
+ || (isGeographicRole(lane.term) && candidateTerms.some(isGeographicRole));
458
+ }).sort((left, right) => right.relevanceScore - left.relevanceScore || left.id.localeCompare(right.id));
459
+ for (const candidate of matches.slice(0, MAX_PER_REQUESTED_ROLE)) {
460
+ if (!selected.some((item) => (item.qualifiedId ?? item.id) === (candidate.qualifiedId ?? candidate.id))) {
461
+ selected.push(candidate);
462
+ }
463
+ }
464
+ }
465
+ return selected.slice(0, MAX_SUPPLEMENTAL_CANDIDATES);
466
+ }
467
+ function expandedRequestedTerms(terms, objects) {
468
+ const vocabulary = new Map();
469
+ for (const object of objects) {
470
+ if (object.objectType !== 'dql_term')
471
+ continue;
472
+ const words = [object.name, ...stringArray(object.payload?.synonyms)].map(normalizeText).filter(Boolean);
473
+ for (const word of words) {
474
+ const peers = vocabulary.get(word) ?? new Set();
475
+ for (const peer of words)
476
+ peers.add(peer);
477
+ vocabulary.set(word, peers);
478
+ }
479
+ }
480
+ const expanded = new Set();
481
+ for (const term of terms.map(normalizeText)) {
482
+ expanded.add(term);
483
+ const tokens = term.split(' ');
484
+ tokens.forEach((token, index) => {
485
+ for (const peer of vocabulary.get(token) ?? []) {
486
+ expanded.add(tokens.map((value, tokenIndex) => tokenIndex === index ? peer : value).join(' '));
487
+ }
488
+ });
489
+ }
490
+ return [...expanded];
491
+ }
492
+ function phraseTermsMatch(left, right) {
493
+ return left === right || left.endsWith(` ${right}`) || right.endsWith(` ${left}`);
494
+ }
495
+ /**
496
+ * Grain comparison for the ranking-measure lane only.
497
+ *
498
+ * A measure frequently declares the MODEL it lives on ("customers") where the
499
+ * question names the ENTITY ("customer"), and a plain term match misses that by
500
+ * one character. This stays local to grain matching rather than loosening
501
+ * `phraseTermsMatch`, which decides identity elsewhere.
502
+ */
503
+ function grainTermsMatch(entityTerm, grainTerm) {
504
+ if (phraseTermsMatch(entityTerm, grainTerm))
505
+ return true;
506
+ const singular = (value) => value.replace(/ies$/, 'y').replace(/s$/, '');
507
+ return phraseTermsMatch(singular(entityTerm), singular(grainTerm));
508
+ }
509
+ function normalizeDimensionRole(value) {
510
+ const leaf = value.split(/[.:/]/).at(-1) ?? value;
511
+ const normalized = normalizeText(leaf);
512
+ if (/^customer(?: name| id| key| label)?$/.test(normalized))
513
+ return 'customer';
514
+ if (/^location(?: name| id| key| label)?$/.test(normalized))
515
+ return 'location';
516
+ if (/^region(?: name| id| key| label)?$/.test(normalized))
517
+ return 'region';
518
+ if (/^product(?: name| id| key| label)?$/.test(normalized))
519
+ return 'product';
520
+ return normalized;
521
+ }
522
+ function dimensionLookupAliases(dimensions) {
523
+ return uniqueStrings(dimensions.flatMap((dimension) => {
524
+ const leaf = dimension.split(/[.:/]/).at(-1) ?? dimension;
525
+ const role = leaf.replace(/(?:_name|_id|_key|_label)$/i, '');
526
+ return role && role !== leaf ? [dimension, role] : [dimension];
527
+ }));
528
+ }
529
+ function isGeographicRole(value) {
530
+ return /^(?:region|location|geography|territory|market|site|store)$/.test(normalizeDimensionRole(value));
531
+ }
208
532
  /**
209
533
  * Bind current-turn values only when an authorized runtime-value observation
210
534
  * and a semantic dimension's exact physical table/column identity agree. Text
@@ -303,16 +627,28 @@ function normalizeIdentifier(value) {
303
627
  */
304
628
  export function applyContextPackCompatibility(evidence, pack, selectedEvidenceId) {
305
629
  const certifiedFits = new Map(pack.retrievalDiagnostics.certifiedCandidateFits.map((fit) => [fit.objectKey, fit]));
306
- const semanticEvidence = new Set(pack.routeDecision.selectedEvidence
307
- .filter((item) => item.role === 'semantic_metric')
308
- .map((item) => item.objectKey));
309
630
  return {
310
631
  ...evidence,
311
632
  candidates: evidence.candidates.map((candidate) => {
312
633
  if (candidate.kind === 'certified_block') {
313
634
  const fit = certifiedFits.get(candidate.id);
635
+ const unrequestedStaticScope = fit?.fit.reasons.some((reason) => reason.startsWith('certified static scope is not requested:')) ?? false;
636
+ const exactAuthoredExample = fit?.action === 'certified_answer'
637
+ && pack.routeDecision.exactObjectKey === candidate.id
638
+ && fit.fit.reasons.includes('question matches a certified example');
314
639
  return {
315
640
  ...candidate,
641
+ // Lexical equality to a block name/alias is useful retrieval signal,
642
+ // but it is not exact analytical authority. Only the selected block's
643
+ // authored exact-example fit may grant the router shortcut.
644
+ exactMatch: exactAuthoredExample,
645
+ ...(exactAuthoredExample
646
+ ? { analyticalFitClass: 'exact' }
647
+ : { analyticalFitClass: undefined }),
648
+ // A statically scoped certified artifact is not an alternative for a
649
+ // broader question. Exclude it before both meaning resolution and
650
+ // identifier-bound clarification; later fit rejection is too late.
651
+ eligible: unrequestedStaticScope ? false : candidate.eligible,
316
652
  compatibility: fit?.action === 'certified_answer'
317
653
  ? 'compatible'
318
654
  : fit?.action === 'rejected_for_fit'
@@ -321,7 +657,7 @@ export function applyContextPackCompatibility(evidence, pack, selectedEvidenceId
321
657
  };
322
658
  }
323
659
  if ((candidate.kind === 'semantic_metric' || candidate.kind === 'semantic_member')
324
- && (semanticEvidence.has(candidate.id) || selectedEvidenceId === candidate.id)
660
+ && (Boolean(candidate.analyticalCapability) || selectedEvidenceId === candidate.id)
325
661
  && (selectedEvidenceId === candidate.id
326
662
  || (pack.routeDecision.route !== 'clarify' && pack.routeDecision.route !== 'conflict'))) {
327
663
  const requestedDimensions = pack.questionPlan.requestedShape.dimensions.map((dimension) => normalizeText(dimension));
@@ -330,7 +666,10 @@ export function applyContextPackCompatibility(evidence, pack, selectedEvidenceId
330
666
  const requestedTimeGrain = analysisTimeGrain(pack.questionPlan);
331
667
  const availableTimeGrains = (candidate.timeGrains ?? []).map((grain) => grain.toLowerCase());
332
668
  const timeGrainFits = !requestedTimeGrain || availableTimeGrains.includes(requestedTimeGrain);
333
- return { ...candidate, compatibility: dimensionsFit && timeGrainFits ? 'compatible' : 'partial' };
669
+ const requestedMeasures = pack.questionPlan.requestedShape.measures.map((measure) => normalizeText(measure));
670
+ const metricPhrases = [candidate.name, ...(candidate.aliases ?? [])].map((value) => normalizeText(value));
671
+ const measuresFit = requestedMeasures.length === 0 || requestedMeasures.every((measure) => metricPhrases.some((phrase) => phrase === measure || phrase.includes(measure)));
672
+ return { ...candidate, compatibility: dimensionsFit && timeGrainFits && measuresFit ? 'compatible' : 'partial' };
334
673
  }
335
674
  if (candidate.trustTier === 'governed_sql')
336
675
  return { ...candidate, compatibility: 'partial' };
@@ -450,6 +789,21 @@ function agentEvidenceKind(candidate) {
450
789
  return 'sql_column';
451
790
  return 'sql_table';
452
791
  }
792
+ function semanticObjectType(objectType) {
793
+ if (objectType === 'semantic_metric')
794
+ return 'metric';
795
+ if (objectType === 'semantic_measure')
796
+ return 'measure';
797
+ if (objectType === 'semantic_dimension')
798
+ return 'dimension';
799
+ if (objectType === 'semantic_entity')
800
+ return 'entity';
801
+ if (objectType === 'semantic_model')
802
+ return 'model';
803
+ if (objectType === 'semantic_saved_query')
804
+ return 'saved_query';
805
+ return undefined;
806
+ }
453
807
  function aliasesFor(object) {
454
808
  const payload = object.payload ?? {};
455
809
  return uniqueStrings([
@@ -467,7 +821,7 @@ function lexicalRelevance(normalizedQuestion, questionTokens, object, aliases) {
467
821
  const normalizedAliases = aliases.map(normalizeText).filter(Boolean);
468
822
  const reasons = [];
469
823
  let score = 0;
470
- if (normalizedAliases.some((alias) => alias === normalizedQuestion)) {
824
+ if (normalizedAliases.some((alias) => isExactAnalyticalPhrase(normalizedQuestion, alias))) {
471
825
  score += 80;
472
826
  reasons.push('exact name or alias');
473
827
  }
@@ -525,7 +879,25 @@ function addPeer(peers, key, peer) {
525
879
  peers.set(key, values);
526
880
  }
527
881
  function normalizeText(value) {
528
- return value.toLowerCase().replace(/[_./:-]+/g, ' ').replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
882
+ return value.toLowerCase()
883
+ .replace(/%/g, ' percentage ')
884
+ .replace(/[_./:-]+/g, ' ')
885
+ .replace(/[^a-z0-9 ]+/g, ' ')
886
+ .replace(/\s+/g, ' ')
887
+ .trim();
888
+ }
889
+ function isExactAnalyticalPhrase(question, alias) {
890
+ if (!alias)
891
+ return false;
892
+ if (alias === question)
893
+ return true;
894
+ const index = question.indexOf(alias);
895
+ if (index < 0)
896
+ return false;
897
+ const remaining = `${question.slice(0, index)} ${question.slice(index + alias.length)}`
898
+ .replace(/\s+/g, ' ')
899
+ .trim();
900
+ return remaining.length === 0 || remaining.split(' ').every((token) => ['a', 'an', 'are', 'can', 'could', 'do', 'does', 'for', 'give', 'is', 'me', 'of', 'please', 'show', 'tell', 'the', 'what', 'which'].includes(token));
529
901
  }
530
902
  function tokenSet(value) {
531
903
  return new Set(value.split(/\s+/).filter((token) => token.length >= 2));