@duckcodeailabs/dql-agent 1.14.2 → 1.14.4

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