@duckcodeailabs/dql-agent 1.14.2 → 1.14.3-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-run-engine.d.ts +63 -2
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +864 -29
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-planner.d.ts +6 -0
- package/dist/agent-run-planner.d.ts.map +1 -1
- package/dist/agent-run-planner.js +2 -0
- package/dist/agent-run-planner.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +48 -3
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +7 -1
- package/dist/agentic/analyst-loop.d.ts.map +1 -1
- package/dist/agentic/analyst-loop.js +163 -0
- package/dist/agentic/analyst-loop.js.map +1 -1
- package/dist/agentic/assumptions.d.ts +1 -1
- package/dist/agentic/assumptions.d.ts.map +1 -1
- package/dist/agentic/assumptions.js.map +1 -1
- package/dist/agentic/research-agent.d.ts.map +1 -1
- package/dist/agentic/research-agent.js +10 -1
- package/dist/agentic/research-agent.js.map +1 -1
- package/dist/agentic/sql-authorization.d.ts +8 -0
- package/dist/agentic/sql-authorization.d.ts.map +1 -1
- package/dist/agentic/sql-authorization.js +3 -0
- package/dist/agentic/sql-authorization.js.map +1 -1
- package/dist/analytical-compatibility.d.ts.map +1 -1
- package/dist/analytical-compatibility.js +8 -1
- package/dist/analytical-compatibility.js.map +1 -1
- package/dist/analytical-failure-repair.d.ts.map +1 -1
- package/dist/analytical-failure-repair.js +6 -2
- package/dist/analytical-failure-repair.js.map +1 -1
- package/dist/analytical-frame.d.ts +23 -1
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +322 -16
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +314 -3
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +486 -43
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +64 -8
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +528 -67
- package/dist/answer-loop.js.map +1 -1
- package/dist/ask-observability/index.d.ts +11 -0
- package/dist/ask-observability/index.d.ts.map +1 -0
- package/dist/ask-observability/index.js +7 -0
- package/dist/ask-observability/index.js.map +1 -0
- package/dist/ask-observability/instrumentation.d.ts +35 -0
- package/dist/ask-observability/instrumentation.d.ts.map +1 -0
- package/dist/ask-observability/instrumentation.js +472 -0
- package/dist/ask-observability/instrumentation.js.map +1 -0
- package/dist/ask-observability/observer.d.ts +62 -0
- package/dist/ask-observability/observer.d.ts.map +1 -0
- package/dist/ask-observability/observer.js +537 -0
- package/dist/ask-observability/observer.js.map +1 -0
- package/dist/ask-observability/portable.d.ts +98 -0
- package/dist/ask-observability/portable.d.ts.map +1 -0
- package/dist/ask-observability/portable.js +527 -0
- package/dist/ask-observability/portable.js.map +1 -0
- package/dist/ask-observability/store.d.ts +130 -0
- package/dist/ask-observability/store.d.ts.map +1 -0
- package/dist/ask-observability/store.js +1114 -0
- package/dist/ask-observability/store.js.map +1 -0
- package/dist/ask-observability/types.d.ts +357 -0
- package/dist/ask-observability/types.d.ts.map +1 -0
- package/dist/ask-observability/types.js +2 -0
- package/dist/ask-observability/types.js.map +1 -0
- package/dist/ask-observability/utils.d.ts +21 -0
- package/dist/ask-observability/utils.d.ts.map +1 -0
- package/dist/ask-observability/utils.js +102 -0
- package/dist/ask-observability/utils.js.map +1 -0
- package/dist/generated-analytical-proposal.d.ts +37 -1
- package/dist/generated-analytical-proposal.d.ts.map +1 -1
- package/dist/generated-analytical-proposal.js +211 -3
- package/dist/generated-analytical-proposal.js.map +1 -1
- package/dist/governed-relational-compiler.d.ts +15 -0
- package/dist/governed-relational-compiler.d.ts.map +1 -1
- package/dist/governed-relational-compiler.js +99 -1
- package/dist/governed-relational-compiler.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +10 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/meaning-resolution.d.ts +78 -6
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +331 -38
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +21 -1
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +1 -1
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +9 -2
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +261 -11
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/plan-execution-adapter.d.ts +9 -1
- package/dist/plan-execution-adapter.d.ts.map +1 -1
- package/dist/plan-execution-adapter.js +93 -13
- package/dist/plan-execution-adapter.js.map +1 -1
- package/dist/provider-egress.d.ts +24 -10
- package/dist/provider-egress.d.ts.map +1 -1
- package/dist/provider-egress.js +22 -17
- package/dist/provider-egress.js.map +1 -1
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +10 -9
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/dispatch.d.ts +27 -1
- package/dist/providers/dispatch.d.ts.map +1 -1
- package/dist/providers/dispatch.js +63 -1
- package/dist/providers/dispatch.js.map +1 -1
- package/dist/providers/gemini.js +10 -10
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.js +15 -15
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.js +22 -22
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +52 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/relationship-proof.d.ts +81 -0
- package/dist/relationship-proof.d.ts.map +1 -1
- package/dist/relationship-proof.js +228 -4
- package/dist/relationship-proof.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +18 -4
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +764 -47
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +10 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1785 -213
- package/dist/router.js.map +1 -1
- package/dist/semantic-bridge/compose.d.ts.map +1 -1
- package/dist/semantic-bridge/compose.js +23 -4
- package/dist/semantic-bridge/compose.js.map +1 -1
- package/package.json +4 -4
package/dist/router.js
CHANGED
|
@@ -16,12 +16,13 @@ import { assumeDominantCandidate } from './agentic/assumptions.js';
|
|
|
16
16
|
* question never pays twice.
|
|
17
17
|
*/
|
|
18
18
|
import { classifyConversationalTurn, decideAgentAction, } from "./intent-controller.js";
|
|
19
|
-
import { buildMeaningEvidencePackage, canonicalizeMetricMeasureCandidates, certifiedCandidateExplicitlyCoversMeasures, defaultQueryIntent, findExplicitEvidenceReference, questionTypeFromText, routeForEvidenceCandidate, validateMeaningResolution, } from "./meaning-resolution.js";
|
|
19
|
+
import { buildMeaningEvidencePackage, canonicalizeMetricMeasureCandidates, certifiedCandidateExplicitlyCoversMeasures, defaultQueryIntent, findExplicitEvidenceReference, mergeMeaningResolutionWithRequirementSeed, questionTypeFromText, routeForEvidenceCandidate, validateMeaningResolution, } from "./meaning-resolution.js";
|
|
20
20
|
import { normalizeAnalyticalQuestionFrameV2 } from "@duckcodeailabs/dql-core";
|
|
21
21
|
import { buildResolvedAnalyticalPlan, } from "./resolved-analytical-plan.js";
|
|
22
|
-
import { buildAnalyticalCascadeDecision, buildAnalyticalRequirementSet, } from './analytical-orchestration.js';
|
|
22
|
+
import { buildAnalyticalCascadeDecision, buildAnalyticalRequirementSeedV1, buildAnalyticalRequirementSet, categoricalDimensionRequirementTerms, candidateConflictsWithExplicitRankingMeasure, candidateMatchesCategoricalDimensionRequirement, evidenceCandidateRoles, isEntityAttributeCandidate, } from './analytical-orchestration.js';
|
|
23
|
+
import { askTraceObserverForV1 } from './ask-observability/index.js';
|
|
23
24
|
import { normalizeEvidenceAnalyticalCapability, solveAnalyticalCompatibility, } from "./analytical-compatibility.js";
|
|
24
|
-
import { buildDeterministicAnalyticalFrame, projectResolvedAnalyticalFrame } from "./analytical-frame.js";
|
|
25
|
+
import { buildDeterministicAnalyticalFrame, projectResolvedAnalyticalFrame, resolveMetricCapabilityDimension, } from "./analytical-frame.js";
|
|
25
26
|
import { conversationHistoryFromContext, renderConversationEnvelopeForPrompt, } from "./conversation/snapshot.js";
|
|
26
27
|
const DEFAULT_THRESHOLD = 0.7;
|
|
27
28
|
const DEFAULT_CACHE_SIZE = 200;
|
|
@@ -197,17 +198,19 @@ function parseClassification(raw) {
|
|
|
197
198
|
function buildMeaningSystemPrompt() {
|
|
198
199
|
return [
|
|
199
200
|
"You resolve business meaning for DQL, a governed analytics system.",
|
|
200
|
-
"The host already performed broad retrieval. Compare ONLY the supplied candidate cards.",
|
|
201
|
-
"Select the
|
|
202
|
-
"Trust
|
|
201
|
+
"The host already performed broad retrieval and supplied a host-owned requirement seed. Compare ONLY the supplied candidate cards against that seed.",
|
|
202
|
+
"Select the supplied candidate IDs that best bind the host-owned metric, entity, dimensions, filters, time grain, ranking, and output requirements.",
|
|
203
|
+
"Trust, route selection, SQL, and the final analytical frame are host-owned. Never select an unrelated certified block over a relevant semantic metric.",
|
|
203
204
|
"You may reference ONLY candidate IDs supplied below. Never invent an ID, table, column, metric, relationship, or filter value.",
|
|
205
|
+
"Do not rewrite, add, remove, or replace any host-owned requirement. Do not use prior conversation unless the host seed says it is a continuation.",
|
|
206
|
+
"Do not emit SQL, trust labels, an execution route, query intent, or an analytical frame. Those fields are ignored if present for legacy compatibility.",
|
|
204
207
|
"Use low confidence and recommend clarify when material business meanings remain unresolved.",
|
|
205
|
-
"Respond with ONLY one JSON object matching this shape:",
|
|
206
|
-
'{"
|
|
207
|
-
"
|
|
208
|
+
"Respond with ONLY one JSON object matching this minimal candidate-ID shape:",
|
|
209
|
+
'{"selectedCandidateIds":string[],"recommendedExecutionId"?:string,"confidence"?:"high"|"medium"|"low","missingInformation"?:string[]}',
|
|
210
|
+
"`selectedConceptIds` is accepted only for legacy compatibility. All omitted optional fields receive host-owned safe defaults.",
|
|
208
211
|
].join("\n");
|
|
209
212
|
}
|
|
210
|
-
function buildMeaningUserPrompt(request, evidence, candidates) {
|
|
213
|
+
function buildMeaningUserPrompt(request, evidence, candidates, requirementSeed) {
|
|
211
214
|
const cards = candidates.map((candidate) => ({
|
|
212
215
|
id: candidate.id,
|
|
213
216
|
kind: candidate.kind,
|
|
@@ -232,21 +235,23 @@ function buildMeaningUserPrompt(request, evidence, candidates) {
|
|
|
232
235
|
analyticalCapability: candidate.analyticalCapability,
|
|
233
236
|
}));
|
|
234
237
|
const lines = [
|
|
235
|
-
`
|
|
236
|
-
`Parsed request hints: ${JSON.stringify(compactQueryIntent(defaultQueryIntent(evidence)))}`,
|
|
238
|
+
`Host-owned requirement seed: ${JSON.stringify(requirementSeed)}`,
|
|
237
239
|
`Candidate cards: ${JSON.stringify(cards)}`,
|
|
238
240
|
];
|
|
239
241
|
const history = effectiveConversationHistory(request);
|
|
240
|
-
|
|
242
|
+
const continuation = request.conversationBinding ?? 'none';
|
|
243
|
+
if (history.length && continuation !== 'none') {
|
|
241
244
|
lines.push(`Recent conversation: ${JSON.stringify(history.slice(-4).map((turn) => ({
|
|
242
245
|
role: turn.role,
|
|
243
246
|
text: compactText(turn.text, 1_200),
|
|
244
247
|
})))}`);
|
|
245
248
|
}
|
|
246
|
-
const envelope =
|
|
249
|
+
const envelope = continuation === 'none'
|
|
250
|
+
? undefined
|
|
251
|
+
: renderConversationEnvelopeForPrompt(request.conversationContext);
|
|
247
252
|
if (envelope)
|
|
248
253
|
lines.push(`Structured conversation state: ${JSON.stringify(envelope)}`);
|
|
249
|
-
lines.push("
|
|
254
|
+
lines.push("Bind only supplied candidate IDs to the host-owned seed and return JSON only.");
|
|
250
255
|
return lines.join("\n");
|
|
251
256
|
}
|
|
252
257
|
function compactText(value, maxLength) {
|
|
@@ -275,42 +280,62 @@ function compactQueryIntent(intent) {
|
|
|
275
280
|
const QUESTION_TYPES = new Set(["definition", "value", "ranking", "trend", "comparison", "diagnosis", "research"]);
|
|
276
281
|
const MEANING_CONFIDENCES = new Set(["high", "medium", "low"]);
|
|
277
282
|
const MEANING_ROUTES = new Set(["certified", "semantic", "governed_sql", "exploratory", "clarify"]);
|
|
278
|
-
function parseMeaningResolution(raw) {
|
|
283
|
+
function parseMeaningResolution(raw, requirementSeed) {
|
|
279
284
|
const parsed = extractJsonObject(raw);
|
|
280
285
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
281
286
|
return undefined;
|
|
282
287
|
const record = parsed;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
288
|
+
const interpretedQuestion = typeof record.interpretedQuestion === "string" && record.interpretedQuestion.trim()
|
|
289
|
+
? record.interpretedQuestion.trim()
|
|
290
|
+
: requirementSeed?.sourceQuestion;
|
|
291
|
+
if (!interpretedQuestion)
|
|
286
292
|
return undefined;
|
|
287
|
-
|
|
293
|
+
const questionType = typeof record.questionType === "string" && QUESTION_TYPES.has(record.questionType)
|
|
294
|
+
? record.questionType
|
|
295
|
+
: requirementSeed ? questionTypeFromText(requirementSeed.sourceQuestion) : undefined;
|
|
296
|
+
if (!questionType)
|
|
288
297
|
return undefined;
|
|
289
|
-
|
|
298
|
+
const selectedConceptIds = Array.isArray(record.selectedCandidateIds)
|
|
299
|
+
? record.selectedCandidateIds
|
|
300
|
+
: record.selectedConceptIds;
|
|
301
|
+
if (!Array.isArray(selectedConceptIds) || selectedConceptIds.some((id) => typeof id !== "string"))
|
|
290
302
|
return undefined;
|
|
291
|
-
|
|
303
|
+
const confidence = typeof record.confidence === "string" && MEANING_CONFIDENCES.has(record.confidence)
|
|
304
|
+
? record.confidence
|
|
305
|
+
: requirementSeed
|
|
306
|
+
? selectedConceptIds.length > 0 ? 'medium' : 'low'
|
|
307
|
+
: undefined;
|
|
308
|
+
if (!confidence)
|
|
292
309
|
return undefined;
|
|
293
|
-
|
|
310
|
+
const recommendedRoute = typeof record.recommendedRoute === "string" && MEANING_ROUTES.has(record.recommendedRoute)
|
|
311
|
+
? record.recommendedRoute
|
|
312
|
+
: requirementSeed ? "clarify" : undefined;
|
|
313
|
+
if (!recommendedRoute)
|
|
294
314
|
return undefined;
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
const dimensions = stringArray(query.dimensions);
|
|
298
|
-
const missingInformation = stringArray(record.missingInformation);
|
|
299
|
-
if (!measures || !dimensions || !missingInformation || !Array.isArray(query.filters))
|
|
315
|
+
const missingInformation = stringArray(record.missingInformation) ?? (requirementSeed ? [] : undefined);
|
|
316
|
+
if (!missingInformation)
|
|
300
317
|
return undefined;
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
318
|
+
const query = record.queryIntent && typeof record.queryIntent === "object" && !Array.isArray(record.queryIntent)
|
|
319
|
+
? record.queryIntent
|
|
320
|
+
: undefined;
|
|
321
|
+
const measures = query ? stringArray(query.measures) : undefined;
|
|
322
|
+
const dimensions = query ? stringArray(query.dimensions) : undefined;
|
|
323
|
+
const filters = Array.isArray(query?.filters)
|
|
324
|
+
? query.filters.flatMap((filter) => {
|
|
325
|
+
if (!filter || typeof filter !== "object" || Array.isArray(filter))
|
|
326
|
+
return [];
|
|
327
|
+
const item = filter;
|
|
328
|
+
return typeof item.field === "string" && typeof item.value === "string"
|
|
329
|
+
? [{ field: item.field, value: item.value }]
|
|
330
|
+
: [];
|
|
331
|
+
})
|
|
332
|
+
: undefined;
|
|
333
|
+
if (!requirementSeed && (!measures || !dimensions || !filters || !Array.isArray(query?.filters) || filters.length !== query.filters.length))
|
|
310
334
|
return undefined;
|
|
311
|
-
|
|
335
|
+
const rejectedCandidatesRaw = record.rejectedCandidates;
|
|
336
|
+
if (rejectedCandidatesRaw !== undefined && !Array.isArray(rejectedCandidatesRaw))
|
|
312
337
|
return undefined;
|
|
313
|
-
const rejectedCandidates =
|
|
338
|
+
const rejectedCandidates = (rejectedCandidatesRaw ?? []).flatMap((candidate) => {
|
|
314
339
|
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
315
340
|
return [];
|
|
316
341
|
const item = candidate;
|
|
@@ -318,42 +343,66 @@ function parseMeaningResolution(raw) {
|
|
|
318
343
|
? [{ id: item.id, reason: item.reason }]
|
|
319
344
|
: [];
|
|
320
345
|
});
|
|
321
|
-
if (rejectedCandidates.length !==
|
|
346
|
+
if (rejectedCandidates.length !== (rejectedCandidatesRaw?.length ?? 0))
|
|
322
347
|
return undefined;
|
|
323
348
|
const recommendedExecutionId = typeof record.recommendedExecutionId === "string"
|
|
324
349
|
? record.recommendedExecutionId
|
|
325
350
|
: undefined;
|
|
326
|
-
const clarifyingQuestion = typeof record.clarifyingQuestion === "string" &&
|
|
327
|
-
record.clarifyingQuestion.trim()
|
|
351
|
+
const clarifyingQuestion = typeof record.clarifyingQuestion === "string" && record.clarifyingQuestion.trim()
|
|
328
352
|
? record.clarifyingQuestion.trim()
|
|
329
353
|
: undefined;
|
|
330
|
-
const
|
|
354
|
+
const providerExplicitlyClarified = (recommendedRoute === 'clarify'
|
|
355
|
+
&& typeof record.recommendedRoute === 'string')
|
|
356
|
+
|| Boolean(clarifyingQuestion)
|
|
357
|
+
|| missingInformation.length > 0
|
|
358
|
+
|| rejectedCandidates.length > 0;
|
|
359
|
+
// Legacy callers may still send a V2 frame. It remains readable without a
|
|
360
|
+
// seed, but a seeded meaning call must never grant a model frame authority.
|
|
361
|
+
const analyticalFrame = requirementSeed || record.analyticalFrame === undefined
|
|
331
362
|
? undefined
|
|
332
363
|
: normalizeAnalyticalQuestionFrameV2(record.analyticalFrame);
|
|
333
|
-
if (record.analyticalFrame !== undefined && !analyticalFrame)
|
|
364
|
+
if (!requirementSeed && record.analyticalFrame !== undefined && !analyticalFrame)
|
|
334
365
|
return undefined;
|
|
335
366
|
return {
|
|
336
|
-
interpretedQuestion
|
|
337
|
-
questionType
|
|
338
|
-
selectedConceptIds:
|
|
367
|
+
interpretedQuestion,
|
|
368
|
+
questionType,
|
|
369
|
+
selectedConceptIds: selectedConceptIds,
|
|
339
370
|
...(recommendedExecutionId ? { recommendedExecutionId } : {}),
|
|
340
|
-
queryIntent:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
? {
|
|
349
|
-
|
|
350
|
-
|
|
371
|
+
queryIntent: requirementSeed
|
|
372
|
+
? queryIntentFromRequirementSeed(requirementSeed)
|
|
373
|
+
: {
|
|
374
|
+
measures: measures,
|
|
375
|
+
dimensions: dimensions,
|
|
376
|
+
filters: filters,
|
|
377
|
+
...(typeof query?.timeRange === "string" ? { timeRange: query.timeRange } : {}),
|
|
378
|
+
...(typeof query?.timeGrain === "string" ? { timeGrain: query.timeGrain } : {}),
|
|
379
|
+
...(query?.order === "asc" || query?.order === "desc" ? { order: query.order } : {}),
|
|
380
|
+
...(typeof query?.limit === "number" && Number.isFinite(query.limit) && query.limit > 0
|
|
381
|
+
? { limit: Math.floor(query.limit) }
|
|
382
|
+
: {}),
|
|
383
|
+
},
|
|
351
384
|
rejectedCandidates,
|
|
352
|
-
confidence
|
|
385
|
+
confidence,
|
|
353
386
|
missingInformation,
|
|
354
|
-
recommendedRoute
|
|
387
|
+
recommendedRoute,
|
|
355
388
|
...(clarifyingQuestion ? { clarifyingQuestion } : {}),
|
|
356
389
|
...(analyticalFrame ? { analyticalFrame } : {}),
|
|
390
|
+
...(selectedConceptIds.length === 0 && !recommendedExecutionId && !providerExplicitlyClarified
|
|
391
|
+
? { emptyCandidateBinding: true }
|
|
392
|
+
: {}),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function queryIntentFromRequirementSeed(seed) {
|
|
396
|
+
return {
|
|
397
|
+
measures: [...seed.queryIntent.measures],
|
|
398
|
+
dimensions: [...seed.queryIntent.dimensions],
|
|
399
|
+
filters: seed.queryIntent.filters.map((filter) => ({ field: filter.field, value: filter.value })),
|
|
400
|
+
...(seed.queryIntent.timeRange ? { timeRange: seed.queryIntent.timeRange } : {}),
|
|
401
|
+
...(seed.queryIntent.timeGrain ? { timeGrain: seed.queryIntent.timeGrain } : {}),
|
|
402
|
+
...(seed.queryIntent.order ? { order: seed.queryIntent.order } : {}),
|
|
403
|
+
...(seed.queryIntent.limit !== undefined ? { limit: seed.queryIntent.limit } : {}),
|
|
404
|
+
...(seed.queryIntent.fiscalCalendarId ? { fiscalCalendarId: seed.queryIntent.fiscalCalendarId } : {}),
|
|
405
|
+
...(seed.queryIntent.fiscalDateRoleId ? { fiscalDateRoleId: seed.queryIntent.fiscalDateRoleId } : {}),
|
|
357
406
|
};
|
|
358
407
|
}
|
|
359
408
|
function stringArray(value) {
|
|
@@ -408,12 +457,204 @@ function conversationPositionToken(context) {
|
|
|
408
457
|
return `${latest}:${turns.length}:${recent}:${tailId}`;
|
|
409
458
|
}
|
|
410
459
|
function retrievalTrace(evidence, candidates) {
|
|
460
|
+
const candidateTraceMetadata = candidates.flatMap((candidate) => evidenceCandidateRoles(candidate).map((role) => ({
|
|
461
|
+
// `id` is the identity accepted by the meaning resolver. It is already
|
|
462
|
+
// source-qualified by the retrieval contract; keep it so later selection
|
|
463
|
+
// IDs can be joined without guessing a legacy-to-canonical mapping.
|
|
464
|
+
candidateId: candidate.id,
|
|
465
|
+
role,
|
|
466
|
+
source: traceSourceForCandidate(candidate),
|
|
467
|
+
...(candidate.retrievalLanes?.length ? { lanes: candidate.retrievalLanes } : {}),
|
|
468
|
+
})));
|
|
411
469
|
return {
|
|
412
470
|
...(evidence.snapshotId ? { snapshotId: evidence.snapshotId } : {}),
|
|
413
471
|
...(evidence.sourceFingerprint ? { sourceFingerprint: evidence.sourceFingerprint } : {}),
|
|
414
472
|
candidateCount: candidates.length,
|
|
415
473
|
candidateIds: candidates.map((candidate) => candidate.id),
|
|
474
|
+
...(candidateTraceMetadata.length ? { candidateTraceMetadata } : {}),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Observability is a projection of snapshot-bound evidence, never a second
|
|
479
|
+
* retriever or a route authority. Keep source classification conservative so
|
|
480
|
+
* an absent lane is not manufactured from an arbitrary ID prefix.
|
|
481
|
+
*/
|
|
482
|
+
function traceSourceForCandidate(candidate) {
|
|
483
|
+
// Qualified candidate identities preserve their originating index. Prefer
|
|
484
|
+
// that declared provenance for collapsed `sql_column`/`sql_table` kinds: a
|
|
485
|
+
// dbt manifest column must not be relabelled as runtime-schema evidence.
|
|
486
|
+
const identity = candidate.qualifiedId ?? candidate.id;
|
|
487
|
+
if (identity.startsWith('dbt:'))
|
|
488
|
+
return 'dbt_manifest';
|
|
489
|
+
if (identity.startsWith('runtime:'))
|
|
490
|
+
return 'runtime_schema';
|
|
491
|
+
if (identity.startsWith('semantic:'))
|
|
492
|
+
return 'semantic';
|
|
493
|
+
if (identity.startsWith('dql:'))
|
|
494
|
+
return candidate.kind === 'certified_block' ? 'certified' : 'governed_relational';
|
|
495
|
+
if (candidate.kind === 'certified_block')
|
|
496
|
+
return 'certified';
|
|
497
|
+
if (candidate.kind === 'semantic_metric' || candidate.kind === 'semantic_member')
|
|
498
|
+
return 'semantic';
|
|
499
|
+
if (candidate.kind === 'dql_modeling')
|
|
500
|
+
return 'governed_relational';
|
|
501
|
+
if (candidate.kind === 'dbt_model' || candidate.kind === 'dbt_source')
|
|
502
|
+
return 'dbt_manifest';
|
|
503
|
+
if (candidate.kind === 'sql_table' || candidate.kind === 'sql_column')
|
|
504
|
+
return 'runtime_schema';
|
|
505
|
+
return 'exploratory';
|
|
506
|
+
}
|
|
507
|
+
function traceCandidateLifecycleBeforePruning(request, evidence, candidates, packageCandidates) {
|
|
508
|
+
const observer = askTraceObserverForV1(request);
|
|
509
|
+
if (!observer.enabled)
|
|
510
|
+
return;
|
|
511
|
+
const packageIds = new Set(packageCandidates.map((candidate) => candidate.id));
|
|
512
|
+
const fuse = observer.startSpan({
|
|
513
|
+
name: 'retrieval.fuse',
|
|
514
|
+
stage: 'retrieval',
|
|
515
|
+
payload: { kind: 'retrieval', candidateCount: candidates.length },
|
|
516
|
+
});
|
|
517
|
+
const requirements = buildAnalyticalRequirementSet({ question: request.question, parsedIntent: evidence.parsedIntent });
|
|
518
|
+
const requestedRoles = new Set([
|
|
519
|
+
...(requirements.measures.length || requirements.ranking?.metricTerms.length ? ['metric'] : []),
|
|
520
|
+
...(requirements.entityTerms.length || requirements.entityDisplayTerms.length ? ['entity_label'] : []),
|
|
521
|
+
...(requirements.time ? ['time_dimension'] : []),
|
|
522
|
+
...(requirements.dimensions.length ? ['categorical_dimension'] : []),
|
|
523
|
+
...(requirements.dimensions.length > 1 || requirements.entityTerms.length ? ['relationship'] : []),
|
|
524
|
+
]);
|
|
525
|
+
const candidateLimit = 32;
|
|
526
|
+
const hasRequestedEntityLabel = requirements.entityTerms.length > 0 || requirements.entityDisplayTerms.length > 0;
|
|
527
|
+
for (const [index, candidate] of candidates.slice(0, candidateLimit).entries()) {
|
|
528
|
+
const roles = evidenceCandidateRoles(candidate);
|
|
529
|
+
const source = traceSourceForCandidate(candidate);
|
|
530
|
+
// This receipt must retain the actual retrieval memberships captured by
|
|
531
|
+
// the snapshot. Do not infer a lane from aliases, IDs, or a relationship
|
|
532
|
+
// attribute after fusion has already lost that physical provenance.
|
|
533
|
+
const lanes = candidate.retrievalLanes
|
|
534
|
+
?.map((entry) => ({ ...entry }))
|
|
535
|
+
.sort((left, right) => (left.rank ?? Number.MAX_SAFE_INTEGER) - (right.rank ?? Number.MAX_SAFE_INTEGER)
|
|
536
|
+
|| left.lane.localeCompare(right.lane));
|
|
537
|
+
const lane = lanes?.[0]?.lane;
|
|
538
|
+
const laneRank = lanes?.[0]?.rank;
|
|
539
|
+
const initialReason = candidate.exactMatch
|
|
540
|
+
? 'exact_name_match'
|
|
541
|
+
: 'unknown';
|
|
542
|
+
const compatibilityCode = candidate.compatibility === 'compatible' ? 'compatible'
|
|
543
|
+
: candidate.compatibility === 'incompatible' ? 'operation_unsupported'
|
|
544
|
+
: 'unknown';
|
|
545
|
+
for (const role of roles) {
|
|
546
|
+
const common = {
|
|
547
|
+
// The resolver and cascade use `id`; keeping that exact stable identity
|
|
548
|
+
// makes retrieval, admission, and model-selection receipts joinable.
|
|
549
|
+
candidateId: candidate.id,
|
|
550
|
+
role,
|
|
551
|
+
source,
|
|
552
|
+
...(lane ? { lane } : {}),
|
|
553
|
+
...(laneRank !== undefined ? { laneRank } : {}),
|
|
554
|
+
...(lanes?.length ? { lanes } : {}),
|
|
555
|
+
fusedRank: index + 1,
|
|
556
|
+
compatibilityCode,
|
|
557
|
+
};
|
|
558
|
+
observer.recordCandidateDecision({
|
|
559
|
+
...common,
|
|
560
|
+
decision: 'retrieved',
|
|
561
|
+
reasonCode: candidate.eligible === false ? 'role_mismatch' : initialReason,
|
|
562
|
+
});
|
|
563
|
+
// An explicit ranking measure is a typed request, not a relevance hint.
|
|
564
|
+
// Keep correlated metrics in the receipt, but record their exclusion at
|
|
565
|
+
// the metric admission boundary instead of allowing them to become
|
|
566
|
+
// clarification options or to fill an unrelated analytical role.
|
|
567
|
+
if (role === 'metric' && candidateConflictsWithExplicitRankingMeasure(candidate, requirements)) {
|
|
568
|
+
observer.recordCandidateDecision({
|
|
569
|
+
...common,
|
|
570
|
+
decision: 'excluded',
|
|
571
|
+
reasonCode: 'explicit_measure_conflict',
|
|
572
|
+
});
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (candidate.eligible === false || candidate.compatibility === 'incompatible') {
|
|
576
|
+
observer.recordCandidateDecision({
|
|
577
|
+
...common,
|
|
578
|
+
decision: 'excluded',
|
|
579
|
+
reasonCode: candidate.compatibility === 'incompatible' ? 'capability_incompatible' : 'role_mismatch',
|
|
580
|
+
});
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (packageIds.has(candidate.id)) {
|
|
584
|
+
const reservedForRole = requestedRoles.has(role);
|
|
585
|
+
observer.recordCandidateDecision({
|
|
586
|
+
...common,
|
|
587
|
+
decision: 'reserved',
|
|
588
|
+
reasonCode: candidate.exactMatch ? 'exact_name_match' : reservedForRole ? 'role_reserved' : 'fused_relevance_fill',
|
|
589
|
+
});
|
|
590
|
+
observer.recordCandidateDecision({
|
|
591
|
+
...common,
|
|
592
|
+
decision: 'admitted',
|
|
593
|
+
reasonCode: candidate.exactMatch ? 'exact_name_match' : reservedForRole ? 'role_reserved' : 'fused_relevance_fill',
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
const isNoisyEntityAttribute = hasRequestedEntityLabel
|
|
598
|
+
&& isEntityAttributeCandidate(candidate)
|
|
599
|
+
&& role === 'categorical_dimension';
|
|
600
|
+
observer.recordCandidateDecision({
|
|
601
|
+
...common,
|
|
602
|
+
decision: 'excluded',
|
|
603
|
+
reasonCode: isNoisyEntityAttribute ? 'entity_label_mismatch' : 'below_fused_limit',
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
observer.finishSpan(fuse, { outcome: 'ok', reasonCode: 'completed' });
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Emit source-bound lane summaries before the meaning-package cap applies.
|
|
612
|
+
* These spans are evidence about the existing retrieval result only; they do
|
|
613
|
+
* not invoke, retry, or reinterpret any retrieval source.
|
|
614
|
+
*/
|
|
615
|
+
function traceRetrievalLanesBeforePruning(request, evidence, candidates) {
|
|
616
|
+
const observer = askTraceObserverForV1(request);
|
|
617
|
+
if (!observer.enabled)
|
|
618
|
+
return;
|
|
619
|
+
const sourceSpan = {
|
|
620
|
+
certified: 'retrieval.certified',
|
|
621
|
+
semantic: 'retrieval.semantic',
|
|
622
|
+
governed_relational: 'retrieval.governed_relational',
|
|
623
|
+
dbt_manifest: 'retrieval.dbt_manifest',
|
|
624
|
+
runtime_schema: 'retrieval.runtime_schema',
|
|
625
|
+
vector: 'retrieval.vector',
|
|
626
|
+
conversation: 'retrieval.conversation',
|
|
627
|
+
};
|
|
628
|
+
const record = (name, source, count, coverage, lane) => {
|
|
629
|
+
const span = observer.startSpan({
|
|
630
|
+
name,
|
|
631
|
+
stage: 'retrieval',
|
|
632
|
+
reasonCode: coverage === 'unavailable' ? 'source_unavailable' : coverage === 'empty' ? 'source_empty' : coverage === 'stale' ? 'source_stale' : 'completed',
|
|
633
|
+
payload: { kind: 'retrieval', ...(source ? { source } : {}), ...(lane ? { lane } : {}), candidateCount: count, ...(coverage ? { coverage } : {}) },
|
|
634
|
+
});
|
|
635
|
+
observer.finishSpan(span, {
|
|
636
|
+
outcome: coverage === 'unavailable' ? 'unavailable' : 'ok',
|
|
637
|
+
reasonCode: coverage === 'unavailable' ? 'source_unavailable' : coverage === 'empty' ? 'source_empty' : coverage === 'stale' ? 'source_stale' : 'completed',
|
|
638
|
+
});
|
|
639
|
+
};
|
|
640
|
+
for (const coverage of sourceCoverageFromEvidence(evidence, candidates)) {
|
|
641
|
+
const name = sourceSpan[coverage.source];
|
|
642
|
+
if (!name)
|
|
643
|
+
continue;
|
|
644
|
+
record(name, coverage.source, coverage.candidateIds.length, coverage.status, coverage.source === 'vector' ? 'vector' : coverage.source === 'conversation' ? 'conversation' : undefined);
|
|
645
|
+
}
|
|
646
|
+
const laneSpan = {
|
|
647
|
+
exact: 'retrieval.exact',
|
|
648
|
+
lexical: 'retrieval.lexical',
|
|
649
|
+
vector: 'retrieval.vector',
|
|
650
|
+
graph: 'retrieval.graph',
|
|
651
|
+
conversation: 'retrieval.conversation',
|
|
416
652
|
};
|
|
653
|
+
for (const lane of Object.keys(laneSpan)) {
|
|
654
|
+
const count = candidates.filter((candidate) => candidate.retrievalLanes?.some((membership) => membership.lane === lane)).length;
|
|
655
|
+
if (count > 0)
|
|
656
|
+
record(laneSpan[lane], undefined, count, 'available', lane);
|
|
657
|
+
}
|
|
417
658
|
}
|
|
418
659
|
/** Preserve actual retrieval provenance; never infer lane state from an ID regex. */
|
|
419
660
|
function sourceCoverageFromEvidence(evidence, candidates) {
|
|
@@ -535,6 +776,9 @@ function withNormalizedAnalyticalRequirements(evidence, question) {
|
|
|
535
776
|
...evidence.parsedIntent,
|
|
536
777
|
measures: requirements.measures,
|
|
537
778
|
dimensions: requirements.dimensions,
|
|
779
|
+
...(requirements.time?.grain && !evidence.parsedIntent.timeGrain
|
|
780
|
+
? { timeGrain: requirements.time.grain }
|
|
781
|
+
: {}),
|
|
538
782
|
},
|
|
539
783
|
};
|
|
540
784
|
}
|
|
@@ -577,11 +821,62 @@ function relationshipSafetyAllowsAutomaticJoin(safety, requiredJoinKey) {
|
|
|
577
821
|
return safety.keys.some((key) => metricTermsMatch(normalizeMetricPhrase(key.from), normalizedKey)
|
|
578
822
|
|| metricTermsMatch(normalizeMetricPhrase(key.to), normalizedKey));
|
|
579
823
|
}
|
|
580
|
-
|
|
824
|
+
/**
|
|
825
|
+
* Relationship evidence has three deliberately different authorities:
|
|
826
|
+
*
|
|
827
|
+
* - governed: a certified, fresh proof may compile a governed relational plan;
|
|
828
|
+
* - exploratory: an explicitly allowed, validated relation may close a raw
|
|
829
|
+
* review-required SQL plan after normal runtime validation; and
|
|
830
|
+
* - hint: anything weaker may improve retrieval, but can never add a join.
|
|
831
|
+
*
|
|
832
|
+
* The first authority remains intentionally stricter. Reusing it for raw
|
|
833
|
+
* fallback is what made a missing semantic capability look like a terminal
|
|
834
|
+
* absence even when the dbt/runtime snapshot had the exact relations, keys,
|
|
835
|
+
* and fields required to inspect it safely. This helper never upgrades a
|
|
836
|
+
* draft relationship to governed trust: callers that use it must select the
|
|
837
|
+
* exploratory tier and retain review_required provenance.
|
|
838
|
+
*/
|
|
839
|
+
function relationshipSafetyAllowsExploratoryJoin(safety, requiredJoinKey) {
|
|
840
|
+
const validation = safety.validation;
|
|
841
|
+
const checkedAt = Date.parse(validation?.checkedAt ?? '');
|
|
842
|
+
const expiresAt = safety.evidenceExpiresAt ? Date.parse(safety.evidenceExpiresAt) : undefined;
|
|
843
|
+
const expirationInvalid = safety.evidenceExpiresAt !== undefined
|
|
844
|
+
&& (expiresAt === undefined || !Number.isFinite(expiresAt) || expiresAt <= Date.now());
|
|
845
|
+
const lifecycleAllowsExploration = safety.status === 'certified'
|
|
846
|
+
|| safety.status === 'validated'
|
|
847
|
+
|| safety.status === 'draft';
|
|
848
|
+
if (!lifecycleAllowsExploration
|
|
849
|
+
|| safety.staleCertification === true
|
|
850
|
+
|| safety.automaticJoinAllowed !== true
|
|
851
|
+
|| safety.fanout !== 'safe'
|
|
852
|
+
|| !['one_to_one', 'one_to_many', 'many_to_one'].includes(safety.cardinality ?? '')
|
|
853
|
+
|| !safety.from?.trim()
|
|
854
|
+
|| !safety.to?.trim()
|
|
855
|
+
|| !validation
|
|
856
|
+
|| validation.status !== 'passed'
|
|
857
|
+
|| !validation.queryFingerprint?.trim()
|
|
858
|
+
|| !validation.proofFingerprint?.trim()
|
|
859
|
+
|| !Number.isFinite(checkedAt)
|
|
860
|
+
|| expirationInvalid
|
|
861
|
+
|| safety.keys.length === 0
|
|
862
|
+
|| safety.keys.some((key) => !key.from.trim() || !key.to.trim()))
|
|
863
|
+
return false;
|
|
864
|
+
if (!requiredJoinKey)
|
|
865
|
+
return true;
|
|
866
|
+
const normalizedKey = normalizeMetricPhrase(requiredJoinKey);
|
|
867
|
+
return safety.keys.some((key) => metricTermsMatch(normalizeMetricPhrase(key.from), normalizedKey)
|
|
868
|
+
|| metricTermsMatch(normalizeMetricPhrase(key.to), normalizedKey));
|
|
869
|
+
}
|
|
870
|
+
function relationshipSafetyAllowsJoin(safety, authority, requiredJoinKey) {
|
|
871
|
+
return authority === 'governed'
|
|
872
|
+
? relationshipSafetyAllowsAutomaticJoin(safety, requiredJoinKey)
|
|
873
|
+
: relationshipSafetyAllowsExploratoryJoin(safety, requiredJoinKey);
|
|
874
|
+
}
|
|
875
|
+
function safeRelationshipProofsForCandidate(candidate, requiredJoinKey, authority = 'governed') {
|
|
581
876
|
const referenced = new Set((candidate.relationshipEvidence ?? []).map(normalizedRelationshipIdentity));
|
|
582
877
|
const proofs = new Map();
|
|
583
878
|
for (const safety of candidate.relationshipSafety ?? []) {
|
|
584
|
-
if (!
|
|
879
|
+
if (!relationshipSafetyAllowsJoin(safety, authority, requiredJoinKey))
|
|
585
880
|
continue;
|
|
586
881
|
if (!relationshipSafetyIdentities(safety).some((identity) => referenced.has(identity)))
|
|
587
882
|
continue;
|
|
@@ -596,6 +891,176 @@ function candidateRelationshipEndpoints(candidate) {
|
|
|
596
891
|
candidate.analyticalCapability?.primaryEntityId ?? '',
|
|
597
892
|
].map(normalizedRelationshipIdentity).filter(Boolean));
|
|
598
893
|
}
|
|
894
|
+
/**
|
|
895
|
+
* An attribution-required relationship is intentionally not a generic join
|
|
896
|
+
* failure. It becomes terminal only when the user explicitly asks to rank an
|
|
897
|
+
* un-attributed/attribution-scoped signal and the same immutable snapshot
|
|
898
|
+
* proves that getting from that signal to the requested entity would require
|
|
899
|
+
* the declared attribution edge. This keeps neutral unsafe relationship
|
|
900
|
+
* evidence available for the normal clarification path (AGT-029), while
|
|
901
|
+
* preventing a bare-ranking metric picker from masking a known governance
|
|
902
|
+
* boundary with unrelated options.
|
|
903
|
+
*/
|
|
904
|
+
const EXPLICIT_ATTRIBUTION_REQUEST_RE = /\b(?:un[-\s]?attributed|attribution|allocation|allocate)\b/i;
|
|
905
|
+
function relationshipEndpointMatchesQuestionConcept(endpoint, question) {
|
|
906
|
+
const leaf = normalizedRelationshipIdentity(endpoint).split('::').at(-1) ?? endpoint;
|
|
907
|
+
const concept = normalizeMetricPhrase(leaf);
|
|
908
|
+
const conceptTokens = substantiveLexicalTokens(concept);
|
|
909
|
+
const questionTokens = new Set(substantiveLexicalTokens(question));
|
|
910
|
+
return conceptTokens.length >= 2 && conceptTokens.every((token) => questionTokens.has(token));
|
|
911
|
+
}
|
|
912
|
+
function relationshipEndpointMatchesEntityTerms(endpoint, entityTerms) {
|
|
913
|
+
const normalizedEndpoint = normalizeMetricPhrase(endpoint);
|
|
914
|
+
return entityTerms.some((term) => metricTermsMatch(normalizedEndpoint, normalizeMetricPhrase(term)));
|
|
915
|
+
}
|
|
916
|
+
function relationshipGraphReaches(starts, targets, relationships) {
|
|
917
|
+
if (starts.size === 0 || targets.size === 0)
|
|
918
|
+
return false;
|
|
919
|
+
const graph = new Map();
|
|
920
|
+
for (const relationship of relationships) {
|
|
921
|
+
const from = normalizedRelationshipIdentity(relationship.from ?? '');
|
|
922
|
+
const to = normalizedRelationshipIdentity(relationship.to ?? '');
|
|
923
|
+
if (!from || !to)
|
|
924
|
+
continue;
|
|
925
|
+
const fromNeighbors = graph.get(from) ?? new Set();
|
|
926
|
+
fromNeighbors.add(to);
|
|
927
|
+
graph.set(from, fromNeighbors);
|
|
928
|
+
const toNeighbors = graph.get(to) ?? new Set();
|
|
929
|
+
toNeighbors.add(from);
|
|
930
|
+
graph.set(to, toNeighbors);
|
|
931
|
+
}
|
|
932
|
+
const pending = [...starts];
|
|
933
|
+
const visited = new Set(pending);
|
|
934
|
+
while (pending.length > 0) {
|
|
935
|
+
const current = pending.shift();
|
|
936
|
+
if (targets.has(current))
|
|
937
|
+
return true;
|
|
938
|
+
for (const next of graph.get(current) ?? []) {
|
|
939
|
+
if (!visited.has(next)) {
|
|
940
|
+
visited.add(next);
|
|
941
|
+
pending.push(next);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
function attributionRequiredRelationshipGapDecision(input) {
|
|
948
|
+
if (!EXPLICIT_ATTRIBUTION_REQUEST_RE.test(input.request.question)
|
|
949
|
+
|| input.requirements.entityTerms.length === 0)
|
|
950
|
+
return undefined;
|
|
951
|
+
// Use the complete same-snapshot retrieval result. The compact provider
|
|
952
|
+
// package is intentionally allowed to omit a relationship card; omission
|
|
953
|
+
// cannot erase an authored governance restriction before it is evaluated.
|
|
954
|
+
const snapshotCandidates = [
|
|
955
|
+
...input.evidence.candidates,
|
|
956
|
+
...(input.evidence.clarificationCandidates ?? []),
|
|
957
|
+
].filter((candidate, index, all) => all.findIndex((other) => other.id === candidate.id) === index);
|
|
958
|
+
const relationships = [...new Map(snapshotCandidates
|
|
959
|
+
.flatMap((candidate) => candidate.relationshipSafety ?? [])
|
|
960
|
+
.map((relationship) => [relationship.id, relationship])).values()]
|
|
961
|
+
.filter((relationship) => Boolean(relationship.from?.trim() && relationship.to?.trim()));
|
|
962
|
+
const attributionRequired = relationships.filter((relationship) => relationship.fanout === 'attribution_required');
|
|
963
|
+
if (attributionRequired.length === 0)
|
|
964
|
+
return undefined;
|
|
965
|
+
const targetEndpoints = new Set(snapshotCandidates
|
|
966
|
+
.flatMap((candidate) => [...candidateRelationshipEndpoints(candidate)])
|
|
967
|
+
.filter((endpoint) => relationshipEndpointMatchesEntityTerms(endpoint, input.requirements.entityTerms)));
|
|
968
|
+
if (targetEndpoints.size === 0)
|
|
969
|
+
return undefined;
|
|
970
|
+
const attributed = attributionRequired.find((relationship) => {
|
|
971
|
+
const endpoints = [
|
|
972
|
+
normalizedRelationshipIdentity(relationship.from ?? ''),
|
|
973
|
+
normalizedRelationshipIdentity(relationship.to ?? ''),
|
|
974
|
+
].filter(Boolean);
|
|
975
|
+
const requestedSignalEndpoints = new Set(endpoints.filter((endpoint) => relationshipEndpointMatchesQuestionConcept(endpoint, input.request.question)));
|
|
976
|
+
if (requestedSignalEndpoints.size === 0)
|
|
977
|
+
return false;
|
|
978
|
+
// The declared graph can demonstrate why the requested entity would need
|
|
979
|
+
// this relation, but only a fully certified/fanout-safe graph may
|
|
980
|
+
// authorize automatic composition. A path that requires this edge is a
|
|
981
|
+
// typed modeling gap, never a reason to invent an attribution join.
|
|
982
|
+
const allReachable = relationshipGraphReaches(requestedSignalEndpoints, targetEndpoints, relationships);
|
|
983
|
+
const safeRelationships = relationships.filter((candidate) => relationshipSafetyAllowsAutomaticJoin(candidate));
|
|
984
|
+
const safelyReachable = relationshipGraphReaches(requestedSignalEndpoints, targetEndpoints, safeRelationships);
|
|
985
|
+
return allReachable && !safelyReachable;
|
|
986
|
+
});
|
|
987
|
+
if (!attributed)
|
|
988
|
+
return undefined;
|
|
989
|
+
const coverage = sourceCoverageFromEvidence(input.evidence, snapshotCandidates);
|
|
990
|
+
const coverageFor = (source) => coverage.find((item) => item.source === source);
|
|
991
|
+
const skipped = (tier) => {
|
|
992
|
+
const item = coverageFor(tier);
|
|
993
|
+
return {
|
|
994
|
+
version: 1,
|
|
995
|
+
tier,
|
|
996
|
+
outcome: item?.status === 'available' ? 'ineligible' : 'unavailable',
|
|
997
|
+
candidateIds: item?.candidateIds ?? [],
|
|
998
|
+
reason: item?.status === 'available'
|
|
999
|
+
? `The ${tier} tier did not prove the complete attribution-safe requested tuple before plan freeze.`
|
|
1000
|
+
: `The ${tier} source was ${item?.status ?? 'unavailable'} in this snapshot.`,
|
|
1001
|
+
planFrozen: false,
|
|
1002
|
+
};
|
|
1003
|
+
};
|
|
1004
|
+
const message = 'The requested un-attributed signal requires a certified attribution relationship or approved allocation policy before it can be ranked by the requested entity. DQL did not infer a relationship or execute a query.';
|
|
1005
|
+
const witnessCandidateIds = [attributed.id];
|
|
1006
|
+
const terminalGap = {
|
|
1007
|
+
code: 'MISSING_RELATIONSHIP',
|
|
1008
|
+
missing: ['a certified attribution relationship or approved allocation policy for the requested un-attributed signal'],
|
|
1009
|
+
witnessCandidateIds,
|
|
1010
|
+
};
|
|
1011
|
+
const observer = askTraceObserverForV1(input.request);
|
|
1012
|
+
for (const candidate of snapshotCandidates.filter((candidate) => (candidate.relationshipSafety ?? []).some((relationship) => relationship.id === attributed.id))) {
|
|
1013
|
+
observer.recordCandidateDecision({
|
|
1014
|
+
candidateId: candidate.id,
|
|
1015
|
+
role: 'relationship',
|
|
1016
|
+
source: traceSourceForCandidate(candidate),
|
|
1017
|
+
decision: 'excluded',
|
|
1018
|
+
reasonCode: 'policy_denied',
|
|
1019
|
+
compatibilityCode: 'unknown',
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
...input.base,
|
|
1024
|
+
action: 'block',
|
|
1025
|
+
confidence: 1,
|
|
1026
|
+
reason: message,
|
|
1027
|
+
source: 'heuristic',
|
|
1028
|
+
category: 'data_lookup',
|
|
1029
|
+
depth: 'quick',
|
|
1030
|
+
requiresClarification: false,
|
|
1031
|
+
clarifyingQuestion: undefined,
|
|
1032
|
+
clarificationOptions: undefined,
|
|
1033
|
+
retrievalEvidence: retrievalTrace(input.evidence, snapshotCandidates),
|
|
1034
|
+
terminalOutcome: {
|
|
1035
|
+
kind: 'modeling_gap',
|
|
1036
|
+
code: 'ANALYTICAL_MODELING_GAP',
|
|
1037
|
+
message,
|
|
1038
|
+
candidateIds: witnessCandidateIds,
|
|
1039
|
+
gap: terminalGap,
|
|
1040
|
+
},
|
|
1041
|
+
analyticalCascadeDecision: buildAnalyticalCascadeDecision({
|
|
1042
|
+
requirements: input.requirements,
|
|
1043
|
+
sourceCoverage: coverage,
|
|
1044
|
+
attempts: [
|
|
1045
|
+
skipped('certified'),
|
|
1046
|
+
skipped('semantic'),
|
|
1047
|
+
{
|
|
1048
|
+
version: 1,
|
|
1049
|
+
tier: 'governed_relational',
|
|
1050
|
+
outcome: 'denied',
|
|
1051
|
+
candidateIds: witnessCandidateIds,
|
|
1052
|
+
reason: 'The declared relationship requires attribution or allocation approval and cannot authorize the requested ranking.',
|
|
1053
|
+
planFrozen: false,
|
|
1054
|
+
},
|
|
1055
|
+
],
|
|
1056
|
+
planFrozen: false,
|
|
1057
|
+
terminalGap: cascadeTerminalRelationshipGap(terminalGap),
|
|
1058
|
+
stopReason: 'denied',
|
|
1059
|
+
}),
|
|
1060
|
+
resolvedAnalyticalPlan: undefined,
|
|
1061
|
+
meaningResolution: undefined,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
599
1064
|
/**
|
|
600
1065
|
* A proof may be structurally safe yet belong to a different domain's relation
|
|
601
1066
|
* with the same leaf name. The selected pair must match the proof's canonical
|
|
@@ -609,9 +1074,9 @@ function relationshipProofMatchesSelectedPair(safety, left, right) {
|
|
|
609
1074
|
return Boolean(from && to && ((leftEndpoints.has(from) && rightEndpoints.has(to))
|
|
610
1075
|
|| (leftEndpoints.has(to) && rightEndpoints.has(from))));
|
|
611
1076
|
}
|
|
612
|
-
function safeRelationshipProofsForPair(left, right, requiredJoinKey) {
|
|
613
|
-
const leftProofs = safeRelationshipProofsForCandidate(left, requiredJoinKey);
|
|
614
|
-
const rightProofs = safeRelationshipProofsForCandidate(right, requiredJoinKey);
|
|
1077
|
+
function safeRelationshipProofsForPair(left, right, requiredJoinKey, authority = 'governed') {
|
|
1078
|
+
const leftProofs = safeRelationshipProofsForCandidate(left, requiredJoinKey, authority);
|
|
1079
|
+
const rightProofs = safeRelationshipProofsForCandidate(right, requiredJoinKey, authority);
|
|
615
1080
|
const shared = new Map();
|
|
616
1081
|
for (const [id, proof] of leftProofs) {
|
|
617
1082
|
const pairedProof = rightProofs.get(id);
|
|
@@ -637,7 +1102,13 @@ function hasSafeExploratoryPhysicalPath(requirements, candidates, missingDimensi
|
|
|
637
1102
|
// their safe-value validation happens independently from proving the raw
|
|
638
1103
|
// physical closure. Treating them as fields made a valid relation appear
|
|
639
1104
|
// unmodeled and produced a false absence diagnostic.
|
|
640
|
-
const terms = [...new Set([
|
|
1105
|
+
const terms = [...new Set([
|
|
1106
|
+
...missingDimensions,
|
|
1107
|
+
...requirements.measures,
|
|
1108
|
+
...requirements.entityDisplayTerms,
|
|
1109
|
+
...(requirements.outputTerms ?? []),
|
|
1110
|
+
...requiredPhysicalFieldTerms,
|
|
1111
|
+
]
|
|
641
1112
|
.map(normalizeMetricPhrase)
|
|
642
1113
|
.filter(Boolean))];
|
|
643
1114
|
if (terms.length === 0) {
|
|
@@ -668,6 +1139,8 @@ function hasSafeExploratoryPhysicalPath(requirements, candidates, missingDimensi
|
|
|
668
1139
|
const physicalRoleAliases = {
|
|
669
1140
|
'product category': ['product type'],
|
|
670
1141
|
category: ['product type'],
|
|
1142
|
+
customer: ['customer name'],
|
|
1143
|
+
account: ['account name'],
|
|
671
1144
|
// `product_price` is a local revenue witness only when the same request
|
|
672
1145
|
// also requires product-category grain. It must not let a bare selected
|
|
673
1146
|
// revenue metric bypass its semantic contract through an unrelated raw
|
|
@@ -762,7 +1235,8 @@ function hasSafeExploratoryPhysicalPath(requirements, candidates, missingDimensi
|
|
|
762
1235
|
// routinely contains unrelated raw tables; requiring every one to join made
|
|
763
1236
|
// a complete local path appear unavailable. Conversely, never infer a join
|
|
764
1237
|
// from names or shared column strings: the proof must retain the structured
|
|
765
|
-
//
|
|
1238
|
+
// exploratory-safe disposition above. A selected path remains
|
|
1239
|
+
// review-required; it is not a governed relational plan.
|
|
766
1240
|
const adjacent = relations.map(() => new Set());
|
|
767
1241
|
const joinColumnsForProof = (proof, left, right) => {
|
|
768
1242
|
const leftEndpoints = candidateRelationshipEndpoints(left);
|
|
@@ -793,7 +1267,7 @@ function hasSafeExploratoryPhysicalPath(requirements, candidates, missingDimensi
|
|
|
793
1267
|
const witnessesForEdge = new Map();
|
|
794
1268
|
for (let left = 0; left < relations.length; left += 1) {
|
|
795
1269
|
for (let right = left + 1; right < relations.length; right += 1) {
|
|
796
|
-
const shared = [...safeRelationshipProofsForPair(relations[left], relations[right]).values()]
|
|
1270
|
+
const shared = [...safeRelationshipProofsForPair(relations[left], relations[right], undefined, 'exploratory').values()]
|
|
797
1271
|
.sort((first, second) => first.id.localeCompare(second.id))
|
|
798
1272
|
.flatMap((proof) => {
|
|
799
1273
|
const joinColumns = joinColumnsForProof(proof, relations[left], relations[right]);
|
|
@@ -853,7 +1327,7 @@ function hasSafeExploratoryPhysicalPath(requirements, candidates, missingDimensi
|
|
|
853
1327
|
return {
|
|
854
1328
|
ok: false,
|
|
855
1329
|
candidateIds: [],
|
|
856
|
-
reason: 'Multiple physical relations lacked one connected,
|
|
1330
|
+
reason: 'Multiple physical relations lacked one connected, structured, fanout-safe exploratory join path.',
|
|
857
1331
|
// This is not a lexical conclusion. Every requested physical field was
|
|
858
1332
|
// found above, no one relation covered the tuple, and the structured
|
|
859
1333
|
// relationship/fanout proof graph could not connect the required
|
|
@@ -861,7 +1335,7 @@ function hasSafeExploratoryPhysicalPath(requirements, candidates, missingDimensi
|
|
|
861
1335
|
// receipts and repair guidance.
|
|
862
1336
|
gap: {
|
|
863
1337
|
code: 'MISSING_RELATIONSHIP',
|
|
864
|
-
missing: ['a connected
|
|
1338
|
+
missing: ['a connected, explicitly allowed, validated, fanout-safe relationship proof'],
|
|
865
1339
|
witnessCandidateIds: stableCandidates(relations).map(stableCandidateId),
|
|
866
1340
|
},
|
|
867
1341
|
};
|
|
@@ -875,7 +1349,7 @@ function hasSafeExploratoryPhysicalPath(requirements, candidates, missingDimensi
|
|
|
875
1349
|
if (selectedWitnesses.length !== selected.edges.length) {
|
|
876
1350
|
return { ok: false, candidateIds: [], reason: 'The selected relationship closure lacked qualified join-key witnesses for every automatic join.' };
|
|
877
1351
|
}
|
|
878
|
-
return physicalEvidence(selectedRelations, requiredColumns, selectedWitnesses.flatMap((witness) => witness.columns), selectedWitnesses.map((witness) => witness.proof.id), 'Qualified physical relations and
|
|
1352
|
+
return physicalEvidence(selectedRelations, requiredColumns, selectedWitnesses.flatMap((witness) => witness.columns), selectedWitnesses.map((witness) => witness.proof.id), 'Qualified physical relations and explicitly allowed relationship proofs support a bounded exploratory plan.');
|
|
879
1353
|
}
|
|
880
1354
|
/**
|
|
881
1355
|
* The pre-freeze cascade is router authority, including when the bounded
|
|
@@ -888,15 +1362,69 @@ function hasSafeExploratoryPhysicalPath(requirements, candidates, missingDimensi
|
|
|
888
1362
|
function preFreezePhysicalCascadeDecision(input) {
|
|
889
1363
|
// `candidates` can be the capped meaning package. Physical eligibility is
|
|
890
1364
|
// allowed one same-snapshot extension, never a new retrieval/domain scope.
|
|
891
|
-
const snapshotCandidates = input.evidence.candidates
|
|
892
|
-
? input.evidence.candidates
|
|
893
|
-
: input.candidates;
|
|
1365
|
+
const snapshotCandidates = immutableSnapshotCandidates(input.evidence, input.candidates);
|
|
894
1366
|
const physicalPath = hasSafeExploratoryPhysicalPath(input.requirements, snapshotCandidates, input.missingTerms, input.requiredPhysicalFieldTerms ?? []);
|
|
895
1367
|
const missingRankingMetric = Boolean(physicalPath.ok
|
|
896
1368
|
&& input.requireRankingMetric
|
|
897
1369
|
&& input.requirements.ranking
|
|
898
1370
|
&& input.requirements.ranking.metricTerms.length === 0);
|
|
899
1371
|
const exploratoryExecutable = physicalPath.ok && !missingRankingMetric;
|
|
1372
|
+
const selectedPhysicalLegacyIds = snapshotCandidates
|
|
1373
|
+
.filter((candidate) => physicalPath.candidateIds.includes(candidate.qualifiedId ?? candidate.id))
|
|
1374
|
+
.map((candidate) => candidate.id);
|
|
1375
|
+
const selectedPhysicalIdsForPlan = selectedPhysicalLegacyIds.length > 0
|
|
1376
|
+
? selectedPhysicalLegacyIds
|
|
1377
|
+
: physicalPath.candidateIds;
|
|
1378
|
+
// The physical cascade is still an authoritative Ask route, not a legacy
|
|
1379
|
+
// escape hatch. Recreate the same host-owned seed used before the meaning
|
|
1380
|
+
// call so a semantic miss cannot erase an explicit entity display key,
|
|
1381
|
+
// member/filter, ranking limit, time role, or output tuple on the way to
|
|
1382
|
+
// review-required exploration.
|
|
1383
|
+
const hostRequirementSeed = buildAnalyticalRequirementSeedV1({
|
|
1384
|
+
question: input.question,
|
|
1385
|
+
parsedIntent: input.evidence.parsedIntent,
|
|
1386
|
+
requirements: input.requirements,
|
|
1387
|
+
fiscalCalendar: declaredFiscalCalendar(input.evidence, snapshotCandidates),
|
|
1388
|
+
});
|
|
1389
|
+
// The router, not the SQL generator, owns analytical meaning and the
|
|
1390
|
+
// physical closure. Freeze that selected exploratory plan before a model
|
|
1391
|
+
// sees SQL work. The host later attaches an authorization receipt for the
|
|
1392
|
+
// exact read-only SQL/target; it must never be the event that changes the
|
|
1393
|
+
// selected tier or reinterprets this request.
|
|
1394
|
+
const exploratoryMeaning = exploratoryExecutable
|
|
1395
|
+
? {
|
|
1396
|
+
interpretedQuestion: input.question,
|
|
1397
|
+
questionType: questionTypeFromText(input.question),
|
|
1398
|
+
selectedConceptIds: selectedPhysicalIdsForPlan,
|
|
1399
|
+
recommendedExecutionId: selectedPhysicalIdsForPlan[0],
|
|
1400
|
+
queryIntent: queryIntentFromRequirementSeed(hostRequirementSeed),
|
|
1401
|
+
rejectedCandidates: [],
|
|
1402
|
+
// Low confidence is intentionally non-executable in a resolved plan.
|
|
1403
|
+
// This is a router-proven physical closure, not a speculative model
|
|
1404
|
+
// answer, so the confidence describes the selected route only.
|
|
1405
|
+
confidence: 'high',
|
|
1406
|
+
missingInformation: [],
|
|
1407
|
+
recommendedRoute: 'exploratory',
|
|
1408
|
+
hostRequirementSeed,
|
|
1409
|
+
compatibilityOutcome: 'modeling_gap',
|
|
1410
|
+
compatibilityFailures: input.missingTerms.map((term) => ({
|
|
1411
|
+
code: 'MISSING_DIMENSION',
|
|
1412
|
+
field: term,
|
|
1413
|
+
message: `${term} was not complete in the earlier governed tiers.`,
|
|
1414
|
+
candidateIds: [],
|
|
1415
|
+
})),
|
|
1416
|
+
}
|
|
1417
|
+
: undefined;
|
|
1418
|
+
const resolvedAnalyticalPlan = exploratoryMeaning
|
|
1419
|
+
? buildResolvedAnalyticalPlan({
|
|
1420
|
+
question: input.question,
|
|
1421
|
+
resolution: exploratoryMeaning,
|
|
1422
|
+
evidence: input.evidence,
|
|
1423
|
+
candidates: snapshotCandidates,
|
|
1424
|
+
mode: 'authoritative',
|
|
1425
|
+
})
|
|
1426
|
+
: undefined;
|
|
1427
|
+
const exploratoryPlanFrozen = resolvedAnalyticalPlan?.capability === 'bounded_exploration';
|
|
900
1428
|
const coverage = sourceCoverageFromEvidence(input.evidence, snapshotCandidates);
|
|
901
1429
|
const coverageFor = (source) => coverage.find((item) => item.source === source);
|
|
902
1430
|
const skippedAttempt = (tier, source) => {
|
|
@@ -934,7 +1462,7 @@ function preFreezePhysicalCascadeDecision(input) {
|
|
|
934
1462
|
reason: missingRankingMetric
|
|
935
1463
|
? `${physicalPath.reason} A ranking measure remains unbound, so exploration cannot be selected.`
|
|
936
1464
|
: physicalPath.reason,
|
|
937
|
-
planFrozen:
|
|
1465
|
+
planFrozen: exploratoryPlanFrozen,
|
|
938
1466
|
},
|
|
939
1467
|
];
|
|
940
1468
|
const message = exploratoryExecutable
|
|
@@ -956,7 +1484,7 @@ function preFreezePhysicalCascadeDecision(input) {
|
|
|
956
1484
|
planFrozen: false,
|
|
957
1485
|
}],
|
|
958
1486
|
...(exploratoryExecutable ? { selectedTier: 'exploratory_sql' } : {}),
|
|
959
|
-
planFrozen:
|
|
1487
|
+
planFrozen: exploratoryPlanFrozen,
|
|
960
1488
|
stopReason: exploratoryExecutable ? 'selected' : missingRankingMetric ? 'ambiguous' : 'coverage_gap',
|
|
961
1489
|
});
|
|
962
1490
|
if (!exploratoryExecutable) {
|
|
@@ -997,29 +1525,96 @@ function preFreezePhysicalCascadeDecision(input) {
|
|
|
997
1525
|
clarificationOptions: undefined,
|
|
998
1526
|
retrievalEvidence: retrievalTrace(input.evidence, snapshotCandidates),
|
|
999
1527
|
analyticalCascadeDecision,
|
|
1000
|
-
meaningResolution:
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1528
|
+
meaningResolution: exploratoryMeaning,
|
|
1529
|
+
resolvedAnalyticalPlan,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* The compact meaning package is deliberately not a proof-of-absence set.
|
|
1534
|
+
* Preserve the complete immutable retrieval snapshot for physical closure and
|
|
1535
|
+
* structured-continuation checks, while retaining a caller's server-issued
|
|
1536
|
+
* selection when it was supplied as a same-snapshot clarification card.
|
|
1537
|
+
*/
|
|
1538
|
+
function immutableSnapshotCandidates(evidence, candidates) {
|
|
1539
|
+
const byId = new Map();
|
|
1540
|
+
for (const candidate of [
|
|
1541
|
+
...evidence.candidates,
|
|
1542
|
+
...(evidence.clarificationCandidates ?? []),
|
|
1543
|
+
...candidates,
|
|
1544
|
+
]) {
|
|
1545
|
+
if (!byId.has(candidate.id))
|
|
1546
|
+
byId.set(candidate.id, candidate);
|
|
1547
|
+
}
|
|
1548
|
+
return [...byId.values()];
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* A normal meaning call may nominate either a semantic metric or a governed
|
|
1552
|
+
* modeling card whose declared execution contract cannot satisfy the exact
|
|
1553
|
+
* tuple. Before presenting that *pre-freeze* modeling gap as terminal,
|
|
1554
|
+
* inspect the complete immutable snapshot for one bounded, relationship-safe
|
|
1555
|
+
* physical closure. This is not a post-freeze downgrade: policy, fiscal, and
|
|
1556
|
+
* unsafe failures remain blocked and a safe result is explicitly
|
|
1557
|
+
* review-required exploratory work.
|
|
1558
|
+
*/
|
|
1559
|
+
function continuePreFreezeModelingGapThroughPhysicalSnapshot(input) {
|
|
1560
|
+
const { decision, evidence } = input;
|
|
1561
|
+
const plan = decision.resolvedAnalyticalPlan;
|
|
1562
|
+
if (!plan
|
|
1563
|
+
|| decision.terminalOutcome?.kind !== 'modeling_gap'
|
|
1564
|
+
|| decision.analyticalCascadeDecision?.planFrozen === true
|
|
1565
|
+
|| (plan.recommendedRoute !== 'semantic' && plan.recommendedRoute !== 'governed_sql'))
|
|
1566
|
+
return decision;
|
|
1567
|
+
const selectedPreFreezeTier = plan.recommendedRoute;
|
|
1568
|
+
const requirements = buildAnalyticalRequirementSet({
|
|
1569
|
+
question: input.question,
|
|
1570
|
+
parsedIntent: input.evidence.parsedIntent,
|
|
1571
|
+
});
|
|
1572
|
+
const unresolved = plan.resolutionFailure?.bindings
|
|
1573
|
+
.filter((binding) => binding.status !== 'resolved')
|
|
1574
|
+
.map((binding) => binding.requested) ?? [];
|
|
1575
|
+
const missingTerms = [...new Set([
|
|
1576
|
+
...unresolved,
|
|
1577
|
+
...requirements.dimensions,
|
|
1578
|
+
...requirements.entityTerms,
|
|
1579
|
+
...requirements.entityDisplayTerms,
|
|
1580
|
+
...(plan.outputContract.requiredOutputs ?? []).map((binding) => binding.requested),
|
|
1581
|
+
])];
|
|
1582
|
+
const requiredPhysicalFieldTerms = [...new Set([
|
|
1583
|
+
...requirements.dimensions,
|
|
1584
|
+
...requirements.entityTerms,
|
|
1585
|
+
...requirements.entityDisplayTerms,
|
|
1586
|
+
...(plan.outputContract.requiredOutputs ?? []).map((binding) => binding.requested),
|
|
1587
|
+
...(plan.query.filters ?? []).map((filter) => filter.field),
|
|
1588
|
+
...unresolved,
|
|
1589
|
+
])];
|
|
1590
|
+
const physicalContinuation = preFreezePhysicalCascadeDecision({
|
|
1591
|
+
base: input.base,
|
|
1592
|
+
evidence,
|
|
1593
|
+
candidates: immutableSnapshotCandidates(evidence, input.candidates),
|
|
1594
|
+
question: input.question,
|
|
1595
|
+
requirements,
|
|
1596
|
+
missingTerms,
|
|
1597
|
+
requiredPhysicalFieldTerms,
|
|
1598
|
+
messagePrefix: selectedPreFreezeTier === 'governed_sql'
|
|
1599
|
+
? 'The selected governed relational interpretation did not prove a compiler-owned DQL projection before plan freeze.'
|
|
1600
|
+
: 'The selected semantic interpretation was pre-freeze-ineligible for the complete requested tuple.',
|
|
1601
|
+
terminalCandidateIds: plan.selectedConceptIds,
|
|
1602
|
+
});
|
|
1603
|
+
if (physicalContinuation.action === 'answer'
|
|
1604
|
+
&& physicalContinuation.analyticalCascadeDecision?.selectedTier === 'exploratory_sql') {
|
|
1605
|
+
return physicalContinuation;
|
|
1606
|
+
}
|
|
1607
|
+
// The physical extension was considered and was not safe/executable. Keep
|
|
1608
|
+
// the original immutable semantic plan and its typed failure rather than
|
|
1609
|
+
// replacing it with a synthetic exploratory meaning; surface the full
|
|
1610
|
+
// snapshot cascade evidence so the terminal block explains why it could not
|
|
1611
|
+
// continue. This retains policy/unsafe/fiscal terminal semantics.
|
|
1612
|
+
return {
|
|
1613
|
+
...decision,
|
|
1614
|
+
reason: physicalContinuation.reason,
|
|
1615
|
+
retrievalEvidence: physicalContinuation.retrievalEvidence,
|
|
1616
|
+
analyticalCascadeDecision: physicalContinuation.analyticalCascadeDecision,
|
|
1617
|
+
terminalOutcome: physicalContinuation.terminalOutcome ?? decision.terminalOutcome,
|
|
1023
1618
|
};
|
|
1024
1619
|
}
|
|
1025
1620
|
/**
|
|
@@ -1102,7 +1697,7 @@ function findSafeComplementaryPhysicalComposition(question, candidates) {
|
|
|
1102
1697
|
const joinKey = displayColumnsOnRelation.flatMap(columnJoinKeys).find((key) => predicateJoinKeys.has(key));
|
|
1103
1698
|
if (!joinKey)
|
|
1104
1699
|
continue;
|
|
1105
|
-
const sharedRelationshipProofIds = [...safeRelationshipProofsForPair(displayRelation, predicateRelation, joinKey).keys()];
|
|
1700
|
+
const sharedRelationshipProofIds = [...safeRelationshipProofsForPair(displayRelation, predicateRelation, joinKey, 'exploratory').keys()];
|
|
1106
1701
|
if (sharedRelationshipProofIds.length === 0)
|
|
1107
1702
|
continue;
|
|
1108
1703
|
const selectedPhysical = [
|
|
@@ -1148,10 +1743,40 @@ function complementaryExploratoryDecision(base, evidence, candidates, question)
|
|
|
1148
1743
|
// Use all already-retrieved cards from the same snapshot. The bounded meaning
|
|
1149
1744
|
// package can omit a supporting raw join column, but no new retrieval/domain
|
|
1150
1745
|
// scope is opened here.
|
|
1151
|
-
const
|
|
1746
|
+
const snapshotCandidates = immutableSnapshotCandidates(evidence, candidates);
|
|
1747
|
+
const composition = findSafeComplementaryPhysicalComposition(question, snapshotCandidates);
|
|
1152
1748
|
if (!composition)
|
|
1153
1749
|
return undefined;
|
|
1154
|
-
const coverage = sourceCoverageFromEvidence(evidence,
|
|
1750
|
+
const coverage = sourceCoverageFromEvidence(evidence, snapshotCandidates);
|
|
1751
|
+
const exploratoryMeaning = {
|
|
1752
|
+
interpretedQuestion: question,
|
|
1753
|
+
questionType: questionTypeFromText(question),
|
|
1754
|
+
selectedConceptIds: composition.candidateIds,
|
|
1755
|
+
recommendedExecutionId: composition.displayColumn.qualifiedId ?? composition.displayColumn.id,
|
|
1756
|
+
queryIntent: {
|
|
1757
|
+
measures: [],
|
|
1758
|
+
dimensions: [composition.displayColumn.qualifiedId ?? composition.displayColumn.id],
|
|
1759
|
+
filters: [{
|
|
1760
|
+
field: composition.predicateColumn.qualifiedId ?? composition.predicateColumn.id,
|
|
1761
|
+
value: 'true',
|
|
1762
|
+
}],
|
|
1763
|
+
},
|
|
1764
|
+
rejectedCandidates: [],
|
|
1765
|
+
confidence: 'high',
|
|
1766
|
+
missingInformation: [],
|
|
1767
|
+
recommendedRoute: 'exploratory',
|
|
1768
|
+
compatibilityOutcome: 'modeling_gap',
|
|
1769
|
+
};
|
|
1770
|
+
const resolvedAnalyticalPlan = buildResolvedAnalyticalPlan({
|
|
1771
|
+
question,
|
|
1772
|
+
resolution: exploratoryMeaning,
|
|
1773
|
+
evidence,
|
|
1774
|
+
candidates: snapshotCandidates,
|
|
1775
|
+
mode: 'authoritative',
|
|
1776
|
+
});
|
|
1777
|
+
const exploratoryPlanFrozen = resolvedAnalyticalPlan.capability === 'bounded_exploration';
|
|
1778
|
+
if (!exploratoryPlanFrozen)
|
|
1779
|
+
return undefined;
|
|
1155
1780
|
const coverageFor = (source) => coverage.find((item) => item.source === source);
|
|
1156
1781
|
const skippedAttempt = (tier, source, reason) => {
|
|
1157
1782
|
const item = coverageFor(source);
|
|
@@ -1184,7 +1809,7 @@ function complementaryExploratoryDecision(base, evidence, candidates, question)
|
|
|
1184
1809
|
outcome: 'executable',
|
|
1185
1810
|
candidateIds: composition.candidateIds,
|
|
1186
1811
|
reason: composition.reason,
|
|
1187
|
-
planFrozen:
|
|
1812
|
+
planFrozen: exploratoryPlanFrozen,
|
|
1188
1813
|
},
|
|
1189
1814
|
];
|
|
1190
1815
|
const analyticalCascadeDecision = buildAnalyticalCascadeDecision({
|
|
@@ -1192,7 +1817,7 @@ function complementaryExploratoryDecision(base, evidence, candidates, question)
|
|
|
1192
1817
|
sourceCoverage: coverage,
|
|
1193
1818
|
attempts,
|
|
1194
1819
|
selectedTier: 'exploratory_sql',
|
|
1195
|
-
planFrozen:
|
|
1820
|
+
planFrozen: exploratoryPlanFrozen,
|
|
1196
1821
|
stopReason: 'selected',
|
|
1197
1822
|
});
|
|
1198
1823
|
return {
|
|
@@ -1204,27 +1829,10 @@ function complementaryExploratoryDecision(base, evidence, candidates, question)
|
|
|
1204
1829
|
depth: 'quick',
|
|
1205
1830
|
reason: `${composition.reason} Certified and semantic execution did not freeze; generated SQL remains review_required.`,
|
|
1206
1831
|
requiresClarification: false,
|
|
1207
|
-
retrievalEvidence: retrievalTrace(evidence,
|
|
1832
|
+
retrievalEvidence: retrievalTrace(evidence, snapshotCandidates),
|
|
1208
1833
|
analyticalCascadeDecision,
|
|
1209
|
-
resolvedAnalyticalPlan
|
|
1210
|
-
meaningResolution:
|
|
1211
|
-
interpretedQuestion: question,
|
|
1212
|
-
questionType: questionTypeFromText(question),
|
|
1213
|
-
selectedConceptIds: composition.candidateIds,
|
|
1214
|
-
recommendedExecutionId: composition.displayColumn.qualifiedId ?? composition.displayColumn.id,
|
|
1215
|
-
queryIntent: {
|
|
1216
|
-
measures: [],
|
|
1217
|
-
dimensions: [composition.displayColumn.qualifiedId ?? composition.displayColumn.id],
|
|
1218
|
-
filters: [{
|
|
1219
|
-
field: composition.predicateColumn.qualifiedId ?? composition.predicateColumn.id,
|
|
1220
|
-
value: 'true',
|
|
1221
|
-
}],
|
|
1222
|
-
},
|
|
1223
|
-
rejectedCandidates: [],
|
|
1224
|
-
confidence: 'low',
|
|
1225
|
-
missingInformation: ['No certified or semantic route proved the complete composed request.'],
|
|
1226
|
-
recommendedRoute: 'exploratory',
|
|
1227
|
-
},
|
|
1834
|
+
resolvedAnalyticalPlan,
|
|
1835
|
+
meaningResolution: exploratoryMeaning,
|
|
1228
1836
|
};
|
|
1229
1837
|
}
|
|
1230
1838
|
function cascadeForResolution(input) {
|
|
@@ -1262,6 +1870,7 @@ function cascadeForResolution(input) {
|
|
|
1262
1870
|
attempts,
|
|
1263
1871
|
...(selectedTier ? { selectedTier } : {}),
|
|
1264
1872
|
planFrozen: frozen,
|
|
1873
|
+
...(input.terminalGap ? { terminalGap: input.terminalGap } : {}),
|
|
1265
1874
|
stopReason: input.reconciliation.outcome === 'ready' ? 'selected' : input.reconciliation.outcome === 'clarify' ? 'ambiguous' : input.reconciliation.outcome === 'policy_blocked' ? 'denied' : 'coverage_gap',
|
|
1266
1875
|
});
|
|
1267
1876
|
}
|
|
@@ -1304,6 +1913,9 @@ function routeDecisionForResolution(base, evidence, candidates, resolution, sour
|
|
|
1304
1913
|
const analytical = routedResolution.questionType === "diagnosis" ||
|
|
1305
1914
|
routedResolution.questionType === "research";
|
|
1306
1915
|
const reason = reconciliation.reason;
|
|
1916
|
+
const terminalGap = reconciliation.outcome === 'modeling_gap'
|
|
1917
|
+
? terminalGapWitnessForResolutionFailure(resolvedAnalyticalPlan)
|
|
1918
|
+
: undefined;
|
|
1307
1919
|
const analyticalCascadeDecision = cascadeForResolution({
|
|
1308
1920
|
evidence,
|
|
1309
1921
|
candidates,
|
|
@@ -1311,12 +1923,22 @@ function routeDecisionForResolution(base, evidence, candidates, resolution, sour
|
|
|
1311
1923
|
plan: resolvedAnalyticalPlan,
|
|
1312
1924
|
reconciliation,
|
|
1313
1925
|
question,
|
|
1926
|
+
...(terminalGap ? { terminalGap: cascadeTerminalRelationshipGap(terminalGap) } : {}),
|
|
1314
1927
|
});
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
|
|
1928
|
+
const declaredDimensionAssumptions = assumptionsForDeclaredDimensionAlternatives({
|
|
1929
|
+
question,
|
|
1930
|
+
evidence,
|
|
1931
|
+
candidates,
|
|
1932
|
+
plan: resolvedAnalyticalPlan,
|
|
1933
|
+
});
|
|
1934
|
+
// The deterministic pre-router may attach a generic soft clarification
|
|
1935
|
+
// while retrieval is still incomplete. Once this same turn has bound and
|
|
1936
|
+
// frozen a plan, that stale prompt must not leak into a completed answer or
|
|
1937
|
+
// its trace. Keep a clarification only when reconciliation itself says the
|
|
1938
|
+
// final immutable tuple is genuinely ambiguous.
|
|
1939
|
+
const { clarifyingQuestion: _staleClarifyingQuestion, clarificationOptions: _staleClarificationOptions, clarifySoft: _staleClarifySoft, ...baseWithoutStaleClarification } = base;
|
|
1318
1940
|
return {
|
|
1319
|
-
...
|
|
1941
|
+
...baseWithoutStaleClarification,
|
|
1320
1942
|
action: needsClarification
|
|
1321
1943
|
? "clarify"
|
|
1322
1944
|
: terminallyBlocked
|
|
@@ -1359,8 +1981,12 @@ function routeDecisionForResolution(base, evidence, candidates, resolution, sour
|
|
|
1359
1981
|
...(needsClarification
|
|
1360
1982
|
? {
|
|
1361
1983
|
clarifyingQuestion: reconciliation.question,
|
|
1984
|
+
...(_staleClarifySoft ? { clarifySoft: true } : {}),
|
|
1362
1985
|
}
|
|
1363
1986
|
: {}),
|
|
1987
|
+
...(declaredDimensionAssumptions.length > 0
|
|
1988
|
+
? { assumptions: declaredDimensionAssumptions }
|
|
1989
|
+
: {}),
|
|
1364
1990
|
};
|
|
1365
1991
|
}
|
|
1366
1992
|
/**
|
|
@@ -1524,6 +2150,7 @@ function reconcileResolvedPlanOutcome(resolution, plan, candidates) {
|
|
|
1524
2150
|
...plan.query.measures.map((binding) => ({ kind: 'measure', binding })),
|
|
1525
2151
|
...plan.query.dimensions.map((binding) => ({ kind: 'dimension', binding })),
|
|
1526
2152
|
...plan.query.filters.map((filter) => ({ kind: 'filter', binding: filter.binding })),
|
|
2153
|
+
...(plan.outputContract.requiredOutputs ?? []).map((binding) => ({ kind: 'output', binding })),
|
|
1527
2154
|
].filter(({ binding }) => binding.status !== 'resolved');
|
|
1528
2155
|
const qualifiedChoiceIds = [...new Set(bindings.flatMap(({ binding }) => binding.candidateIds))].sort();
|
|
1529
2156
|
const userResolvableBinding = bindings.some(({ binding }) => binding.candidateIds.length > 0);
|
|
@@ -1686,19 +2313,57 @@ function routedClarificationQuestion(resolution, bindings, options) {
|
|
|
1686
2313
|
return resolution.clarifyingQuestion
|
|
1687
2314
|
?? `Which governed binding should I use before running this query?`;
|
|
1688
2315
|
}
|
|
1689
|
-
function continueCascadeAfterIncompleteSelection(base, evidence, candidates, selected, question) {
|
|
2316
|
+
function continueCascadeAfterIncompleteSelection(base, evidence, candidates, selected, question, selectedConceptIds = [selected.id]) {
|
|
1690
2317
|
// Preserve the exact stable ID emitted by the clarification option. A
|
|
1691
2318
|
// candidate may also carry a source-qualified execution alias, but replacing
|
|
1692
2319
|
// the clicked ID here makes the persisted ambiguity contract and the later
|
|
1693
2320
|
// diagnostic receipt disagree even though the user chose a legitimate item.
|
|
1694
2321
|
const selectedId = selected.id;
|
|
2322
|
+
// A server-issued display-key click has already passed the snapshot and
|
|
2323
|
+
// metric-capability checks. If its tuple is pre-freeze-ineligible, preserve
|
|
2324
|
+
// both the metric authority and the exact clicked ID through the typed gap
|
|
2325
|
+
// or same-snapshot exploratory continuation. Reconstructing only the
|
|
2326
|
+
// metric made restart receipts falsely look as though no choice was made.
|
|
2327
|
+
const preservedSelectionIds = [...new Set([
|
|
2328
|
+
...selectedConceptIds.filter((id) => Boolean(id?.trim())),
|
|
2329
|
+
selectedId,
|
|
2330
|
+
])];
|
|
2331
|
+
// A structured choice is validated against its exact stable identity, but an
|
|
2332
|
+
// incomplete selected capability must evaluate physical eligibility against
|
|
2333
|
+
// the entire immutable snapshot—not the compact model package that happened
|
|
2334
|
+
// to carry the clarification. This is one same-snapshot extension only.
|
|
2335
|
+
const snapshotCandidates = immutableSnapshotCandidates(evidence, candidates);
|
|
1695
2336
|
const requirements = buildAnalyticalRequirementSet({ question, parsedIntent: evidence.parsedIntent });
|
|
1696
2337
|
const requiredPhysicalFieldTerms = [
|
|
1697
2338
|
...(evidence.parsedIntent?.dimensions ?? []),
|
|
1698
2339
|
...(evidence.parsedIntent?.filters ?? []).map((filter) => filter.field),
|
|
1699
2340
|
];
|
|
1700
|
-
const physicalPath = hasSafeExploratoryPhysicalPath(requirements,
|
|
1701
|
-
|
|
2341
|
+
const physicalPath = hasSafeExploratoryPhysicalPath(requirements, snapshotCandidates, requirements.dimensions, requiredPhysicalFieldTerms);
|
|
2342
|
+
// A structured clarification consumes the exact selected ID once, but an
|
|
2343
|
+
// incomplete governed capability is not a post-freeze terminal. Reuse the
|
|
2344
|
+
// router-owned physical cascade so the same-snapshot exploratory closure is
|
|
2345
|
+
// resolved and frozen *before* SQL is generated. The host may later attach
|
|
2346
|
+
// only an authorization receipt for that frozen plan; it must not select or
|
|
2347
|
+
// freeze another plan after SQL exists.
|
|
2348
|
+
if (physicalPath.ok) {
|
|
2349
|
+
return preFreezePhysicalCascadeDecision({
|
|
2350
|
+
base,
|
|
2351
|
+
evidence,
|
|
2352
|
+
candidates: snapshotCandidates,
|
|
2353
|
+
question,
|
|
2354
|
+
requirements,
|
|
2355
|
+
missingTerms: [...new Set([
|
|
2356
|
+
...requirements.dimensions,
|
|
2357
|
+
...requirements.entityTerms,
|
|
2358
|
+
...requirements.entityDisplayTerms,
|
|
2359
|
+
])],
|
|
2360
|
+
requiredPhysicalFieldTerms,
|
|
2361
|
+
messagePrefix: `The selected governed meaning ${selectedId} did not prove the complete requested tuple. DQL consumed that selection once and did not substitute a correlated metric or execute a different artifact;`,
|
|
2362
|
+
terminalCandidateIds: preservedSelectionIds,
|
|
2363
|
+
requireRankingMetric: Boolean(requirements.ranking),
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
const coverage = sourceCoverageFromEvidence(evidence, snapshotCandidates);
|
|
1702
2367
|
const governedCoverage = coverage.find((item) => item.source === 'governed_relational');
|
|
1703
2368
|
const attempts = [
|
|
1704
2369
|
{
|
|
@@ -1713,7 +2378,7 @@ function continueCascadeAfterIncompleteSelection(base, evidence, candidates, sel
|
|
|
1713
2378
|
version: 1,
|
|
1714
2379
|
tier: 'semantic',
|
|
1715
2380
|
outcome: 'ineligible',
|
|
1716
|
-
candidateIds: [...new Set([
|
|
2381
|
+
candidateIds: [...new Set([...preservedSelectionIds, ...(coverage.find((item) => item.source === 'semantic')?.candidateIds ?? [])])],
|
|
1717
2382
|
reason: `The explicit selection ${selectedId} was consumed once but did not prove the complete semantic tuple.`,
|
|
1718
2383
|
planFrozen: false,
|
|
1719
2384
|
},
|
|
@@ -1744,7 +2409,7 @@ function continueCascadeAfterIncompleteSelection(base, evidence, candidates, sel
|
|
|
1744
2409
|
sourceCoverage: coverage,
|
|
1745
2410
|
attempts: physicalPath.ok
|
|
1746
2411
|
? attempts
|
|
1747
|
-
: [...attempts, { version: 1, tier: 'clarify_or_gap', outcome: 'unavailable', candidateIds:
|
|
2412
|
+
: [...attempts, { version: 1, tier: 'clarify_or_gap', outcome: 'unavailable', candidateIds: preservedSelectionIds, reason: message, planFrozen: false }],
|
|
1748
2413
|
...(physicalPath.ok ? { selectedTier: 'exploratory_sql' } : {}),
|
|
1749
2414
|
planFrozen: false,
|
|
1750
2415
|
stopReason: physicalPath.ok ? 'selected' : 'coverage_gap',
|
|
@@ -1752,7 +2417,7 @@ function continueCascadeAfterIncompleteSelection(base, evidence, candidates, sel
|
|
|
1752
2417
|
const meaningResolution = {
|
|
1753
2418
|
interpretedQuestion: question,
|
|
1754
2419
|
questionType: 'value',
|
|
1755
|
-
selectedConceptIds:
|
|
2420
|
+
selectedConceptIds: preservedSelectionIds,
|
|
1756
2421
|
queryIntent: defaultQueryIntent(evidence),
|
|
1757
2422
|
rejectedCandidates: [],
|
|
1758
2423
|
confidence: 'low',
|
|
@@ -1763,7 +2428,7 @@ function continueCascadeAfterIncompleteSelection(base, evidence, candidates, sel
|
|
|
1763
2428
|
code: 'INCOMPLETE_SELECTED_CAPABILITY',
|
|
1764
2429
|
field: 'selected capability',
|
|
1765
2430
|
message,
|
|
1766
|
-
candidateIds:
|
|
2431
|
+
candidateIds: preservedSelectionIds,
|
|
1767
2432
|
}],
|
|
1768
2433
|
};
|
|
1769
2434
|
if (!physicalPath.ok) {
|
|
@@ -1775,7 +2440,7 @@ function continueCascadeAfterIncompleteSelection(base, evidence, candidates, sel
|
|
|
1775
2440
|
source: 'heuristic',
|
|
1776
2441
|
category: 'data_lookup',
|
|
1777
2442
|
depth: 'quick',
|
|
1778
|
-
retrievalEvidence: retrievalTrace(evidence,
|
|
2443
|
+
retrievalEvidence: retrievalTrace(evidence, snapshotCandidates),
|
|
1779
2444
|
requiresClarification: false,
|
|
1780
2445
|
resolvedAnalyticalPlan: undefined,
|
|
1781
2446
|
meaningResolution,
|
|
@@ -1784,7 +2449,7 @@ function continueCascadeAfterIncompleteSelection(base, evidence, candidates, sel
|
|
|
1784
2449
|
kind: 'modeling_gap',
|
|
1785
2450
|
code: 'ANALYTICAL_MODELING_GAP',
|
|
1786
2451
|
message,
|
|
1787
|
-
candidateIds:
|
|
2452
|
+
candidateIds: preservedSelectionIds,
|
|
1788
2453
|
...(physicalPath.gap ? { gap: physicalPath.gap } : {}),
|
|
1789
2454
|
},
|
|
1790
2455
|
};
|
|
@@ -1797,7 +2462,7 @@ function continueCascadeAfterIncompleteSelection(base, evidence, candidates, sel
|
|
|
1797
2462
|
source: 'heuristic',
|
|
1798
2463
|
category: 'data_lookup',
|
|
1799
2464
|
depth: 'quick',
|
|
1800
|
-
retrievalEvidence: retrievalTrace(evidence,
|
|
2465
|
+
retrievalEvidence: retrievalTrace(evidence, snapshotCandidates),
|
|
1801
2466
|
requiresClarification: false,
|
|
1802
2467
|
resolvedAnalyticalPlan: undefined,
|
|
1803
2468
|
meaningResolution,
|
|
@@ -1857,12 +2522,38 @@ function enforceAnalyticalCompatibility(resolution, evidence, candidates) {
|
|
|
1857
2522
|
const metricEvidence = candidates.filter((candidate) => candidate.kind === "semantic_metric" &&
|
|
1858
2523
|
candidate.analyticalCapability?.metricId &&
|
|
1859
2524
|
metricIds.has(candidate.analyticalCapability.metricId));
|
|
2525
|
+
// Preserve an already validated semantic member when the resulting frame
|
|
2526
|
+
// proves that the selected metric owns that exact dimension. This is how a
|
|
2527
|
+
// server-issued “Account Name” clarification continues alongside the
|
|
2528
|
+
// explicit Revenue metric; stripping it here made trace meaning IDs and
|
|
2529
|
+
// durable continuation state look empty even though the frame was valid.
|
|
2530
|
+
const selectedFrameMembers = resolution.selectedConceptIds.flatMap((id) => {
|
|
2531
|
+
const member = candidates.find((candidate) => candidate.id === id);
|
|
2532
|
+
if (member?.kind !== 'semantic_member')
|
|
2533
|
+
return [];
|
|
2534
|
+
const resolvesIntoFrame = metricEvidence.some((metric) => {
|
|
2535
|
+
const identities = [
|
|
2536
|
+
member.qualifiedId,
|
|
2537
|
+
member.id,
|
|
2538
|
+
member.name,
|
|
2539
|
+
...(member.aliases ?? []),
|
|
2540
|
+
].filter((identity) => Boolean(identity));
|
|
2541
|
+
return identities.some((identity) => {
|
|
2542
|
+
const dimension = resolveMetricCapabilityDimension(metric, identity);
|
|
2543
|
+
return Boolean(dimension && result.frame.dimensions.some((binding) => binding.dimensionId === dimension.dimensionId));
|
|
2544
|
+
});
|
|
2545
|
+
});
|
|
2546
|
+
return resolvesIntoFrame ? [member.id] : [];
|
|
2547
|
+
});
|
|
1860
2548
|
return {
|
|
1861
2549
|
...resolution,
|
|
1862
2550
|
analyticalFrame: result.frame,
|
|
1863
2551
|
analyticalPolicyIds: result.policyIds,
|
|
1864
2552
|
...(metricEvidence.length > 0
|
|
1865
|
-
? { selectedConceptIds:
|
|
2553
|
+
? { selectedConceptIds: [...new Set([
|
|
2554
|
+
...metricEvidence.map((candidate) => candidate.id),
|
|
2555
|
+
...selectedFrameMembers,
|
|
2556
|
+
])] }
|
|
1866
2557
|
: {}),
|
|
1867
2558
|
recommendedExecutionId: result.candidateId,
|
|
1868
2559
|
recommendedRoute: result.route,
|
|
@@ -1904,7 +2595,12 @@ function enforceAnalyticalCompatibility(resolution, evidence, candidates) {
|
|
|
1904
2595
|
analyticalFrame: result.frame,
|
|
1905
2596
|
analyticalPolicyIds: result.policyIds,
|
|
1906
2597
|
confidence: result.status === "clarify" ? "low" : resolution.confidence,
|
|
1907
|
-
|
|
2598
|
+
// A compatibility block is a pre-freeze failure of the nominated tier,
|
|
2599
|
+
// not a new model decision to route somewhere else. Retain the semantic
|
|
2600
|
+
// nomination so the authoritative cascade can inspect the complete
|
|
2601
|
+
// same-snapshot qualified physical closure. Only genuine multi-option
|
|
2602
|
+
// ambiguity changes the route to clarification.
|
|
2603
|
+
recommendedRoute: result.status === 'clarify' ? 'clarify' : resolution.recommendedRoute,
|
|
1908
2604
|
compatibilityOutcome,
|
|
1909
2605
|
compatibilityFailures,
|
|
1910
2606
|
missingInformation: [
|
|
@@ -1981,6 +2677,9 @@ function persistedClarificationSelectionContext(request) {
|
|
|
1981
2677
|
const sourceTurnId = typeof pendingRecord.sourceTurnId === 'string' && pendingRecord.sourceTurnId.trim()
|
|
1982
2678
|
? pendingRecord.sourceTurnId.trim()
|
|
1983
2679
|
: undefined;
|
|
2680
|
+
const sourceQuestion = typeof pendingRecord.sourceQuestion === 'string' && pendingRecord.sourceQuestion.trim()
|
|
2681
|
+
? pendingRecord.sourceQuestion.trim()
|
|
2682
|
+
: undefined;
|
|
1984
2683
|
const threadId = typeof envelope?.threadId === 'string' && envelope.threadId.trim()
|
|
1985
2684
|
? envelope.threadId.trim()
|
|
1986
2685
|
: undefined;
|
|
@@ -2016,6 +2715,7 @@ function persistedClarificationSelectionContext(request) {
|
|
|
2016
2715
|
...(requirements ? { requirements } : {}),
|
|
2017
2716
|
...(snapshotId ? { snapshotId } : {}),
|
|
2018
2717
|
...(sourceTurnId ? { sourceTurnId } : {}),
|
|
2718
|
+
...(sourceQuestion ? { sourceQuestion } : {}),
|
|
2019
2719
|
...(threadId ? { threadId } : {}),
|
|
2020
2720
|
serverIssued: !invalidReason,
|
|
2021
2721
|
...(invalidReason ? { invalidReason } : {}),
|
|
@@ -2100,17 +2800,182 @@ function mergeClarificationRequirements(current, persisted) {
|
|
|
2100
2800
|
entityTerms: [...new Set([...current.entityTerms, ...(persisted.entityTerms ?? [])])],
|
|
2101
2801
|
entityDisplayTerms: [...new Set([...current.entityDisplayTerms, ...(persisted.entityDisplayTerms ?? [])])],
|
|
2102
2802
|
memberTerms: [...new Set([...current.memberTerms, ...(persisted.memberTerms ?? [])])],
|
|
2803
|
+
...(current.ranking || persisted.ranking
|
|
2804
|
+
? {
|
|
2805
|
+
ranking: current.ranking && persisted.ranking
|
|
2806
|
+
? {
|
|
2807
|
+
...current.ranking,
|
|
2808
|
+
metricTerms: [...new Set([...current.ranking.metricTerms, ...persisted.ranking.metricTerms])],
|
|
2809
|
+
entityTerms: [...new Set([...current.ranking.entityTerms, ...persisted.ranking.entityTerms])],
|
|
2810
|
+
}
|
|
2811
|
+
: current.ranking ?? persisted.ranking,
|
|
2812
|
+
}
|
|
2813
|
+
: {}),
|
|
2814
|
+
...(current.time || persisted.time
|
|
2815
|
+
? {
|
|
2816
|
+
time: current.time && persisted.time
|
|
2817
|
+
? {
|
|
2818
|
+
...current.time,
|
|
2819
|
+
grain: current.time.grain ?? persisted.time.grain,
|
|
2820
|
+
fiscalPeriod: current.time.fiscalPeriod ?? persisted.time.fiscalPeriod,
|
|
2821
|
+
requiresDeclaredFiscalCalendar: current.time.requiresDeclaredFiscalCalendar
|
|
2822
|
+
|| persisted.time.requiresDeclaredFiscalCalendar,
|
|
2823
|
+
}
|
|
2824
|
+
: current.time ?? persisted.time,
|
|
2825
|
+
}
|
|
2826
|
+
: {}),
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
/**
|
|
2830
|
+
* A structured choice continues the server-rendered analytical frame. The
|
|
2831
|
+
* browser may echo `clarificationSourceQuestion`, but the persisted server
|
|
2832
|
+
* envelope wins whenever it is available so a choice label cannot be parsed as
|
|
2833
|
+
* a new question on reload or restart.
|
|
2834
|
+
*/
|
|
2835
|
+
function structuredClarificationRequirements(request, persisted) {
|
|
2836
|
+
const sourceQuestion = persisted?.serverIssued && persisted.sourceQuestion
|
|
2837
|
+
? persisted.sourceQuestion
|
|
2838
|
+
: request.clarificationSourceQuestion?.trim() || request.question;
|
|
2839
|
+
return mergeClarificationRequirements(buildAnalyticalRequirementSet({ question: sourceQuestion }), persisted?.serverIssued ? persisted.requirements : undefined);
|
|
2840
|
+
}
|
|
2841
|
+
/**
|
|
2842
|
+
* A dimension clarification is not an alternate definition of the measure.
|
|
2843
|
+
* Find the exact semantic metric that owns the persisted explicit measure
|
|
2844
|
+
* before accepting a server-issued semantic member as a display/grouping
|
|
2845
|
+
* selection. Multiple metrics remain a real ambiguity; we never use a
|
|
2846
|
+
* dimension click to guess one of them.
|
|
2847
|
+
*/
|
|
2848
|
+
function uniqueStructuredSelectionMetric(candidates, requirements) {
|
|
2849
|
+
const measures = explicitMeasureTermsForClarification(requirements);
|
|
2850
|
+
if (measures.length !== 1)
|
|
2851
|
+
return undefined;
|
|
2852
|
+
const byMetricId = new Map();
|
|
2853
|
+
for (const candidate of canonicalizeMetricMeasureCandidates(candidates)) {
|
|
2854
|
+
if (candidate.kind !== 'semantic_metric'
|
|
2855
|
+
|| candidate.eligible === false
|
|
2856
|
+
// The first clarification deliberately exists because the metric's
|
|
2857
|
+
// display/entity role was not yet fully bound. A partial fit can still
|
|
2858
|
+
// be the unique, complete semantic capability for the explicit measure;
|
|
2859
|
+
// after the server-issued dimension choice binds it, the ordinary
|
|
2860
|
+
// immutable-plan compatibility checks remain authoritative. Only a
|
|
2861
|
+
// proven incompatible metric is disqualified at this identity boundary.
|
|
2862
|
+
|| candidate.compatibility === 'incompatible'
|
|
2863
|
+
|| candidateConflictsWithExplicitRankingMeasure(candidate, requirements)
|
|
2864
|
+
|| !candidateProvesMetricTerm(candidate, measures[0]))
|
|
2865
|
+
continue;
|
|
2866
|
+
const normalized = normalizeEvidenceAnalyticalCapability(candidate);
|
|
2867
|
+
if (normalized.status !== 'complete' || !normalized.capability)
|
|
2868
|
+
continue;
|
|
2869
|
+
const current = byMetricId.get(normalized.capability.metricId);
|
|
2870
|
+
if (!current
|
|
2871
|
+
|| candidate.exactMatch && !current.exactMatch
|
|
2872
|
+
|| candidate.relevanceScore > current.relevanceScore) {
|
|
2873
|
+
byMetricId.set(normalized.capability.metricId, candidate);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
return byMetricId.size === 1 ? [...byMetricId.values()][0] : undefined;
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* A resolver can offer a qualified semantic capability dimension without a
|
|
2880
|
+
* standalone retrieval card. That is expected: the option is an authored
|
|
2881
|
+
* child of a metric capability, not an independently ranked candidate. On a
|
|
2882
|
+
* later click, restore that child only when the active server-issued
|
|
2883
|
+
* clarification contract names the exact option and the *same immutable
|
|
2884
|
+
* snapshot* still proves it through one exact metric capability.
|
|
2885
|
+
*
|
|
2886
|
+
* This is deliberately not a fuzzy retrieval fallback. A malformed,
|
|
2887
|
+
* foreign, stale, or client-invented ID never reaches this helper because the
|
|
2888
|
+
* host-only envelope/snapshot checks occur before the capability projection.
|
|
2889
|
+
*/
|
|
2890
|
+
function rehydrateServerIssuedCapabilityDimensionSelection(input) {
|
|
2891
|
+
const selectedId = input.request.selectedEvidenceId?.trim();
|
|
2892
|
+
if (!selectedId)
|
|
2893
|
+
return undefined;
|
|
2894
|
+
const persisted = persistedClarificationSelectionContext(input.request);
|
|
2895
|
+
if (!persisted?.serverIssued || persisted.snapshotId !== input.evidence.snapshotId)
|
|
2896
|
+
return undefined;
|
|
2897
|
+
const offeredIds = new Set([...persisted.optionIds, ...persisted.ambiguityCandidateIds]);
|
|
2898
|
+
if (!offeredIds.has(selectedId))
|
|
2899
|
+
return undefined;
|
|
2900
|
+
const requirements = structuredClarificationRequirements(input.request, persisted);
|
|
2901
|
+
const metricCandidate = uniqueStructuredSelectionMetric(input.candidates, requirements);
|
|
2902
|
+
if (!metricCandidate)
|
|
2903
|
+
return undefined;
|
|
2904
|
+
const dimension = resolveMetricCapabilityDimension(metricCandidate, selectedId);
|
|
2905
|
+
if (!dimension)
|
|
2906
|
+
return undefined;
|
|
2907
|
+
if (requirements.ranking && !dimension.supportedRoles.includes('rank_entity'))
|
|
2908
|
+
return undefined;
|
|
2909
|
+
if (!requirements.ranking
|
|
2910
|
+
&& !dimension.supportedRoles.includes('group_by')
|
|
2911
|
+
&& !dimension.supportedRoles.includes('filter'))
|
|
2912
|
+
return undefined;
|
|
2913
|
+
return {
|
|
2914
|
+
// Keep the exact server-issued ID as the router identity and persistable
|
|
2915
|
+
// choice binding. The metric capability's authored ID stays available as
|
|
2916
|
+
// provenance for semantic-frame construction.
|
|
2917
|
+
id: selectedId,
|
|
2918
|
+
qualifiedId: dimension.dimensionId,
|
|
2919
|
+
kind: 'semantic_member',
|
|
2920
|
+
semanticObjectType: 'dimension',
|
|
2921
|
+
trustTier: 'semantic',
|
|
2922
|
+
name: dimension.label || qualifiedIdLabel(dimension.dimensionId),
|
|
2923
|
+
...(dimension.aliases?.length ? { aliases: dimension.aliases } : {}),
|
|
2924
|
+
...(metricCandidate.domain ? { domain: metricCandidate.domain } : {}),
|
|
2925
|
+
...(metricCandidate.semanticModel ? { semanticModel: metricCandidate.semanticModel } : {}),
|
|
2926
|
+
relevanceScore: metricCandidate.relevanceScore,
|
|
2927
|
+
matchReasons: ['server-issued snapshot capability binding'],
|
|
2928
|
+
compatibility: 'compatible',
|
|
2929
|
+
eligible: true,
|
|
2103
2930
|
};
|
|
2104
2931
|
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Validate a selected display/grouping member against the selected metric's
|
|
2934
|
+
* physical semantic capability. The candidate must already have been offered
|
|
2935
|
+
* by the server; this only determines which typed role it can complete.
|
|
2936
|
+
*/
|
|
2937
|
+
function structuredDimensionSelection(selected, candidates, requirements) {
|
|
2938
|
+
if (selected.kind !== 'semantic_member'
|
|
2939
|
+
|| selected.eligible === false
|
|
2940
|
+
|| selected.compatibility === 'incompatible')
|
|
2941
|
+
return undefined;
|
|
2942
|
+
const roles = evidenceCandidateRoles(selected);
|
|
2943
|
+
if (!roles.some((role) => role === 'entity_label'
|
|
2944
|
+
|| role === 'categorical_dimension'
|
|
2945
|
+
|| role === 'time_dimension'))
|
|
2946
|
+
return undefined;
|
|
2947
|
+
const metricCandidate = uniqueStructuredSelectionMetric(candidates, requirements);
|
|
2948
|
+
if (!metricCandidate)
|
|
2949
|
+
return undefined;
|
|
2950
|
+
const dimension = resolveMetricCapabilityDimension(metricCandidate, selected.qualifiedId ?? selected.id);
|
|
2951
|
+
if (!dimension)
|
|
2952
|
+
return undefined;
|
|
2953
|
+
// A top/bottom clarification must select an authored rankable entity, not a
|
|
2954
|
+
// filter-only field. For ordinary dimensional requests, grouping is enough.
|
|
2955
|
+
if (requirements.ranking && !dimension.supportedRoles.includes('rank_entity'))
|
|
2956
|
+
return undefined;
|
|
2957
|
+
if (!requirements.ranking
|
|
2958
|
+
&& !dimension.supportedRoles.includes('group_by')
|
|
2959
|
+
&& !dimension.supportedRoles.includes('filter'))
|
|
2960
|
+
return undefined;
|
|
2961
|
+
return {
|
|
2962
|
+
kind: 'dimension',
|
|
2963
|
+
metricCandidate,
|
|
2964
|
+
selectedDimensionId: selected.id,
|
|
2965
|
+
dimensionId: dimension.dimensionId,
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
function compatibleStructuredClarificationCandidates(candidates, requirements) {
|
|
2969
|
+
return candidates.filter((candidate) => candidateOwnsExplicitClarificationRoles(candidate, requirements)
|
|
2970
|
+
|| Boolean(structuredDimensionSelection(candidate, candidates, requirements)));
|
|
2971
|
+
}
|
|
2105
2972
|
function validateStructuredClarificationSelection(input) {
|
|
2106
2973
|
const persisted = persistedClarificationSelectionContext(input.request);
|
|
2107
|
-
const sourceQuestion = input.request.clarificationSourceQuestion?.trim() || input.request.question;
|
|
2108
|
-
const requirements = mergeClarificationRequirements(
|
|
2109
2974
|
// A click/reload continuation is an identity action. Use the original user
|
|
2110
|
-
// question
|
|
2111
|
-
//
|
|
2112
|
-
|
|
2113
|
-
const compatible =
|
|
2975
|
+
// question and server-persisted typed contract, not arbitrary inherited
|
|
2976
|
+
// parser hints or the rendered option label.
|
|
2977
|
+
const requirements = structuredClarificationRequirements(input.request, persisted);
|
|
2978
|
+
const compatible = compatibleStructuredClarificationCandidates(input.candidates, requirements);
|
|
2114
2979
|
const choiceIds = compatible.map((candidate) => candidate.id);
|
|
2115
2980
|
if (!persisted?.serverIssued) {
|
|
2116
2981
|
return {
|
|
@@ -2132,7 +2997,10 @@ function validateStructuredClarificationSelection(input) {
|
|
|
2132
2997
|
if (!input.selected) {
|
|
2133
2998
|
return { ok: false, requirements, choiceIds, reason: 'The selected governed identifier is no longer present in the retrieved snapshot.' };
|
|
2134
2999
|
}
|
|
2135
|
-
|
|
3000
|
+
const selection = candidateOwnsExplicitClarificationRoles(input.selected, requirements)
|
|
3001
|
+
? { kind: 'metric' }
|
|
3002
|
+
: structuredDimensionSelection(input.selected, input.candidates, requirements);
|
|
3003
|
+
if (!selection) {
|
|
2136
3004
|
return {
|
|
2137
3005
|
ok: false,
|
|
2138
3006
|
requirements,
|
|
@@ -2161,7 +3029,7 @@ function validateStructuredClarificationSelection(input) {
|
|
|
2161
3029
|
reason: 'The selected governed identifier is incompatible with the current typed requirement set.',
|
|
2162
3030
|
};
|
|
2163
3031
|
}
|
|
2164
|
-
return { ok: true, requirements, choiceIds };
|
|
3032
|
+
return { ok: true, requirements, choiceIds, selection };
|
|
2165
3033
|
}
|
|
2166
3034
|
function invalidStructuredSelectionDecision(input) {
|
|
2167
3035
|
const coverage = sourceCoverageFromEvidence(input.evidence, input.candidates);
|
|
@@ -2411,7 +3279,68 @@ function preventDegenerateRankingResolution(resolution, evidence, candidates, qu
|
|
|
2411
3279
|
: `I found ${selected.name}, but it counts the ranked entity and cannot identify the top individual customers. Which measure should I use for the ranking?`,
|
|
2412
3280
|
};
|
|
2413
3281
|
}
|
|
2414
|
-
function
|
|
3282
|
+
function isSameSnapshotCategoricalExtensionForMetric(candidate, metricCandidate) {
|
|
3283
|
+
const extension = candidate.sameSnapshotRoleExtension;
|
|
3284
|
+
if (!extension
|
|
3285
|
+
|| extension.version !== 1
|
|
3286
|
+
|| extension.role !== 'categorical_dimension'
|
|
3287
|
+
|| (extension.basis !== 'sole_metricflow_grouping_dimension'
|
|
3288
|
+
&& extension.basis !== 'exact_metricflow_grouping_dimension')
|
|
3289
|
+
|| candidate.kind !== 'semantic_member'
|
|
3290
|
+
|| (candidate.qualifiedId ?? candidate.id) !== extension.dimensionId)
|
|
3291
|
+
return false;
|
|
3292
|
+
const metricIds = new Set([
|
|
3293
|
+
metricCandidate.id,
|
|
3294
|
+
metricCandidate.qualifiedId,
|
|
3295
|
+
normalizeEvidenceAnalyticalCapability(metricCandidate).capability?.metricId,
|
|
3296
|
+
].filter((id) => Boolean(id)));
|
|
3297
|
+
if (!metricIds.has(extension.metricId))
|
|
3298
|
+
return false;
|
|
3299
|
+
const capability = normalizeEvidenceAnalyticalCapability(metricCandidate).capability;
|
|
3300
|
+
return Boolean(capability?.dimensions.some((dimension) => dimension.dimensionId === extension.dimensionId
|
|
3301
|
+
&& dimension.supportedRoles.includes('group_by')));
|
|
3302
|
+
}
|
|
3303
|
+
/**
|
|
3304
|
+
* A model chooses from the bounded package, but it cannot remove a unique
|
|
3305
|
+
* host-required categorical grouping whose exact qualified field is already
|
|
3306
|
+
* declared by the selected metric's immutable capability. This is a binding,
|
|
3307
|
+
* not a semantic guess: zero or multiple capability-backed cards leave the
|
|
3308
|
+
* requirement unresolved for the normal clarify/gap path.
|
|
3309
|
+
*/
|
|
3310
|
+
function hostBoundCategoricalExtensions(input) {
|
|
3311
|
+
const bound = [];
|
|
3312
|
+
for (const requested of categoricalDimensionRequirementTerms(input.requirements)) {
|
|
3313
|
+
const matches = input.candidates.filter((candidate) => isSameSnapshotCategoricalExtensionForMetric(candidate, input.metricCandidate)
|
|
3314
|
+
&& normalizeMetricPhrase(candidate.sameSnapshotRoleExtension.requestedTerm)
|
|
3315
|
+
=== normalizeMetricPhrase(requested));
|
|
3316
|
+
if (matches.length === 1)
|
|
3317
|
+
bound.push(matches[0]);
|
|
3318
|
+
}
|
|
3319
|
+
return bound.filter((candidate, index, all) => all.findIndex((item) => item.id === candidate.id) === index);
|
|
3320
|
+
}
|
|
3321
|
+
function directResolution(request, evidence, candidate, candidates, selectedDimensionIds = [], selectedDimensionConceptIds = selectedDimensionIds) {
|
|
3322
|
+
// Keep deterministic/direct routing on the same host-owned tuple as the
|
|
3323
|
+
// model path. Raw parser terms are retrieval hints only: they may not turn a
|
|
3324
|
+
// normalized business alias back into a phantom metric or dimension after
|
|
3325
|
+
// the meaning boundary has been intentionally skipped.
|
|
3326
|
+
const requirementSeed = buildAnalyticalRequirementSeedV1({
|
|
3327
|
+
question: request.question,
|
|
3328
|
+
parsedIntent: evidence.parsedIntent,
|
|
3329
|
+
fiscalCalendar: declaredFiscalCalendar(evidence, candidates),
|
|
3330
|
+
});
|
|
3331
|
+
const hostOwnedEvidence = {
|
|
3332
|
+
...evidence,
|
|
3333
|
+
parsedIntent: {
|
|
3334
|
+
...evidence.parsedIntent,
|
|
3335
|
+
measures: requirementSeed.queryIntent.measures,
|
|
3336
|
+
dimensions: requirementSeed.queryIntent.dimensions,
|
|
3337
|
+
filters: requirementSeed.queryIntent.filters,
|
|
3338
|
+
...(requirementSeed.queryIntent.timeRange ? { timeRange: requirementSeed.queryIntent.timeRange } : {}),
|
|
3339
|
+
...(requirementSeed.queryIntent.timeGrain ? { timeGrain: requirementSeed.queryIntent.timeGrain } : {}),
|
|
3340
|
+
...(requirementSeed.queryIntent.order ? { order: requirementSeed.queryIntent.order } : {}),
|
|
3341
|
+
...(requirementSeed.queryIntent.limit !== undefined ? { limit: requirementSeed.queryIntent.limit } : {}),
|
|
3342
|
+
},
|
|
3343
|
+
};
|
|
2415
3344
|
const inferredQuestionType = questionTypeFromText(request.question);
|
|
2416
3345
|
const questionType = inferredQuestionType === 'definition'
|
|
2417
3346
|
&& candidate.kind === 'semantic_metric'
|
|
@@ -2421,16 +3350,40 @@ function directResolution(request, evidence, candidate, candidates) {
|
|
|
2421
3350
|
&& !/\b(?:define|definition|meaning|mean)\b/i.test(request.question)
|
|
2422
3351
|
? 'value'
|
|
2423
3352
|
: inferredQuestionType;
|
|
2424
|
-
const metricCandidates = explicitlyRequestedMetricCandidates(request.question,
|
|
3353
|
+
const metricCandidates = explicitlyRequestedMetricCandidates(request.question, hostOwnedEvidence, candidate, candidates);
|
|
3354
|
+
const requirements = requirementSeed.requirements;
|
|
3355
|
+
const hostBoundExtensions = hostBoundCategoricalExtensions({
|
|
3356
|
+
metricCandidate: candidate,
|
|
3357
|
+
candidates,
|
|
3358
|
+
requirements,
|
|
3359
|
+
});
|
|
3360
|
+
const resolvedSelectedDimensionIds = [...new Set([
|
|
3361
|
+
...selectedDimensionIds,
|
|
3362
|
+
...hostBoundExtensions.map((item) => item.qualifiedId ?? item.id),
|
|
3363
|
+
])];
|
|
2425
3364
|
const analyticalFrame = buildDeterministicAnalyticalFrame({
|
|
2426
3365
|
question: request.question,
|
|
2427
3366
|
questionType,
|
|
2428
|
-
evidence,
|
|
3367
|
+
evidence: hostOwnedEvidence,
|
|
2429
3368
|
metricCandidate: candidate,
|
|
2430
3369
|
metricCandidates,
|
|
3370
|
+
entityTerms: requirements.entityTerms,
|
|
3371
|
+
entityDisplayTerms: requirements.entityDisplayTerms,
|
|
3372
|
+
selectedDimensionIds: resolvedSelectedDimensionIds,
|
|
2431
3373
|
candidates,
|
|
2432
3374
|
});
|
|
2433
|
-
const defaultIntent = defaultQueryIntent(
|
|
3375
|
+
const defaultIntent = defaultQueryIntent(hostOwnedEvidence);
|
|
3376
|
+
// `top` without a count is a deterministic product convention, not a
|
|
3377
|
+
// missing business meaning. Bind the default into the direct execution
|
|
3378
|
+
// nomination while the typed requirements retain `defaultedLimit: true` for
|
|
3379
|
+
// the answer/receipt to disclose the assumption.
|
|
3380
|
+
const rankedDefaultIntent = requirements.ranking
|
|
3381
|
+
? {
|
|
3382
|
+
...defaultIntent,
|
|
3383
|
+
order: defaultIntent.order ?? (requirements.ranking.direction === 'bottom' ? 'asc' : 'desc'),
|
|
3384
|
+
limit: defaultIntent.limit ?? requirements.ranking.limit,
|
|
3385
|
+
}
|
|
3386
|
+
: defaultIntent;
|
|
2434
3387
|
// An exact authored certified example has already proved this block's own
|
|
2435
3388
|
// output contract. Parser wording such as "food and drink" describes the
|
|
2436
3389
|
// values of the block's declared `category` output; it must not manufacture
|
|
@@ -2444,15 +3397,45 @@ function directResolution(request, evidence, candidate, candidates) {
|
|
|
2444
3397
|
&& certifiedCandidateExplicitlyCoversMeasures(candidate, defaultIntent.measures)
|
|
2445
3398
|
&& (candidate.dimensions?.length ?? 0) > 0;
|
|
2446
3399
|
const queryIntent = exactCertifiedExample
|
|
2447
|
-
? { ...
|
|
2448
|
-
:
|
|
3400
|
+
? { ...rankedDefaultIntent, dimensions: candidate.dimensions ?? [] }
|
|
3401
|
+
: rankedDefaultIntent;
|
|
3402
|
+
// Preserve a selected semantic field as a typed dimension binding rather
|
|
3403
|
+
// than folding its label into the question. The ID comes from the selected
|
|
3404
|
+
// metric's capability contract, so the immutable plan will revalidate it
|
|
3405
|
+
// against the same snapshot before it can freeze.
|
|
3406
|
+
const selectedQueryDimensions = analyticalFrame?.dimensions
|
|
3407
|
+
// Server-issued structured selections may add their qualified identity to
|
|
3408
|
+
// the intent carrier. A host-bound same-snapshot extension instead binds
|
|
3409
|
+
// through the V2 frame below; adding its ID as a second text dimension
|
|
3410
|
+
// would create a duplicate request (`region` plus its qualified field).
|
|
3411
|
+
.filter((binding) => selectedDimensionIds.includes(binding.dimensionId))
|
|
3412
|
+
.map((binding) => binding.dimensionId) ?? [];
|
|
3413
|
+
// A direct host resolution can bind a categorical capability dimension from
|
|
3414
|
+
// the current question without a model-selected member card. Carry the
|
|
3415
|
+
// *actual* same-snapshot card into the resolution as well, otherwise the
|
|
3416
|
+
// immutable solver sees a valid V2 frame but cannot prove the selected
|
|
3417
|
+
// MetricFlow grouping evidence and incorrectly marks the tuple partial.
|
|
3418
|
+
// This is not a fuzzy name lookup: the candidate must be the dimension's
|
|
3419
|
+
// own qualified identity or the already-recorded same-snapshot extension.
|
|
3420
|
+
const hostBoundDimensionConceptIds = analyticalFrame?.dimensions.flatMap((binding) => hostBoundExtensions
|
|
3421
|
+
.filter((item) => (item.qualifiedId ?? item.id) === binding.dimensionId)
|
|
3422
|
+
.map((item) => item.id)) ?? [];
|
|
3423
|
+
const queryIntentWithSelectedDimensions = selectedQueryDimensions.length > 0
|
|
3424
|
+
? {
|
|
3425
|
+
...queryIntent,
|
|
3426
|
+
dimensions: [...new Set([
|
|
3427
|
+
...queryIntent.dimensions,
|
|
3428
|
+
...selectedQueryDimensions,
|
|
3429
|
+
])],
|
|
3430
|
+
}
|
|
3431
|
+
: queryIntent;
|
|
2449
3432
|
const memberCandidates = candidates.filter((item) => {
|
|
2450
3433
|
if (item.kind !== 'semantic_member' || item.compatibility === 'incompatible')
|
|
2451
3434
|
return false;
|
|
2452
3435
|
const identities = [item.name, ...(item.aliases ?? [])].map(normalizeMetricPhrase).filter(Boolean);
|
|
2453
|
-
return
|
|
3436
|
+
return queryIntentWithSelectedDimensions.filters.some((filter) => identities.includes(normalizeMetricPhrase(filter.value)));
|
|
2454
3437
|
});
|
|
2455
|
-
const canonicalFilters =
|
|
3438
|
+
const canonicalFilters = queryIntentWithSelectedDimensions.filters.map((filter) => {
|
|
2456
3439
|
const member = memberCandidates.find((item) => [item.name, ...(item.aliases ?? [])]
|
|
2457
3440
|
.map(normalizeMetricPhrase)
|
|
2458
3441
|
.includes(normalizeMetricPhrase(filter.value)));
|
|
@@ -2461,18 +3444,138 @@ function directResolution(request, evidence, candidate, candidates) {
|
|
|
2461
3444
|
return {
|
|
2462
3445
|
interpretedQuestion: request.question,
|
|
2463
3446
|
questionType,
|
|
2464
|
-
selectedConceptIds: [...metricCandidates, ...memberCandidates]
|
|
3447
|
+
selectedConceptIds: [...metricCandidates, ...memberCandidates]
|
|
3448
|
+
.map((item) => item.id)
|
|
3449
|
+
.concat(selectedDimensionConceptIds, hostBoundDimensionConceptIds)
|
|
3450
|
+
.filter((id, index, all) => all.indexOf(id) === index),
|
|
2465
3451
|
recommendedExecutionId: candidate.id,
|
|
2466
|
-
queryIntent: { ...
|
|
3452
|
+
queryIntent: { ...queryIntentWithSelectedDimensions, filters: canonicalFilters },
|
|
2467
3453
|
rejectedCandidates: [],
|
|
2468
3454
|
confidence: "high",
|
|
2469
3455
|
missingInformation: [],
|
|
2470
3456
|
recommendedRoute: routeForEvidenceCandidate(candidate),
|
|
3457
|
+
// Direct/exact routing deliberately bypasses the model, not the host
|
|
3458
|
+
// meaning boundary. Carry the same immutable seed into plan binding so
|
|
3459
|
+
// display-key projection and downstream receipts cannot fall back to raw
|
|
3460
|
+
// parser wording.
|
|
3461
|
+
hostRequirementSeed: requirementSeed,
|
|
3462
|
+
...(selectedDimensionIds.length > 0
|
|
3463
|
+
? { structuredDimensionIds: [...new Set(selectedDimensionIds)] }
|
|
3464
|
+
: {}),
|
|
2471
3465
|
...(analyticalFrame ? { analyticalFrame } : {}),
|
|
2472
3466
|
};
|
|
2473
3467
|
}
|
|
3468
|
+
/**
|
|
3469
|
+
* Build the V2 execution frame after the one candidate-ID meaning call.
|
|
3470
|
+
*
|
|
3471
|
+
* The model's response has already passed package membership validation. It
|
|
3472
|
+
* may therefore identify a supplied semantic metric and supplied semantic
|
|
3473
|
+
* members, but it is never allowed to author the frame itself. This helper
|
|
3474
|
+
* rebuilds the frame from the immutable host seed plus those qualified
|
|
3475
|
+
* identities, exactly as {@link directResolution} does for a zero-call path.
|
|
3476
|
+
*
|
|
3477
|
+
* Keeping this boundary here is important: a candidate-only model response
|
|
3478
|
+
* used to bind a perfectly valid semantic tuple into a V1 plan. V1 plans
|
|
3479
|
+
* bypass the immutable analytical execution graph and fall into the legacy
|
|
3480
|
+
* semantic SQL path, where generic aggregation validation cannot prove a
|
|
3481
|
+
* MetricFlow capability. A host-built V2 frame keeps the selected semantic
|
|
3482
|
+
* route compiler-owned without letting the model change the requested tuple.
|
|
3483
|
+
*/
|
|
3484
|
+
function attachHostOwnedAnalyticalFrame(input) {
|
|
3485
|
+
const { request, evidence, candidates, requirementSeed, resolution } = input;
|
|
3486
|
+
if (resolution.recommendedRoute !== 'semantic' || resolution.questionType === 'definition') {
|
|
3487
|
+
return resolution;
|
|
3488
|
+
}
|
|
3489
|
+
const matchesIdentity = (candidate, identity) => Boolean(identity && (candidate.id === identity || candidate.qualifiedId === identity));
|
|
3490
|
+
const metricCandidate = candidates.find((candidate) => candidate.kind === 'semantic_metric'
|
|
3491
|
+
&& matchesIdentity(candidate, resolution.recommendedExecutionId))
|
|
3492
|
+
?? candidates.find((candidate) => candidate.kind === 'semantic_metric'
|
|
3493
|
+
&& resolution.selectedConceptIds.some((identity) => matchesIdentity(candidate, identity)));
|
|
3494
|
+
if (!metricCandidate || normalizeEvidenceAnalyticalCapability(metricCandidate).status !== 'complete') {
|
|
3495
|
+
return resolution;
|
|
3496
|
+
}
|
|
3497
|
+
// Parsed intent is retrieval evidence only. The deterministic frame reads
|
|
3498
|
+
// the same host-owned seed that the plan will later bind, so stale retrieval
|
|
3499
|
+
// terms cannot re-enter as a metric, dimension, filter, output, or time
|
|
3500
|
+
// constraint merely because the model selected an otherwise valid card.
|
|
3501
|
+
const hostOwnedEvidence = {
|
|
3502
|
+
...evidence,
|
|
3503
|
+
parsedIntent: {
|
|
3504
|
+
...evidence.parsedIntent,
|
|
3505
|
+
measures: [...requirementSeed.queryIntent.measures],
|
|
3506
|
+
dimensions: [...requirementSeed.queryIntent.dimensions],
|
|
3507
|
+
filters: requirementSeed.queryIntent.filters.map((filter) => ({ ...filter })),
|
|
3508
|
+
...(requirementSeed.queryIntent.timeRange
|
|
3509
|
+
? { timeRange: requirementSeed.queryIntent.timeRange }
|
|
3510
|
+
: {}),
|
|
3511
|
+
...(requirementSeed.queryIntent.timeGrain
|
|
3512
|
+
? { timeGrain: requirementSeed.queryIntent.timeGrain }
|
|
3513
|
+
: {}),
|
|
3514
|
+
...(requirementSeed.queryIntent.order
|
|
3515
|
+
? { order: requirementSeed.queryIntent.order }
|
|
3516
|
+
: {}),
|
|
3517
|
+
...(requirementSeed.queryIntent.limit !== undefined
|
|
3518
|
+
? { limit: requirementSeed.queryIntent.limit }
|
|
3519
|
+
: {}),
|
|
3520
|
+
},
|
|
3521
|
+
};
|
|
3522
|
+
const modelSelectedDimensionIds = resolution.selectedConceptIds.flatMap((identity) => {
|
|
3523
|
+
const candidate = candidates.find((item) => matchesIdentity(item, identity));
|
|
3524
|
+
return candidate?.kind === 'semantic_member'
|
|
3525
|
+
? [candidate.qualifiedId ?? candidate.id]
|
|
3526
|
+
: [];
|
|
3527
|
+
});
|
|
3528
|
+
const hostBoundExtensions = hostBoundCategoricalExtensions({
|
|
3529
|
+
metricCandidate,
|
|
3530
|
+
candidates,
|
|
3531
|
+
requirements: requirementSeed.requirements,
|
|
3532
|
+
});
|
|
3533
|
+
const selectedDimensionIds = [...new Set([
|
|
3534
|
+
...modelSelectedDimensionIds,
|
|
3535
|
+
...hostBoundExtensions.map((candidate) => candidate.qualifiedId ?? candidate.id),
|
|
3536
|
+
])];
|
|
3537
|
+
const analyticalFrame = buildDeterministicAnalyticalFrame({
|
|
3538
|
+
question: requirementSeed.sourceQuestion,
|
|
3539
|
+
questionType: resolution.questionType,
|
|
3540
|
+
evidence: hostOwnedEvidence,
|
|
3541
|
+
metricCandidate,
|
|
3542
|
+
metricCandidates: explicitlyRequestedMetricCandidates(requirementSeed.sourceQuestion, hostOwnedEvidence, metricCandidate, candidates),
|
|
3543
|
+
entityTerms: requirementSeed.requirements.entityTerms,
|
|
3544
|
+
entityDisplayTerms: requirementSeed.requirements.entityDisplayTerms,
|
|
3545
|
+
selectedDimensionIds,
|
|
3546
|
+
candidates,
|
|
3547
|
+
});
|
|
3548
|
+
if (!analyticalFrame)
|
|
3549
|
+
return resolution;
|
|
3550
|
+
const hostBoundConceptIds = hostBoundExtensions.map((candidate) => candidate.id);
|
|
3551
|
+
const selectedConceptIds = [...new Set([
|
|
3552
|
+
...resolution.selectedConceptIds,
|
|
3553
|
+
...hostBoundConceptIds,
|
|
3554
|
+
])];
|
|
3555
|
+
const overrideReceipts = hostBoundConceptIds.length > 0
|
|
3556
|
+
? [
|
|
3557
|
+
...(resolution.overrideReceipts ?? []),
|
|
3558
|
+
{
|
|
3559
|
+
version: 1,
|
|
3560
|
+
field: 'candidate_selection',
|
|
3561
|
+
action: 'host_preserved',
|
|
3562
|
+
reason: 'A unique, same-snapshot MetricFlow grouping field is required by the host-owned categorical dimension and cannot be removed by a model omission.',
|
|
3563
|
+
candidateIds: hostBoundConceptIds,
|
|
3564
|
+
},
|
|
3565
|
+
]
|
|
3566
|
+
: resolution.overrideReceipts;
|
|
3567
|
+
return {
|
|
3568
|
+
...resolution,
|
|
3569
|
+
selectedConceptIds,
|
|
3570
|
+
analyticalFrame,
|
|
3571
|
+
...(overrideReceipts?.length ? { overrideReceipts } : {}),
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
2474
3574
|
function explicitlyRequestedMetricCandidates(question, evidence, primary, candidates) {
|
|
2475
|
-
const
|
|
3575
|
+
const requirements = buildAnalyticalRequirementSet({ question, parsedIntent: evidence.parsedIntent });
|
|
3576
|
+
const requested = requirements.ranking?.metricTerms.length
|
|
3577
|
+
? requirements.ranking.metricTerms
|
|
3578
|
+
: requirements.measures;
|
|
2476
3579
|
const requestedTerms = [...new Set(requested.map(normalizeMetricPhrase).filter(Boolean))];
|
|
2477
3580
|
// A single business metric can be retrieved alongside technical dbt measure
|
|
2478
3581
|
// shims and registry aliases that share its words. Those are execution
|
|
@@ -2489,6 +3592,8 @@ function explicitlyRequestedMetricCandidates(question, evidence, primary, candid
|
|
|
2489
3592
|
const metrics = candidates.filter((candidate) => {
|
|
2490
3593
|
if (candidate.kind !== 'semantic_metric' || candidate.compatibility === 'incompatible')
|
|
2491
3594
|
return false;
|
|
3595
|
+
if (candidateConflictsWithExplicitRankingMeasure(candidate, requirements))
|
|
3596
|
+
return false;
|
|
2492
3597
|
if (!normalizeEvidenceAnalyticalCapability(candidate).capability)
|
|
2493
3598
|
return false;
|
|
2494
3599
|
if (candidate.id === primary.id)
|
|
@@ -2607,6 +3712,29 @@ mayAssumeInterpretation = true) {
|
|
|
2607
3712
|
}
|
|
2608
3713
|
return unresolvedAnalyticalPlanDecision(base, evidence, candidates, request.question);
|
|
2609
3714
|
}
|
|
3715
|
+
/**
|
|
3716
|
+
* A bounded meaning call may return malformed JSON or a syntactically valid
|
|
3717
|
+
* low-confidence response with no candidate binding. It must not override a
|
|
3718
|
+
* plan the host can prove from one exact current-turn semantic identity. The
|
|
3719
|
+
* predicate deliberately requires a frozen executable plan: ordinary metric
|
|
3720
|
+
* ambiguity, a coverage gap, policy denial, and every unfrozen candidate still
|
|
3721
|
+
* take their existing clarification/block paths.
|
|
3722
|
+
*/
|
|
3723
|
+
function isFrozenExecutableHostFallback(decision) {
|
|
3724
|
+
return decision.analyticalCascadeDecision?.planFrozen === true
|
|
3725
|
+
&& Boolean(decision.resolvedAnalyticalPlan)
|
|
3726
|
+
&& !decision.requiresClarification
|
|
3727
|
+
&& !decision.terminalOutcome;
|
|
3728
|
+
}
|
|
3729
|
+
function meaningResolutionHasNoBinding(resolution) {
|
|
3730
|
+
return resolution.selectedConceptIds.length === 0
|
|
3731
|
+
&& !resolution.recommendedExecutionId
|
|
3732
|
+
&& (resolution.emptyCandidateBinding === true || (!resolution.clarifyingQuestion
|
|
3733
|
+
&& resolution.recommendedRoute !== 'clarify'
|
|
3734
|
+
&& resolution.compatibilityOutcome !== 'clarify'
|
|
3735
|
+
&& resolution.missingInformation.length === 0
|
|
3736
|
+
&& resolution.rejectedCandidates.length === 0));
|
|
3737
|
+
}
|
|
2610
3738
|
/** Leaf identity of a governed candidate, ignoring its source qualification. */
|
|
2611
3739
|
function candidateLeafName(candidate) {
|
|
2612
3740
|
const identity = candidate.qualifiedId ?? candidate.id;
|
|
@@ -2919,7 +4047,11 @@ function deterministicPrePlanClarification(request, base, evidence, candidates)
|
|
|
2919
4047
|
// requires a declared calendar at compilation time; this router simply must
|
|
2920
4048
|
// not manufacture one or report an absence before relational/runtime context
|
|
2921
4049
|
// has had a chance to bind the declared date role.
|
|
2922
|
-
|
|
4050
|
+
// The host-owned requirements are the canonical reader tuple. Parsed intent
|
|
4051
|
+
// is only a retrieval seed: carrying its raw phrase here made "sales based
|
|
4052
|
+
// on the region" look like a literal physical dimension even after the
|
|
4053
|
+
// requirement seed had correctly normalized it to `region`.
|
|
4054
|
+
const requestedDimensions = requirements.dimensions
|
|
2923
4055
|
.filter((dimension) => !/^(?:date|day|week|month|quarter|year|fiscal year|fy\d{2,4})$/.test(dimension));
|
|
2924
4056
|
const modeledFilterFields = new Set((evidence.parsedIntent?.filters ?? []).flatMap((filter) => candidates.some((candidate) => isCompatibleQualifiedMember(candidate)
|
|
2925
4057
|
&& candidateIdentityTerms(candidate).some((term) => metricTermsMatch(term, normalizeMetricPhrase(filter.value))))
|
|
@@ -2984,6 +4116,16 @@ function deterministicPrePlanClarification(request, base, evidence, candidates)
|
|
|
2984
4116
|
messagePrefix: `The certified and semantic candidates did not prove ${requestedLabel}.${temporalNote}`,
|
|
2985
4117
|
});
|
|
2986
4118
|
}
|
|
4119
|
+
// A single declared alternative can only be assumed when an executable
|
|
4120
|
+
// metric capability in this same snapshot explicitly contains it. A bare
|
|
4121
|
+
// standalone semantic member (for example `location_name` marked as an
|
|
4122
|
+
// alternative for `region`) is evidence of a possible wording, not proof
|
|
4123
|
+
// that it can safely answer this tuple. Without that closure, retain the
|
|
4124
|
+
// concise modeled-gap clarification rather than letting a later meaning
|
|
4125
|
+
// path substitute an unrelated field or metric.
|
|
4126
|
+
if (alternatives.length === 1
|
|
4127
|
+
&& declaredDimensionAlternativeCompletesExecutableTuple(alternatives[0], candidates, requirements))
|
|
4128
|
+
return undefined;
|
|
2987
4129
|
const requestedLabel = missingDimensions.map((term) => `“${term}”`).join(' and ');
|
|
2988
4130
|
const alternativeLabels = alternatives.map(renderCandidateChoice);
|
|
2989
4131
|
return {
|
|
@@ -3009,6 +4151,22 @@ function deterministicPrePlanClarification(request, base, evidence, candidates)
|
|
|
3009
4151
|
}
|
|
3010
4152
|
return undefined;
|
|
3011
4153
|
}
|
|
4154
|
+
/**
|
|
4155
|
+
* The broad router terminal witness can carry reader-safe missing-role prose
|
|
4156
|
+
* for several gap classes. Observability persists only the relationship
|
|
4157
|
+
* variant, and only as an enumerated proof requirement, so an export cannot
|
|
4158
|
+
* accidentally turn an unresolved question term into trace content.
|
|
4159
|
+
*/
|
|
4160
|
+
function cascadeTerminalRelationshipGap(gap) {
|
|
4161
|
+
if (!gap || gap.code !== 'MISSING_RELATIONSHIP')
|
|
4162
|
+
return undefined;
|
|
4163
|
+
return {
|
|
4164
|
+
version: 1,
|
|
4165
|
+
code: 'MISSING_RELATIONSHIP',
|
|
4166
|
+
requirement: 'certified_relationship_or_allocation_proof',
|
|
4167
|
+
witnessCandidateIds: [...new Set(gap.witnessCandidateIds)].sort().slice(0, 32),
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
3012
4170
|
/**
|
|
3013
4171
|
* A clarification with NO selectable options is unanswerable, and asking it is a
|
|
3014
4172
|
* dead end rather than a safety measure.
|
|
@@ -3249,10 +4407,73 @@ function candidateProvesMetricTerm(candidate, requested) {
|
|
|
3249
4407
|
.some((term) => metricTermsMatch(term, requested));
|
|
3250
4408
|
}
|
|
3251
4409
|
function candidateIsDeclaredDimensionAlternative(candidate, missingDimensions) {
|
|
4410
|
+
const extension = candidate.sameSnapshotRoleExtension;
|
|
4411
|
+
if (extension?.role === 'categorical_dimension'
|
|
4412
|
+
&& (extension.basis === 'sole_metricflow_grouping_dimension'
|
|
4413
|
+
|| extension.basis === 'exact_metricflow_grouping_dimension')
|
|
4414
|
+
&& missingDimensions.some((requested) => normalizeMetricPhrase(requested) === normalizeMetricPhrase(extension.requestedTerm)))
|
|
4415
|
+
return true;
|
|
3252
4416
|
const facts = candidate.compatibilityFacts?.map(normalizeMetricPhrase) ?? [];
|
|
3253
4417
|
return missingDimensions.some((requested) => facts.includes(`alternative for ${requested}`)
|
|
3254
4418
|
|| facts.includes(`dimension alternative for ${requested}`));
|
|
3255
4419
|
}
|
|
4420
|
+
function declaredDimensionAlternativeCompletesExecutableTuple(alternative, candidates, requirements) {
|
|
4421
|
+
// A substitution is executable only when a metric capability in the same
|
|
4422
|
+
// snapshot names this exact qualified dimension. Do not treat a lone
|
|
4423
|
+
// semantic-member card as an authorization to change the reader's request.
|
|
4424
|
+
if (requirements.measures.length === 0)
|
|
4425
|
+
return false;
|
|
4426
|
+
const alternativeId = alternative.qualifiedId ?? alternative.id;
|
|
4427
|
+
return candidates.some((candidate) => {
|
|
4428
|
+
if (candidate.kind !== 'semantic_metric' || candidate.compatibility === 'incompatible')
|
|
4429
|
+
return false;
|
|
4430
|
+
if (!requirements.measures.every((measure) => candidateProvesMetricTerm(candidate, measure)))
|
|
4431
|
+
return false;
|
|
4432
|
+
const normalized = normalizeEvidenceAnalyticalCapability(candidate);
|
|
4433
|
+
return normalized.status === 'complete'
|
|
4434
|
+
&& Boolean(normalized.capability?.dimensions.some((dimension) => dimension.dimensionId === alternativeId));
|
|
4435
|
+
});
|
|
4436
|
+
}
|
|
4437
|
+
/**
|
|
4438
|
+
* A declared dimension alternative is safe only after the immutable plan has
|
|
4439
|
+
* bound that exact qualified field. This makes the one permitted vocabulary
|
|
4440
|
+
* substitution visible to the reader without letting a raw parser phrase or
|
|
4441
|
+
* model response authorize it.
|
|
4442
|
+
*/
|
|
4443
|
+
function assumptionsForDeclaredDimensionAlternatives(input) {
|
|
4444
|
+
const requirements = buildAnalyticalRequirementSet({
|
|
4445
|
+
question: input.question,
|
|
4446
|
+
parsedIntent: input.evidence.parsedIntent,
|
|
4447
|
+
});
|
|
4448
|
+
const boundDimensionIds = new Set(input.plan.query.dimensions
|
|
4449
|
+
.filter((binding) => binding.status === 'resolved' && binding.qualifiedId)
|
|
4450
|
+
.map((binding) => binding.qualifiedId));
|
|
4451
|
+
const assumptions = [];
|
|
4452
|
+
for (const requested of requirements.dimensions) {
|
|
4453
|
+
const alternatives = input.candidates
|
|
4454
|
+
.filter(isCompatibleQualifiedMember)
|
|
4455
|
+
.filter((candidate) => candidateIsDeclaredDimensionAlternative(candidate, [requested]))
|
|
4456
|
+
.filter((candidate) => boundDimensionIds.has(candidate.qualifiedId ?? candidate.id));
|
|
4457
|
+
if (alternatives.length !== 1)
|
|
4458
|
+
continue;
|
|
4459
|
+
const alternative = alternatives[0];
|
|
4460
|
+
const extension = alternative.sameSnapshotRoleExtension;
|
|
4461
|
+
const assumption = assumeDominantCandidate({
|
|
4462
|
+
about: 'dimension',
|
|
4463
|
+
candidates: [{
|
|
4464
|
+
id: alternative.qualifiedId ?? alternative.id,
|
|
4465
|
+
label: alternative.name,
|
|
4466
|
+
score: alternative.relevanceScore,
|
|
4467
|
+
}],
|
|
4468
|
+
because: (chosen) => extension
|
|
4469
|
+
? `“${requested}” is bound to ${chosen.label ?? chosen.id}, the sole same-snapshot MetricFlow grouping field shared by the selected metric.`
|
|
4470
|
+
: `“${requested}” is not modeled directly; ${chosen.label ?? chosen.id} is the sole compatible, declared alternative in this snapshot.`,
|
|
4471
|
+
});
|
|
4472
|
+
if (assumption)
|
|
4473
|
+
assumptions.push(assumption);
|
|
4474
|
+
}
|
|
4475
|
+
return assumptions;
|
|
4476
|
+
}
|
|
3256
4477
|
/**
|
|
3257
4478
|
* A missing optional parsed-intent projection must not erase positive metric
|
|
3258
4479
|
* evidence already retrieved for the question. Only complete, compatible,
|
|
@@ -3350,12 +4571,50 @@ function renderCandidateChoice(candidate) {
|
|
|
3350
4571
|
* metrics therefore remain ambiguous and continue to clarification/resolution.
|
|
3351
4572
|
*/
|
|
3352
4573
|
function uniqueExecutableSemanticMetric(evidence, candidates) {
|
|
3353
|
-
const
|
|
4574
|
+
const requestedMeasures = [...new Set((evidence.parsedIntent?.measures ?? [])
|
|
4575
|
+
.map(normalizeMetricPhrase)
|
|
4576
|
+
.filter(Boolean))];
|
|
4577
|
+
const requestedTokens = metricTokens(requestedMeasures);
|
|
3354
4578
|
if (requestedTokens.size === 0)
|
|
3355
4579
|
return undefined;
|
|
4580
|
+
// Do this before capability normalization. Local metadata can retain an
|
|
4581
|
+
// executable semantic metric card while its pre-plan compatibility hint is
|
|
4582
|
+
// still partial/unknown; a selected provider binding correctly reaches the
|
|
4583
|
+
// capability solver in that state. The exact leaf identity is nevertheless
|
|
4584
|
+
// a real, qualified current-snapshot meaning. Dedupe only representations
|
|
4585
|
+
// of the same qualified metric, never two same-named metrics from different
|
|
4586
|
+
// models, and let the immutable cascade prove execution afterwards.
|
|
4587
|
+
if (requestedMeasures.length === 1) {
|
|
4588
|
+
const requested = requestedMeasures[0];
|
|
4589
|
+
const exactByMetricIdentity = new Map();
|
|
4590
|
+
for (const candidate of candidates) {
|
|
4591
|
+
if (candidate.kind !== 'semantic_metric'
|
|
4592
|
+
|| candidate.compatibility === 'incompatible'
|
|
4593
|
+
|| candidate.eligible === false
|
|
4594
|
+
|| !candidateMetricLeafIdentities(candidate).includes(requested))
|
|
4595
|
+
continue;
|
|
4596
|
+
const normalized = normalizeEvidenceAnalyticalCapability(candidate);
|
|
4597
|
+
const metricIdentity = normalized.capability?.metricId
|
|
4598
|
+
?? candidate.qualifiedId
|
|
4599
|
+
?? candidate.id;
|
|
4600
|
+
const current = exactByMetricIdentity.get(metricIdentity);
|
|
4601
|
+
if (!current || candidate.relevanceScore > current.relevanceScore) {
|
|
4602
|
+
exactByMetricIdentity.set(metricIdentity, candidate);
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
if (exactByMetricIdentity.size === 1) {
|
|
4606
|
+
return [...exactByMetricIdentity.values()][0];
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
3356
4609
|
const byMetricId = new Map();
|
|
3357
4610
|
for (const candidate of candidates) {
|
|
3358
|
-
|
|
4611
|
+
// Retrieval compatibility is a pre-plan hint. A current snapshot can mark
|
|
4612
|
+
// a MetricFlow metric `unknown`/`partial` until its selected grouping is
|
|
4613
|
+
// bound, even though the capability itself is complete. The direct route
|
|
4614
|
+
// below still solves the full tuple before freeze, so exclude only an
|
|
4615
|
+
// explicitly incompatible metric here rather than turning exact `revenue`
|
|
4616
|
+
// into an artificial choice against `product_revenue`.
|
|
4617
|
+
if (candidate.kind !== "semantic_metric" || candidate.compatibility === "incompatible")
|
|
3359
4618
|
continue;
|
|
3360
4619
|
if (/\bdbt\s+measure\b/i.test(candidate.provenance ?? ""))
|
|
3361
4620
|
continue;
|
|
@@ -3374,8 +4633,33 @@ function uniqueExecutableSemanticMetric(evidence, candidates) {
|
|
|
3374
4633
|
byMetricId.set(normalized.capability.metricId, candidate);
|
|
3375
4634
|
}
|
|
3376
4635
|
}
|
|
4636
|
+
// A malformed/empty candidate-ID meaning response may also safely fall back
|
|
4637
|
+
// when a complete capability proves one exact semantic metric identity from
|
|
4638
|
+
// the current-turn requirement seed. This is intentionally stricter than
|
|
4639
|
+
// the token fallback below: `order_items.revenue` has the leaf identity
|
|
4640
|
+
// `revenue`, whereas `order_items.product_revenue` is a different meaning
|
|
4641
|
+
// and must not force a clarification when the reader asked only for
|
|
4642
|
+
// revenue. Multi-metric questions retain their dedicated exact tuple path.
|
|
4643
|
+
if (requestedMeasures.length === 1) {
|
|
4644
|
+
const requested = requestedMeasures[0];
|
|
4645
|
+
const exactMatches = [...byMetricId.values()].filter((candidate) => candidateMetricLeafIdentities(candidate).includes(requested));
|
|
4646
|
+
if (exactMatches.length === 1)
|
|
4647
|
+
return exactMatches[0];
|
|
4648
|
+
}
|
|
3377
4649
|
return byMetricId.size === 1 ? [...byMetricId.values()][0] : undefined;
|
|
3378
4650
|
}
|
|
4651
|
+
function candidateMetricLeafIdentities(candidate) {
|
|
4652
|
+
return [...new Set([
|
|
4653
|
+
candidate.id,
|
|
4654
|
+
candidate.qualifiedId ?? '',
|
|
4655
|
+
candidate.name,
|
|
4656
|
+
...(candidate.aliases ?? []),
|
|
4657
|
+
].flatMap((identity) => {
|
|
4658
|
+
const normalized = normalizeMetricPhrase(identity);
|
|
4659
|
+
const leaf = normalizeMetricPhrase(identity.split(/[.:/]/).at(-1) ?? identity);
|
|
4660
|
+
return [normalized, leaf].filter(Boolean);
|
|
4661
|
+
}))];
|
|
4662
|
+
}
|
|
3379
4663
|
function metricTokens(values) {
|
|
3380
4664
|
const ignored = new Set([
|
|
3381
4665
|
"a", "an", "and", "as", "at", "by", "current", "for", "from", "is",
|
|
@@ -3478,6 +4762,78 @@ function rethrowCancellation(error, ...signals) {
|
|
|
3478
4762
|
if (error instanceof Error && error.name === "AbortError")
|
|
3479
4763
|
throw error;
|
|
3480
4764
|
}
|
|
4765
|
+
/**
|
|
4766
|
+
* Admit the one narrowly typed extension produced by metadata only when it
|
|
4767
|
+
* closes an actually unmet requested categorical role and its source metric is
|
|
4768
|
+
* already in the compact meaning package. The extension was derived from the
|
|
4769
|
+
* same immutable metric capability snapshot; this function does not search,
|
|
4770
|
+
* infer a join, or create an alias. Multiple candidates for a role stay an
|
|
4771
|
+
* ambiguity instead of being silently ranked into a new business meaning.
|
|
4772
|
+
*/
|
|
4773
|
+
function sameSnapshotRoleTargetedMeaningExtensions(input) {
|
|
4774
|
+
const extensionsByRole = new Map();
|
|
4775
|
+
const requestedCategoricalTerms = categoricalDimensionRequirementTerms(input.requirements);
|
|
4776
|
+
for (const candidate of input.clarificationCandidates) {
|
|
4777
|
+
const extension = candidate.sameSnapshotRoleExtension;
|
|
4778
|
+
if (!extension
|
|
4779
|
+
|| extension.version !== 1
|
|
4780
|
+
|| extension.role !== 'categorical_dimension'
|
|
4781
|
+
|| (extension.basis !== 'sole_metricflow_grouping_dimension'
|
|
4782
|
+
&& extension.basis !== 'exact_metricflow_grouping_dimension')
|
|
4783
|
+
|| candidate.kind !== 'semantic_member'
|
|
4784
|
+
|| (candidate.qualifiedId ?? candidate.id) !== extension.dimensionId)
|
|
4785
|
+
continue;
|
|
4786
|
+
if (!requestedCategoricalTerms.some((requested) => normalizeMetricPhrase(requested) === normalizeMetricPhrase(extension.requestedTerm)))
|
|
4787
|
+
continue;
|
|
4788
|
+
// A raw/dbt column can lexically satisfy "product category", but it is
|
|
4789
|
+
// not a MetricFlow binding for this semantic metric. Do not let that
|
|
4790
|
+
// broad retrieval hit suppress the exact same-snapshot semantic member:
|
|
4791
|
+
// otherwise a package contains only the raw column, the model cannot bind
|
|
4792
|
+
// the metric-native grouping, and the cascade falsely reports absence.
|
|
4793
|
+
// An already-admitted semantic member remains sufficient and keeps this
|
|
4794
|
+
// extension bounded to genuinely unmet semantic roles.
|
|
4795
|
+
if (input.candidates.some((admitted) => admitted.kind === 'semantic_member'
|
|
4796
|
+
&& candidateMatchesCategoricalDimensionRequirement(admitted, [extension.requestedTerm])))
|
|
4797
|
+
continue;
|
|
4798
|
+
const sourceMetricAdmitted = input.candidates.some((admitted) => admitted.kind === 'semantic_metric'
|
|
4799
|
+
&& (admitted.id === extension.metricId || admitted.qualifiedId === extension.metricId));
|
|
4800
|
+
if (!sourceMetricAdmitted)
|
|
4801
|
+
continue;
|
|
4802
|
+
const role = normalizeMetricPhrase(extension.requestedTerm);
|
|
4803
|
+
const values = extensionsByRole.get(role) ?? [];
|
|
4804
|
+
if (!values.some((value) => value.id === candidate.id))
|
|
4805
|
+
values.push(candidate);
|
|
4806
|
+
extensionsByRole.set(role, values);
|
|
4807
|
+
}
|
|
4808
|
+
return [...extensionsByRole.values()]
|
|
4809
|
+
.flatMap((values) => values.length === 1 ? values : [])
|
|
4810
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
4811
|
+
}
|
|
4812
|
+
/**
|
|
4813
|
+
* Keep the role-targeted form of a qualified candidate when retrieval has
|
|
4814
|
+
* emitted both the ordinary catalog card and a same-snapshot extension with
|
|
4815
|
+
* the identical stable ID. The extension is not a second object or a new
|
|
4816
|
+
* join: it is the metadata proof explaining why that exact qualified object
|
|
4817
|
+
* closes a currently unmet role for a selected metric. Dropping it merely
|
|
4818
|
+
* because the less-specific catalog card appeared first makes the compact
|
|
4819
|
+
* meaning package lose a declared MetricFlow grouping and can manufacture a
|
|
4820
|
+
* false coverage gap.
|
|
4821
|
+
*/
|
|
4822
|
+
function consolidateClarificationCandidates(input) {
|
|
4823
|
+
const byId = new Map();
|
|
4824
|
+
for (const candidate of input.candidates) {
|
|
4825
|
+
if (candidate.eligible === false
|
|
4826
|
+
|| candidateConflictsWithExplicitRankingMeasure(candidate, input.requirements))
|
|
4827
|
+
continue;
|
|
4828
|
+
const existing = byId.get(candidate.id);
|
|
4829
|
+
if (!existing || (!existing.sameSnapshotRoleExtension && candidate.sameSnapshotRoleExtension)) {
|
|
4830
|
+
// Map replacement preserves the original stable ordering while retaining
|
|
4831
|
+
// the stricter same-snapshot proof for this exact candidate identity.
|
|
4832
|
+
byId.set(candidate.id, candidate);
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
4835
|
+
return [...byId.values()];
|
|
4836
|
+
}
|
|
3481
4837
|
/**
|
|
3482
4838
|
* Build a retrieval-first hybrid router. Narrow conversational/app preflight is
|
|
3483
4839
|
* deterministic. Every other turn loads structured evidence before any
|
|
@@ -3541,6 +4897,10 @@ export function createHybridRouter(options = {}) {
|
|
|
3541
4897
|
// boundary so every downstream plan/meaning path consumes the same
|
|
3542
4898
|
// typed measure + entity/dimension requirements.
|
|
3543
4899
|
evidence = withNormalizedAnalyticalRequirements(evidence, request.question);
|
|
4900
|
+
// Keep the complete snapshot result for trace-only lifecycle evidence
|
|
4901
|
+
// before canonicalization and bounded meaning-package admission prune it.
|
|
4902
|
+
const retrievedCandidates = evidence.candidates;
|
|
4903
|
+
traceRetrievalLanesBeforePruning(request, evidence, retrievedCandidates);
|
|
3544
4904
|
evidence = {
|
|
3545
4905
|
...evidence,
|
|
3546
4906
|
candidates: canonicalizeMetricMeasureCandidates(evidence.candidates),
|
|
@@ -3551,26 +4911,99 @@ export function createHybridRouter(options = {}) {
|
|
|
3551
4911
|
]).filter((candidate) => evidence.clarificationCandidates.some((item) => item.id === candidate.id)) }
|
|
3552
4912
|
: {}),
|
|
3553
4913
|
};
|
|
4914
|
+
// Ranking measure identity is resolved from the same immutable
|
|
4915
|
+
// request/evidence frame that drives package reservation. A metric
|
|
4916
|
+
// may remain in broad retrieval context, but it cannot become a
|
|
4917
|
+
// clarification or meaning-selection alternative after an explicit
|
|
4918
|
+
// comparator already bound a different ranking measure.
|
|
4919
|
+
const persistedSelection = request.selectedEvidenceId
|
|
4920
|
+
? persistedClarificationSelectionContext(request)
|
|
4921
|
+
: undefined;
|
|
4922
|
+
// A structured click is a continuation of the original server turn,
|
|
4923
|
+
// not a new parse of its display label. Retain the typed partial frame
|
|
4924
|
+
// (ranking/time included) before candidate pruning can affect it.
|
|
4925
|
+
const continuationQuestion = persistedSelection?.serverIssued && persistedSelection.sourceQuestion
|
|
4926
|
+
? persistedSelection.sourceQuestion
|
|
4927
|
+
// A free-text turn is always a new source question. A browser may
|
|
4928
|
+
// retain clarificationSourceQuestion for display, but it must never
|
|
4929
|
+
// make stale retrieval intent or a previous result authoritative.
|
|
4930
|
+
: request.question;
|
|
4931
|
+
// Retrieval parser output is never an independent source of request
|
|
4932
|
+
// authority, but it can refine a host-owned source question when every
|
|
4933
|
+
// business term is demonstrably present in that question. In
|
|
4934
|
+
// particular, "active seats" is a legitimate current-turn metric
|
|
4935
|
+
// refinement for "Which workspaces have the most active seats?";
|
|
4936
|
+
// omitting it altogether forced the seed to erase an explicit metric
|
|
4937
|
+
// before the model had a chance only to bind it. The shared
|
|
4938
|
+
// requirement builder rejects stale parser terms (for example a prior
|
|
4939
|
+
// rollover balance, member filter, or top-N), so this remains a
|
|
4940
|
+
// source-question-plus-server-selection tuple rather than retrieval
|
|
4941
|
+
// intent becoming plan authority.
|
|
4942
|
+
// A server-composed child Ask (currently a Research hypothesis) may
|
|
4943
|
+
// carry an exact host-owned seed. Planner prose is not user wording:
|
|
4944
|
+
// reparsing it can turn an asset namespace or explanatory phrase into
|
|
4945
|
+
// a second requested measure. Accept the seed only when it belongs
|
|
4946
|
+
// to this exact source question and there is no structured
|
|
4947
|
+
// continuation to merge; public request parsers never hydrate this
|
|
4948
|
+
// host-only field.
|
|
4949
|
+
const hostSeed = !request.selectedEvidenceId
|
|
4950
|
+
&& request.hostRequirementSeed?.version === 1
|
|
4951
|
+
&& request.hostRequirementSeed.sourceQuestion === continuationQuestion
|
|
4952
|
+
? request.hostRequirementSeed
|
|
4953
|
+
: undefined;
|
|
4954
|
+
const sourceQuestionRequirements = hostSeed?.requirements
|
|
4955
|
+
?? buildAnalyticalRequirementSet({
|
|
4956
|
+
question: continuationQuestion,
|
|
4957
|
+
parsedIntent: evidence.parsedIntent,
|
|
4958
|
+
});
|
|
4959
|
+
const analyticalRequirements = request.selectedEvidenceId
|
|
4960
|
+
? mergeClarificationRequirements(sourceQuestionRequirements, persistedSelection?.serverIssued ? persistedSelection.requirements : undefined)
|
|
4961
|
+
: sourceQuestionRequirements;
|
|
4962
|
+
// The host freezes the reader's requested tuple before the bounded
|
|
4963
|
+
// meaning call. A model may bind only supplied candidate identities;
|
|
4964
|
+
// it cannot erase explicit outputs, ranking, time, or a new question's
|
|
4965
|
+
// independent scope by rephrasing the request.
|
|
4966
|
+
const requirementSeed = hostSeed
|
|
4967
|
+
? hostSeed
|
|
4968
|
+
: buildAnalyticalRequirementSeedV1({
|
|
4969
|
+
question: continuationQuestion,
|
|
4970
|
+
parsedIntent: evidence.parsedIntent,
|
|
4971
|
+
requirements: analyticalRequirements,
|
|
4972
|
+
fiscalCalendar: declaredFiscalCalendar(evidence, evidence.candidates),
|
|
4973
|
+
});
|
|
3554
4974
|
// Retrieval can retain more context internally; only this compact,
|
|
3555
4975
|
// role-balanced package reaches the one meaning call. The question is
|
|
3556
4976
|
// supplied so explicit revenue, entity labels, and time roles cannot be
|
|
3557
4977
|
// pruned by unrelated lexical matches.
|
|
3558
|
-
let candidates = buildMeaningEvidencePackage(evidence, options.maxMeaningCandidates ?? 16,
|
|
3559
|
-
|
|
3560
|
-
// complete already-retrieved set
|
|
3561
|
-
//
|
|
3562
|
-
// generic/generated answer
|
|
3563
|
-
// package
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
4978
|
+
let candidates = buildMeaningEvidencePackage(evidence, options.maxMeaningCandidates ?? 16, continuationQuestion);
|
|
4979
|
+
traceCandidateLifecycleBeforePruning(request, evidence, retrievedCandidates, candidates);
|
|
4980
|
+
// The complete already-retrieved set is retained only to validate a
|
|
4981
|
+
// server-issued stable selection before any route can fall through to
|
|
4982
|
+
// a generic/generated answer. New free-text choices are constrained to
|
|
4983
|
+
// the admitted package below, so a pruned candidate cannot reappear as
|
|
4984
|
+
// an unrelated clarification option.
|
|
4985
|
+
const clarificationCandidates = consolidateClarificationCandidates({
|
|
4986
|
+
candidates: [
|
|
4987
|
+
...evidence.candidates,
|
|
4988
|
+
...(evidence.clarificationCandidates ?? []),
|
|
4989
|
+
],
|
|
4990
|
+
requirements: analyticalRequirements,
|
|
4991
|
+
});
|
|
3568
4992
|
// A structured clarification selection is identity input, not a new
|
|
3569
4993
|
// fuzzy-search phrase. Look in BOTH lists: ranking-measure choices are
|
|
3570
4994
|
// supplemental clarification candidates, not execution candidates.
|
|
3571
|
-
|
|
4995
|
+
let selectedEvidence = request.selectedEvidenceId
|
|
3572
4996
|
? clarificationCandidates.find((candidate) => candidate.id === request.selectedEvidenceId)
|
|
3573
4997
|
: undefined;
|
|
4998
|
+
if (!selectedEvidence && request.selectedEvidenceId) {
|
|
4999
|
+
selectedEvidence = rehydrateServerIssuedCapabilityDimensionSelection({
|
|
5000
|
+
request,
|
|
5001
|
+
evidence,
|
|
5002
|
+
candidates: clarificationCandidates,
|
|
5003
|
+
});
|
|
5004
|
+
if (selectedEvidence)
|
|
5005
|
+
clarificationCandidates.push(selectedEvidence);
|
|
5006
|
+
}
|
|
3574
5007
|
const structuredSelection = request.selectedEvidenceId
|
|
3575
5008
|
? validateStructuredClarificationSelection({
|
|
3576
5009
|
request,
|
|
@@ -3589,10 +5022,77 @@ export function createHybridRouter(options = {}) {
|
|
|
3589
5022
|
validation: structuredSelection,
|
|
3590
5023
|
});
|
|
3591
5024
|
}
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
5025
|
+
const selectedDimensionBinding = structuredSelection?.ok
|
|
5026
|
+
&& structuredSelection.selection.kind === 'dimension'
|
|
5027
|
+
? structuredSelection.selection
|
|
5028
|
+
: undefined;
|
|
5029
|
+
const stableSelectionCandidates = [
|
|
5030
|
+
...(selectedDimensionBinding ? [selectedDimensionBinding.metricCandidate] : []),
|
|
5031
|
+
...(selectedEvidence ? [selectedEvidence] : []),
|
|
5032
|
+
].filter((candidate, index, all) => all.findIndex((item) => item.id === candidate.id) === index);
|
|
5033
|
+
// One host-authored, role-targeted extension may enter the same
|
|
5034
|
+
// snapshot package before absence is declared. It is eligible only
|
|
5035
|
+
// when a selected/retrieved semantic metric itself proves one unique
|
|
5036
|
+
// MetricFlow grouping dimension for the unmet business role. This is
|
|
5037
|
+
// not a lexical location→region rule and never creates a raw join.
|
|
5038
|
+
const roleTargetedExtensions = sameSnapshotRoleTargetedMeaningExtensions({
|
|
5039
|
+
candidates,
|
|
5040
|
+
clarificationCandidates,
|
|
5041
|
+
requirements: analyticalRequirements,
|
|
5042
|
+
});
|
|
5043
|
+
const extensions = [
|
|
5044
|
+
...stableSelectionCandidates,
|
|
5045
|
+
...roleTargetedExtensions,
|
|
5046
|
+
].filter((candidate, index, all) => all.findIndex((item) => item.id === candidate.id) === index
|
|
5047
|
+
// A role-targeted card may intentionally share the stable ID of an
|
|
5048
|
+
// unscoped catalog card already admitted by generic relevance. In
|
|
5049
|
+
// that case replace the card in-place below so the provider sees the
|
|
5050
|
+
// same qualified object plus the immutable role proof; do not drop
|
|
5051
|
+
// the proof just because its base record was admitted first.
|
|
5052
|
+
&& (() => {
|
|
5053
|
+
const existing = candidates.find((item) => item.id === candidate.id);
|
|
5054
|
+
return !existing || (!existing.sameSnapshotRoleExtension && candidate.sameSnapshotRoleExtension);
|
|
5055
|
+
})());
|
|
5056
|
+
if (extensions.length > 0) {
|
|
5057
|
+
// Keep the exact metric and the separately selected semantic member
|
|
5058
|
+
// together. The choice never becomes a synthetic metric merely to
|
|
5059
|
+
// fit the bounded meaning package.
|
|
5060
|
+
candidates = [...extensions, ...candidates.filter((candidate) => !extensions.some((extension) => extension.id === candidate.id))]
|
|
5061
|
+
.slice(0, Math.max(1, Math.min(16, options.maxMeaningCandidates ?? 16)));
|
|
5062
|
+
const observer = askTraceObserverForV1(request);
|
|
5063
|
+
for (const extension of extensions) {
|
|
5064
|
+
const roles = evidenceCandidateRoles(extension);
|
|
5065
|
+
for (const role of roles) {
|
|
5066
|
+
observer.recordCandidateDecision({
|
|
5067
|
+
// This must stay on the router's stable resolver identity. The
|
|
5068
|
+
// qualifiedId is display/provenance data and can differ from the
|
|
5069
|
+
// candidate id used by meaning validation and model selection.
|
|
5070
|
+
candidateId: extension.id,
|
|
5071
|
+
role,
|
|
5072
|
+
source: traceSourceForCandidate(extension),
|
|
5073
|
+
decision: 'extended',
|
|
5074
|
+
reasonCode: 'same_snapshot_extension',
|
|
5075
|
+
compatibilityCode: 'unknown',
|
|
5076
|
+
});
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
3595
5079
|
}
|
|
5080
|
+
// Only package-admitted, role-compatible candidates may become a new
|
|
5081
|
+
// free-text clarification option. The complete snapshot is retained
|
|
5082
|
+
// above exclusively to validate a server-issued stable selection or
|
|
5083
|
+
// to prove a terminal safety boundary; it must never let a pruned,
|
|
5084
|
+
// unrelated metric re-enter the answer flow as a choice.
|
|
5085
|
+
const admittedClarificationCandidates = candidates.filter((candidate) => candidate.eligible !== false
|
|
5086
|
+
&& candidate.compatibility !== 'incompatible'
|
|
5087
|
+
&& !candidateConflictsWithExplicitRankingMeasure(candidate, analyticalRequirements));
|
|
5088
|
+
const attributionGap = attributionRequiredRelationshipGapDecision({
|
|
5089
|
+
request,
|
|
5090
|
+
base,
|
|
5091
|
+
evidence,
|
|
5092
|
+
requirements: analyticalRequirements,
|
|
5093
|
+
});
|
|
5094
|
+
if (attributionGap)
|
|
5095
|
+
return attributionGap;
|
|
3596
5096
|
// The compact meaning package can be empty when every ranked card was
|
|
3597
5097
|
// reserved for a role that the parser marked missing. A unique authored
|
|
3598
5098
|
// certified example is still an authoritative snapshot fact, so it
|
|
@@ -3614,7 +5114,13 @@ export function createHybridRouter(options = {}) {
|
|
|
3614
5114
|
const fiscalClarification = fiscalCalendarClarification(request, base, evidence, candidates);
|
|
3615
5115
|
if (fiscalClarification)
|
|
3616
5116
|
return fiscalClarification;
|
|
3617
|
-
|
|
5117
|
+
// A selected display/grouping field completes a persisted frame; the
|
|
5118
|
+
// matched semantic metric remains the only execution/measure
|
|
5119
|
+
// authority. A selected dimension must therefore never flow through
|
|
5120
|
+
// the metric-only direct-resolution path as the primary candidate.
|
|
5121
|
+
const explicit = selectedDimensionBinding?.metricCandidate
|
|
5122
|
+
?? selectedEvidence
|
|
5123
|
+
?? findExplicitEvidenceReference(request.question, candidates);
|
|
3618
5124
|
const explicitMeaningBinding = Boolean(explicit && (request.selectedEvidenceId
|
|
3619
5125
|
|| /@(metric|block|model|table|column)\(/i.test(request.question)));
|
|
3620
5126
|
const shouldUseMeaningCall = requireMeaningCall
|
|
@@ -3627,7 +5133,7 @@ export function createHybridRouter(options = {}) {
|
|
|
3627
5133
|
// execution shim as the answer and never let the meaning model see
|
|
3628
5134
|
// the ranking entity/measure distinction.
|
|
3629
5135
|
if (!shouldUseMeaningCall && !explicitMeaningBinding) {
|
|
3630
|
-
const deterministicClarification = deterministicPrePlanClarification(request, base, evidence,
|
|
5136
|
+
const deterministicClarification = deterministicPrePlanClarification(request, base, evidence, admittedClarificationCandidates);
|
|
3631
5137
|
if (deterministicClarification)
|
|
3632
5138
|
return deterministicClarification;
|
|
3633
5139
|
}
|
|
@@ -3638,7 +5144,7 @@ export function createHybridRouter(options = {}) {
|
|
|
3638
5144
|
&& !hasExplicitRankingMeasure(request.question, evidence)) {
|
|
3639
5145
|
return rankingMetricChoiceDecision(base, evidence, candidates, explicit, request.question);
|
|
3640
5146
|
}
|
|
3641
|
-
const decision = routeDecisionForResolution(base, evidence, candidates, directResolution(request, evidence, explicit, candidates), "heuristic", request.question, options.resolvedPlanMode ?? 'authoritative');
|
|
5147
|
+
const decision = routeDecisionForResolution(base, evidence, candidates, directResolution(request, evidence, explicit, candidates, selectedDimensionBinding ? [selectedDimensionBinding.dimensionId] : [], selectedDimensionBinding ? [selectedDimensionBinding.selectedDimensionId] : []), "heuristic", request.question, options.resolvedPlanMode ?? 'authoritative');
|
|
3642
5148
|
// A valid stable semantic choice may still be pre-freeze-ineligible
|
|
3643
5149
|
// in this local snapshot (for example, the semantic adapter cannot
|
|
3644
5150
|
// execute the selected tuple). That is a normal cascade condition:
|
|
@@ -3648,10 +5154,16 @@ export function createHybridRouter(options = {}) {
|
|
|
3648
5154
|
const selectedPreFreezeModelingGap = decision.terminalOutcome?.kind === 'modeling_gap'
|
|
3649
5155
|
&& decision.resolvedAnalyticalPlan?.capability === 'blocked'
|
|
3650
5156
|
&& decision.analyticalCascadeDecision?.planFrozen === false;
|
|
3651
|
-
return
|
|
5157
|
+
return explicit
|
|
3652
5158
|
&& structuredSelection?.ok
|
|
3653
5159
|
&& (decision.requiresClarification || selectedPreFreezeModelingGap)
|
|
3654
|
-
? continueCascadeAfterIncompleteSelection(base, evidence, candidates,
|
|
5160
|
+
? continueCascadeAfterIncompleteSelection(base, evidence, candidates,
|
|
5161
|
+
// Preserve the display-key identity the user clicked in
|
|
5162
|
+
// user-facing gap/continuation evidence; the paired metric
|
|
5163
|
+
// remains recorded below as execution provenance.
|
|
5164
|
+
selectedEvidence ?? explicit, request.question, selectedDimensionBinding
|
|
5165
|
+
? [selectedDimensionBinding.metricCandidate.id, selectedDimensionBinding.selectedDimensionId]
|
|
5166
|
+
: [explicit.id])
|
|
3655
5167
|
: decision;
|
|
3656
5168
|
}
|
|
3657
5169
|
const multiMetricPrimary = !shouldUseMeaningCall
|
|
@@ -3690,38 +5202,90 @@ export function createHybridRouter(options = {}) {
|
|
|
3690
5202
|
if (request.runBudget && !request.runBudget.mayStartDiscovery('clarify')) {
|
|
3691
5203
|
return softBoundaryDecision(request, base, 'clarify');
|
|
3692
5204
|
}
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
5205
|
+
let resolution;
|
|
5206
|
+
try {
|
|
5207
|
+
resolution = options.resolveMeaning
|
|
5208
|
+
? await options.resolveMeaning({
|
|
5209
|
+
question: requirementSeed.sourceQuestion,
|
|
5210
|
+
history: request.conversationBinding && request.conversationBinding !== 'none'
|
|
5211
|
+
? effectiveConversationHistory(request)
|
|
5212
|
+
: undefined,
|
|
5213
|
+
// The resolver/provider receives the same bounded evidence
|
|
5214
|
+
// package as its candidate argument. Supplemental qualified
|
|
5215
|
+
// cards are a host-only clarification aid and must not leak
|
|
5216
|
+
// through this richer carrier.
|
|
5217
|
+
evidence: {
|
|
5218
|
+
...evidence,
|
|
5219
|
+
candidates,
|
|
5220
|
+
clarificationCandidates: undefined,
|
|
5221
|
+
},
|
|
3703
5222
|
candidates,
|
|
3704
|
-
|
|
3705
|
-
},
|
|
3706
|
-
candidates,
|
|
3707
|
-
signal: request.signal ?? options.signal,
|
|
3708
|
-
})
|
|
3709
|
-
: options.complete
|
|
3710
|
-
? parseMeaningResolution(await options.complete({
|
|
3711
|
-
system: buildMeaningSystemPrompt(),
|
|
3712
|
-
user: buildMeaningUserPrompt(request, evidence, candidates),
|
|
5223
|
+
requirementSeed,
|
|
3713
5224
|
signal: request.signal ?? options.signal,
|
|
3714
|
-
})
|
|
3715
|
-
:
|
|
5225
|
+
})
|
|
5226
|
+
: options.complete
|
|
5227
|
+
? parseMeaningResolution(await options.complete({
|
|
5228
|
+
system: buildMeaningSystemPrompt(),
|
|
5229
|
+
user: buildMeaningUserPrompt(request, evidence, candidates, requirementSeed),
|
|
5230
|
+
signal: request.signal ?? options.signal,
|
|
5231
|
+
request,
|
|
5232
|
+
phase: 'meaning_resolution',
|
|
5233
|
+
}), requirementSeed)
|
|
5234
|
+
: undefined;
|
|
5235
|
+
}
|
|
5236
|
+
catch (error) {
|
|
5237
|
+
throw error;
|
|
5238
|
+
}
|
|
3716
5239
|
if (resolution) {
|
|
5240
|
+
// A model is allowed to say that it cannot bind any supplied
|
|
5241
|
+
// card, but that is not a new business ambiguity by itself. If
|
|
5242
|
+
// the host can prove one exact current-turn semantic identity,
|
|
5243
|
+
// take the ordinary direct/cascade path instead of preserving an
|
|
5244
|
+
// empty selection as a clarification. This is intentionally
|
|
5245
|
+
// evaluated before merge/validation so the host never treats an
|
|
5246
|
+
// empty provider response as an authoritative analytical frame.
|
|
5247
|
+
if (meaningResolutionHasNoBinding(resolution)) {
|
|
5248
|
+
const hostFallback = routeWithoutMeaningModel(request, base, evidence, candidates, options.resolvedPlanMode ?? 'authoritative', true);
|
|
5249
|
+
if (isFrozenExecutableHostFallback(hostFallback)) {
|
|
5250
|
+
return remember(key, hostFallback);
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
// Merge exactly once at the model boundary. Candidate IDs remain
|
|
5254
|
+
// subject to the bounded-package validator below; model route,
|
|
5255
|
+
// SQL-adjacent frame, rephrased question, and query intent never
|
|
5256
|
+
// become downstream authority.
|
|
5257
|
+
resolution = mergeMeaningResolutionWithRequirementSeed({
|
|
5258
|
+
seed: requirementSeed,
|
|
5259
|
+
resolution,
|
|
5260
|
+
candidates,
|
|
5261
|
+
});
|
|
3717
5262
|
// Repair an incomplete certified nomination before generic
|
|
3718
5263
|
// evidence validation. This preserves the real semantic choices
|
|
3719
5264
|
// instead of turning a false certified selection into an opaque
|
|
3720
5265
|
// invalid-evidence gap.
|
|
3721
5266
|
const certifiedSafeResolution = repairIncompleteCertifiedMeasureSelection(resolution, evidence, candidates);
|
|
3722
|
-
const validated = validateMeaningResolution(certifiedSafeResolution, candidates, evidence.parsedIntent?.measures ?? certifiedSafeResolution.queryIntent.measures);
|
|
5267
|
+
const validated = validateMeaningResolution(certifiedSafeResolution, candidates, evidence.parsedIntent?.measures ?? certifiedSafeResolution.queryIntent.measures, { requirements: analyticalRequirements });
|
|
3723
5268
|
if (validated.ok) {
|
|
3724
|
-
|
|
5269
|
+
// The meaning model may select only supplied candidate IDs. It
|
|
5270
|
+
// intentionally cannot return an analytical frame, because a
|
|
5271
|
+
// model-owned frame could add a metric, dimension, output, or
|
|
5272
|
+
// prior-turn scope after the host froze the requirement seed.
|
|
5273
|
+
//
|
|
5274
|
+
// Do not leave that candidate-only result on the legacy V1
|
|
5275
|
+
// path, though: a valid selected semantic metric plus its
|
|
5276
|
+
// selected supplied members must receive the same *host-built*
|
|
5277
|
+
// V2 frame as a zero-call/direct resolution. Otherwise the
|
|
5278
|
+
// router freezes a V1 semantic plan, the answer loop skips the
|
|
5279
|
+
// immutable semantic execution graph, and legacy SQL is
|
|
5280
|
+
// incorrectly checked against generic aggregation metadata.
|
|
5281
|
+
const frameBoundResolution = attachHostOwnedAnalyticalFrame({
|
|
5282
|
+
request,
|
|
5283
|
+
evidence,
|
|
5284
|
+
candidates,
|
|
5285
|
+
requirementSeed,
|
|
5286
|
+
resolution: validated.resolution,
|
|
5287
|
+
});
|
|
5288
|
+
const safeResolution = preventDegenerateRankingResolution(frameBoundResolution, evidence, candidates, request.question);
|
|
3725
5289
|
// Meaning interpretation is still required for a fresh turn,
|
|
3726
5290
|
// but it cannot invent a ranking measure when the user only
|
|
3727
5291
|
// supplied an entity. Preserve the precise follow-up after the
|
|
@@ -3770,24 +5334,27 @@ export function createHybridRouter(options = {}) {
|
|
|
3770
5334
|
.some((item) => /measure|metric/i.test(item));
|
|
3771
5335
|
const assumedRanking = resolverFlagged
|
|
3772
5336
|
? undefined
|
|
3773
|
-
: assumableRankingMeasure(request.question,
|
|
5337
|
+
: assumableRankingMeasure(request.question, admittedClarificationCandidates);
|
|
3774
5338
|
if (assumedRanking) {
|
|
3775
5339
|
return {
|
|
3776
5340
|
...routeDecisionForResolution(base, evidence, candidates, directResolution(request, evidence, assumedRanking.candidate, candidates), 'heuristic', request.question),
|
|
3777
5341
|
assumptions: [assumedRanking.assumption],
|
|
3778
5342
|
};
|
|
3779
5343
|
}
|
|
3780
|
-
return bareRankingClarification(base, retrievalTrace(evidence, candidates), request.question, evidence,
|
|
3781
|
-
// Supplemental clarification cards carry the ranking
|
|
3782
|
-
// measures for the requested entity, which the execution
|
|
3783
|
-
// candidate set deliberately does not.
|
|
3784
|
-
clarificationCandidates, !resolverFlagged);
|
|
5344
|
+
return bareRankingClarification(base, retrievalTrace(evidence, candidates), request.question, evidence, admittedClarificationCandidates, !resolverFlagged);
|
|
3785
5345
|
}
|
|
3786
|
-
const deterministicGap = deterministicPrePlanClarification(request, base, evidence,
|
|
5346
|
+
const deterministicGap = deterministicPrePlanClarification(request, base, evidence, admittedClarificationCandidates);
|
|
3787
5347
|
if (deterministicGap && safeResolution.recommendedRoute === 'clarify') {
|
|
3788
5348
|
return deterministicGap;
|
|
3789
5349
|
}
|
|
3790
|
-
|
|
5350
|
+
const meaningDecision = routeDecisionForResolution(base, evidence, candidates, safeResolution, "llm", request.question, options.resolvedPlanMode ?? 'authoritative');
|
|
5351
|
+
return remember(key, continuePreFreezeModelingGapThroughPhysicalSnapshot({
|
|
5352
|
+
decision: meaningDecision,
|
|
5353
|
+
base,
|
|
5354
|
+
evidence,
|
|
5355
|
+
candidates,
|
|
5356
|
+
question: request.question,
|
|
5357
|
+
}));
|
|
3791
5358
|
}
|
|
3792
5359
|
const invalidResolution = {
|
|
3793
5360
|
interpretedQuestion: request.question,
|
|
@@ -3823,11 +5390,14 @@ export function createHybridRouter(options = {}) {
|
|
|
3823
5390
|
const fallbackDecision = routeWithoutMeaningModel(request, base, evidence, candidates, options.resolvedPlanMode ?? 'authoritative', meaningResolverReachable);
|
|
3824
5391
|
if (!shouldUseMeaningCall)
|
|
3825
5392
|
return fallbackDecision;
|
|
5393
|
+
if (isFrozenExecutableHostFallback(fallbackDecision)) {
|
|
5394
|
+
return fallbackDecision;
|
|
5395
|
+
}
|
|
3826
5396
|
// The provider was unavailable or returned malformed JSON. Apply the
|
|
3827
5397
|
// deterministic clarification only after the bounded meaning attempt
|
|
3828
5398
|
// has been exhausted; this preserves a precise recovery path without
|
|
3829
5399
|
// allowing the generic governed error to terminate the question.
|
|
3830
|
-
return deterministicPrePlanClarification(request, base, evidence,
|
|
5400
|
+
return deterministicPrePlanClarification(request, base, evidence, admittedClarificationCandidates)
|
|
3831
5401
|
?? fallbackDecision;
|
|
3832
5402
|
}
|
|
3833
5403
|
}
|
|
@@ -3890,6 +5460,8 @@ export function createHybridRouter(options = {}) {
|
|
|
3890
5460
|
system: buildSystemPrompt(),
|
|
3891
5461
|
user: buildUserPrompt(request, catalogContext),
|
|
3892
5462
|
signal: request.signal ?? options.signal,
|
|
5463
|
+
request,
|
|
5464
|
+
phase: 'classification',
|
|
3893
5465
|
});
|
|
3894
5466
|
const classification = parseClassification(raw);
|
|
3895
5467
|
if (classification)
|