@duckcodeailabs/dql-agent 1.14.1 → 1.14.3-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-run-engine.d.ts +71 -2
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +1048 -31
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +12 -0
- package/dist/agent-run-gates.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 +211 -28
- 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/synthesis.d.ts +4 -0
- package/dist/agentic/research/synthesis.d.ts.map +1 -1
- package/dist/agentic/research/synthesis.js +23 -8
- package/dist/agentic/research/synthesis.js.map +1 -1
- package/dist/agentic/research-agent.d.ts.map +1 -1
- package/dist/agentic/research-agent.js +13 -3
- 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 +233 -2
- 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 +332 -17
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +613 -0
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +1007 -0
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +110 -6
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +1180 -125
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +32 -1
- package/dist/answer-shape.d.ts.map +1 -1
- package/dist/answer-shape.js +49 -3
- package/dist/answer-shape.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/conversation/snapshot.d.ts +20 -0
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +30 -1
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/fixtures/ask-ai-office-shaped.d.ts +153 -0
- package/dist/fixtures/ask-ai-office-shaped.d.ts.map +1 -0
- package/dist/fixtures/ask-ai-office-shaped.js +94 -0
- package/dist/fixtures/ask-ai-office-shaped.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 +11 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +38 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +31 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/meaning-resolution.d.ts +141 -2
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +444 -30
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +144 -7
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/block-fit.d.ts +18 -0
- package/dist/metadata/block-fit.d.ts.map +1 -1
- package/dist/metadata/block-fit.js +250 -38
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +58 -12
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +19 -3
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +518 -54
- 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/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +23 -1
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +22 -1
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +773 -49
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +14 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +3668 -425
- 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/dist/semantic-bridge/member-select.d.ts.map +1 -1
- package/dist/semantic-bridge/member-select.js +39 -21
- package/dist/semantic-bridge/member-select.js.map +1 -1
- package/package.json +5 -5
package/dist/answer-loop.js
CHANGED
|
@@ -30,12 +30,12 @@ import { certifiedFitAllowsTier1, certifiedTerminationVerdict, evaluateCertified
|
|
|
30
30
|
import { buildGovernedMetricFirstSql, matchSemanticMetric, resolveGovernedMetricDefinition, resolveGovernedMetricSql, semanticMetricEmbeddingOptions } from './metadata/metric-match.js';
|
|
31
31
|
import { decideAgentAction } from './intent-controller.js';
|
|
32
32
|
import { createContextLedger } from './grounding/context-ledger.js';
|
|
33
|
-
import { validateAnswerResultShape } from './answer-shape.js';
|
|
33
|
+
import { validateAnswerResultShape, } from './answer-shape.js';
|
|
34
34
|
import { analyticalErrorDetail, } from './analytical-error.js';
|
|
35
35
|
import { fanoutWarningsForSql } from './metadata/grain-ledger.js';
|
|
36
36
|
import { buildAggregationSafetyProof, buildSemanticCompilationAggregationSafetyProof, } from './aggregation-safety-proof.js';
|
|
37
37
|
import { buildSemanticProofAuthorityV1 } from './semantic-proof-authority.js';
|
|
38
|
-
import { GENERATED_ANALYTICAL_TUPLE_DRIFT_MESSAGE, validateGeneratedAnalyticalProposal, } from './generated-analytical-proposal.js';
|
|
38
|
+
import { GENERATED_ANALYTICAL_TUPLE_DRIFT_MESSAGE, frozenRequiredOutputBindingProofsForPlan, validateFrozenRequiredOutputProjection, validateGeneratedAnalyticalProposal, } from './generated-analytical-proposal.js';
|
|
39
39
|
import { evaluateDbtFirstGeneratedSql } from './metadata/dbt-first-safety.js';
|
|
40
40
|
import { planAnalyticalPath, humanizeAnalyticalEntityId, analyticalPolicyUserFacingReason, } from './metadata/analytical-policy.js';
|
|
41
41
|
import { planCertifiedAdaptation } from './metadata/block-adapt.js';
|
|
@@ -384,6 +384,142 @@ function certifiedInvocationInputs(block, plan) {
|
|
|
384
384
|
}
|
|
385
385
|
return Object.keys(parameters).length > 0 ? { parameters, parameterSources } : {};
|
|
386
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Return only output aliases that the selected certified artifact itself
|
|
389
|
+
* declares. This is deliberately built after the router has frozen the plan:
|
|
390
|
+
* retrieval context, tags, examples, and sibling metrics never enter the
|
|
391
|
+
* result-shape proof.
|
|
392
|
+
*
|
|
393
|
+
* A block may express an entity role (`customer`) through a readable label
|
|
394
|
+
* column (`customer_name`), or a declared dimension role through a physical
|
|
395
|
+
* projection alias (`product_type`). Those are valid execution contracts only
|
|
396
|
+
* when the selected block owns both sides of the binding.
|
|
397
|
+
*/
|
|
398
|
+
function frozenCertifiedResultShapeBindings(plan, block, questionPlan, options = {}) {
|
|
399
|
+
if (!plan || block.kind !== 'block')
|
|
400
|
+
return [];
|
|
401
|
+
const canonical = (value) => value
|
|
402
|
+
.toLowerCase()
|
|
403
|
+
.replace(/[_\-./]+/g, ' ')
|
|
404
|
+
.replace(/[^a-z0-9 ]+/g, ' ')
|
|
405
|
+
.split(/\s+/)
|
|
406
|
+
.filter(Boolean)
|
|
407
|
+
.map((token) => token.endsWith('ies') && token.length > 4
|
|
408
|
+
? `${token.slice(0, -3)}y`
|
|
409
|
+
: token.endsWith('s') && token.length > 3
|
|
410
|
+
? token.slice(0, -1)
|
|
411
|
+
: token)
|
|
412
|
+
.join('_');
|
|
413
|
+
const declared = (block.outputContract?.length
|
|
414
|
+
? block.outputContract.map((output) => ({ name: output.name, role: output.role }))
|
|
415
|
+
: (block.declaredOutputs ?? []).map((name) => ({ name })))
|
|
416
|
+
.filter((output) => Boolean(output.name?.trim()));
|
|
417
|
+
const declaredByCanonical = new Map(declared.map((output) => [canonical(output.name), output]));
|
|
418
|
+
const bindings = [];
|
|
419
|
+
const add = (binding) => {
|
|
420
|
+
const key = `${binding.role}\0${canonical(binding.requested)}\0${canonical(binding.output)}`;
|
|
421
|
+
const existing = bindings.find((candidate) => `${candidate.role}\0${canonical(candidate.requested)}\0${canonical(candidate.output)}` === key);
|
|
422
|
+
if (existing) {
|
|
423
|
+
const aliases = [...(existing.aliases ?? []), ...(binding.aliases ?? [])]
|
|
424
|
+
.map((alias) => alias.trim())
|
|
425
|
+
.filter(Boolean);
|
|
426
|
+
if (aliases.length > 0)
|
|
427
|
+
existing.aliases = Array.from(new Set(aliases));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
bindings.push(binding);
|
|
431
|
+
};
|
|
432
|
+
// Measures are only admitted when the frozen plan retained the block's
|
|
433
|
+
// strict, artifact-local declared output identity. Do not infer an alias
|
|
434
|
+
// from output role here: that would reintroduce lifetime_spend -> revenue.
|
|
435
|
+
for (const measure of plan.query.measures) {
|
|
436
|
+
if (!measure.outputName)
|
|
437
|
+
continue;
|
|
438
|
+
const output = declaredByCanonical.get(canonical(measure.outputName));
|
|
439
|
+
if (output)
|
|
440
|
+
add({ requested: measure.requested, output: output.name, role: 'measure' });
|
|
441
|
+
}
|
|
442
|
+
const requestedDimensions = Array.from(new Set([
|
|
443
|
+
...plan.query.dimensions.map((binding) => binding.requested),
|
|
444
|
+
...questionPlan.requestedShape.dimensions,
|
|
445
|
+
].map((value) => canonical(value)).filter(Boolean)));
|
|
446
|
+
const declaredEntities = new Set((block.entities ?? []).map(canonical).filter(Boolean));
|
|
447
|
+
const declaredDimensions = new Set((block.dimensions ?? []).map(canonical).filter(Boolean));
|
|
448
|
+
const typedDimensionOutputs = declared.filter((output) => output.role === 'dimension');
|
|
449
|
+
for (const requested of requestedDimensions) {
|
|
450
|
+
// The declared entity role can be rendered as a standard display label,
|
|
451
|
+
// e.g. `customer` -> `customer_name`. Require both the authored entity
|
|
452
|
+
// declaration and the output's explicit entity-label shape.
|
|
453
|
+
if (declaredEntities.has(requested)) {
|
|
454
|
+
const entityLabel = declared.find((output) => {
|
|
455
|
+
const outputName = canonical(output.name);
|
|
456
|
+
return output.role === 'entity_label'
|
|
457
|
+
|| outputName === `${requested}_name`
|
|
458
|
+
|| outputName === `${requested}_label`;
|
|
459
|
+
});
|
|
460
|
+
if (entityLabel)
|
|
461
|
+
add({ requested, output: entityLabel.name, role: 'entity_label' });
|
|
462
|
+
}
|
|
463
|
+
// An exact declared output is an artifact-local business-dimension binding
|
|
464
|
+
// even when an older authored block predates explicit `role = dimension`.
|
|
465
|
+
// This is not a pooled or lexical alias: both the requested business
|
|
466
|
+
// dimension and the selected artifact's own projection must agree.
|
|
467
|
+
const exactDeclaredDimension = declaredByCanonical.get(requested);
|
|
468
|
+
if (declaredDimensions.has(requested) && exactDeclaredDimension) {
|
|
469
|
+
add({ requested, output: exactDeclaredDimension.name, role: 'dimension' });
|
|
470
|
+
// Otherwise a physical output can stand for a business dimension only when
|
|
471
|
+
// the block declares that dimension and has one unambiguous typed
|
|
472
|
+
// dimension projection. Multiple physical outputs require an authored
|
|
473
|
+
// alias rather than a guess.
|
|
474
|
+
}
|
|
475
|
+
else if (declaredDimensions.has(requested) && typedDimensionOutputs.length === 1) {
|
|
476
|
+
add({ requested, output: typedDimensionOutputs[0].name, role: 'dimension' });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// An exact *unique* authored example can prove that a parser token is a
|
|
480
|
+
// member value of the selected block's already-declared dimension. For
|
|
481
|
+
// example, the block-owned example "revenue by food and drink" returns the
|
|
482
|
+
// declared `category` output; `food` is not an independently requested
|
|
483
|
+
// physical column. Keep this binding intentionally local to the frozen
|
|
484
|
+
// block and its resolved dimension: no tags, descriptions, sibling evidence,
|
|
485
|
+
// or general lexical aliases are admitted here. Measures remain strict.
|
|
486
|
+
if (options.uniqueExactExampleContract === true) {
|
|
487
|
+
const plannedDimensions = new Set(plan.query.dimensions.map((binding) => canonical(binding.requested)));
|
|
488
|
+
const resolvedDimensionBindings = bindings.filter((binding) => binding.role === 'dimension'
|
|
489
|
+
&& plannedDimensions.has(canonical(binding.requested))
|
|
490
|
+
&& declaredByCanonical.has(canonical(binding.output)));
|
|
491
|
+
if (resolvedDimensionBindings.length === 1) {
|
|
492
|
+
const dimensionBinding = resolvedDimensionBindings[0];
|
|
493
|
+
const protectedTerms = new Set([
|
|
494
|
+
...questionPlan.requestedShape.measures,
|
|
495
|
+
...plan.query.measures.map((binding) => binding.requested),
|
|
496
|
+
...questionPlan.requestedShape.filters,
|
|
497
|
+
].map(canonical).filter(Boolean));
|
|
498
|
+
const structuralRoles = new Set([
|
|
499
|
+
'product', 'customer', 'account', 'user', 'member', 'category', 'segment',
|
|
500
|
+
'region', 'channel', 'order', 'day', 'week', 'month', 'quarter', 'year',
|
|
501
|
+
]);
|
|
502
|
+
const memberNoise = Array.from(new Set([
|
|
503
|
+
...questionPlan.requestedShape.dimensions,
|
|
504
|
+
...questionPlan.requestedShape.requiredOutputs,
|
|
505
|
+
].map(canonical).filter(Boolean))).filter((token) => token !== canonical(dimensionBinding.requested)
|
|
506
|
+
&& !declaredByCanonical.has(token)
|
|
507
|
+
&& !declaredEntities.has(token)
|
|
508
|
+
&& !declaredDimensions.has(token)
|
|
509
|
+
&& !protectedTerms.has(token)
|
|
510
|
+
&& !structuralRoles.has(token));
|
|
511
|
+
if (memberNoise.length > 0) {
|
|
512
|
+
add({
|
|
513
|
+
requested: dimensionBinding.requested,
|
|
514
|
+
output: dimensionBinding.output,
|
|
515
|
+
role: 'dimension',
|
|
516
|
+
aliases: memberNoise,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return bindings;
|
|
522
|
+
}
|
|
387
523
|
function damerauLevenshteinDistance(left, right) {
|
|
388
524
|
const rows = left.length + 1;
|
|
389
525
|
const columns = right.length + 1;
|
|
@@ -1025,9 +1161,17 @@ export async function answer(input) {
|
|
|
1025
1161
|
const analyticalExecutionGraph = analyticalGraphBuild?.status === 'ready'
|
|
1026
1162
|
? analyticalGraphBuild.graph
|
|
1027
1163
|
: undefined;
|
|
1164
|
+
// A v2 plan can also be synthesized for legacy/direct AnswerLoop callers so
|
|
1165
|
+
// they can opt into the deterministic compiler. That compatibility bridge is
|
|
1166
|
+
// not a router freeze: it must not turn a missing optional graph/adapter into
|
|
1167
|
+
// a terminal answer before the established, safe legacy semantic execution
|
|
1168
|
+
// path is considered. Router-selected semantic plans carry the explicit
|
|
1169
|
+
// `selectedCascadeTier` signal and remain immutable/terminal below.
|
|
1170
|
+
const routerFrozenSemanticPlan = Boolean(frozenSemanticRouteForInput(normalizedInput));
|
|
1028
1171
|
const analyticalExecutionGraphFailure = normalizedInput.resolvedAnalyticalPlan?.schemaVersion === 2
|
|
1029
1172
|
&& !multiMetricPlan
|
|
1030
1173
|
&& !generatedProposalPlan
|
|
1174
|
+
&& routerFrozenSemanticPlan
|
|
1031
1175
|
? analyticalGraphBuild?.status === 'blocked'
|
|
1032
1176
|
? {
|
|
1033
1177
|
code: analyticalGraphBuild.code,
|
|
@@ -1112,7 +1256,23 @@ export async function answer(input) {
|
|
|
1112
1256
|
}
|
|
1113
1257
|
: compiledInput;
|
|
1114
1258
|
const loopResult = await runAnswerLoop(executionInput);
|
|
1115
|
-
|
|
1259
|
+
let result = applyHollowAnswerGate(validateCertifiedAnalyticalGraphAnswer(loopResult, executionInput.analyticalExecutionGraph, executionInput.resolvedAnalyticalPlan?.analyticalFrame));
|
|
1260
|
+
const frozenSemanticRoute = frozenSemanticRouteForInput(executionInput);
|
|
1261
|
+
// A final defensive boundary keeps future return sites from accidentally
|
|
1262
|
+
// restoring the old semantic -> generated fallthrough. Do not relabel an
|
|
1263
|
+
// actual generated result as semantic: replace it with an inspectable,
|
|
1264
|
+
// same-tier terminal failure instead.
|
|
1265
|
+
const returnedNonSemanticRoute = frozenSemanticRoute && ((result.route !== undefined && result.route.tier !== 'semantic_metric')
|
|
1266
|
+
|| (result.route === undefined && result.kind !== 'no_answer' && result.sourceTier !== 'semantic_layer'));
|
|
1267
|
+
if (returnedNonSemanticRoute && frozenSemanticRoute) {
|
|
1268
|
+
result = frozenSemanticPlanFailureAnswer({
|
|
1269
|
+
answerInput: executionInput,
|
|
1270
|
+
considered: result.considered ?? [],
|
|
1271
|
+
providerName: executionInput.provider.name,
|
|
1272
|
+
code: 'EXECUTION_GRAPH_MISMATCH',
|
|
1273
|
+
reason: `The pinned semantic plan ${frozenSemanticRoute.ref ?? 'selected by the router'} returned a non-semantic answer route. DQL did not substitute generated SQL.`,
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1116
1276
|
// Attach the canonical trust label once, at the single exit point, so every
|
|
1117
1277
|
// return site inside runAnswerLoop stays untouched and backward compatible.
|
|
1118
1278
|
// Freshness-aware trust: for a certified answer, fold the source block's data
|
|
@@ -1120,7 +1280,7 @@ export async function answer(input) {
|
|
|
1120
1280
|
// stale data" / "Certified · upstream failed". Non-certified or fresh answers
|
|
1121
1281
|
// are unaffected.
|
|
1122
1282
|
const { _semanticMetricMatch, ...publicResult } = result;
|
|
1123
|
-
const chosenRoute = result.route ?? deriveAiRoute(result, _semanticMetricMatch);
|
|
1283
|
+
const chosenRoute = frozenSemanticRoute ?? result.route ?? deriveAiRoute(result, _semanticMetricMatch);
|
|
1124
1284
|
// P0 — record the high-level action this turn warranted, so callers can route
|
|
1125
1285
|
// (compose_app → app build, investigate → research) and the UI can show the
|
|
1126
1286
|
// agent's reasoning. Computed once at the single exit from the finished answer.
|
|
@@ -1201,6 +1361,20 @@ function cascadeExecutionStatus(result) {
|
|
|
1201
1361
|
*/
|
|
1202
1362
|
function deriveAiRoute(result, metricMatch) {
|
|
1203
1363
|
if (result.kind === 'no_answer') {
|
|
1364
|
+
// Preserve the frozen certified tier as provenance when that exact block
|
|
1365
|
+
// executed but failed its own result contract. The route is not a trust
|
|
1366
|
+
// badge: `certification` remains review-required and no generated fallback
|
|
1367
|
+
// may be selected after freeze.
|
|
1368
|
+
if (result.certifiedResultShapeFailure && result.sourceTier === 'certified_artifact') {
|
|
1369
|
+
const ref = result.sourceCertifiedBlock ?? result.block?.name ?? result.citations[0]?.name;
|
|
1370
|
+
return {
|
|
1371
|
+
tier: 'certified_block',
|
|
1372
|
+
label: ref
|
|
1373
|
+
? `Certified block ${ref} did not return the complete requested output shape.`
|
|
1374
|
+
: 'The selected certified block did not return the complete requested output shape.',
|
|
1375
|
+
ref,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1204
1378
|
if (result.exploratoryCandidate) {
|
|
1205
1379
|
return {
|
|
1206
1380
|
tier: 'no_answer',
|
|
@@ -1240,6 +1414,69 @@ function deriveAiRoute(result, metricMatch) {
|
|
|
1240
1414
|
? { tier: 'generated_sql', label: 'Prepared review-required DQL artifact with SQL preview.' }
|
|
1241
1415
|
: { tier: 'generated_sql', label: 'Prepared review-required SQL preview.' };
|
|
1242
1416
|
}
|
|
1417
|
+
/**
|
|
1418
|
+
* The router owns the cascade decision. Once it freezes a semantic plan, the
|
|
1419
|
+
* answer loop may either execute that exact plan or return a semantic-tier
|
|
1420
|
+
* terminal failure. It must never let an unavailable semantic adapter fall
|
|
1421
|
+
* through to the legacy generated-SQL lane merely because the latter can still
|
|
1422
|
+
* construct a preview.
|
|
1423
|
+
*/
|
|
1424
|
+
function frozenSemanticRouteForInput(input) {
|
|
1425
|
+
const plan = input.resolvedAnalyticalPlan;
|
|
1426
|
+
// `freezeLegacySemanticSelection()` upgrades older direct AnswerLoop callers
|
|
1427
|
+
// to a typed plan so they can use the deterministic semantic compiler. That
|
|
1428
|
+
// migration is not the router's immutable cascade freeze. The server-owned
|
|
1429
|
+
// cascade explicitly carries `selectedCascadeTier: 'semantic'`; only that
|
|
1430
|
+
// signal activates the no-downgrade boundary below. Otherwise direct legacy
|
|
1431
|
+
// callers retain their established deterministic semantic behavior.
|
|
1432
|
+
if (input.selectedCascadeTier !== 'semantic'
|
|
1433
|
+
|| plan?.mode !== 'authoritative'
|
|
1434
|
+
|| plan.capability !== 'semantic_execution')
|
|
1435
|
+
return undefined;
|
|
1436
|
+
const executionId = plan.executionId ?? plan.selectedConceptIds[0];
|
|
1437
|
+
return {
|
|
1438
|
+
tier: 'semantic_metric',
|
|
1439
|
+
label: executionId
|
|
1440
|
+
? `Frozen semantic plan ${executionId}`
|
|
1441
|
+
: 'Frozen semantic plan',
|
|
1442
|
+
...(executionId ? { ref: executionId } : {}),
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
function frozenSemanticPlanFailureAnswer(input) {
|
|
1446
|
+
const { answerInput, considered, providerName, code, reason } = input;
|
|
1447
|
+
const route = frozenSemanticRouteForInput(answerInput);
|
|
1448
|
+
// Callers use this only for an authoritative semantic plan. Keep the
|
|
1449
|
+
// fallback defensive so an internal misuse still produces a safe no-answer.
|
|
1450
|
+
const analyticalFailure = analyticalFailureForInput(answerInput, {
|
|
1451
|
+
error: { code, message: reason },
|
|
1452
|
+
phase: 'compilation',
|
|
1453
|
+
failedBindings: answerInput.resolvedAnalyticalPlan?.executionId
|
|
1454
|
+
? [{
|
|
1455
|
+
qualifiedId: answerInput.resolvedAnalyticalPlan.executionId,
|
|
1456
|
+
role: 'metric',
|
|
1457
|
+
reasonCode: code,
|
|
1458
|
+
}]
|
|
1459
|
+
: undefined,
|
|
1460
|
+
});
|
|
1461
|
+
return {
|
|
1462
|
+
kind: 'no_answer',
|
|
1463
|
+
sourceTier: 'no_answer',
|
|
1464
|
+
certification: 'analyst_review_required',
|
|
1465
|
+
reviewStatus: 'none',
|
|
1466
|
+
confidence: 0,
|
|
1467
|
+
text: analyticalFailure.message,
|
|
1468
|
+
answer: analyticalFailure.message,
|
|
1469
|
+
executionError: analyticalFailure.message,
|
|
1470
|
+
refusalCode: 'grounding_gap',
|
|
1471
|
+
refusalDetails: { code, message: analyticalFailure.message },
|
|
1472
|
+
analyticalFailure,
|
|
1473
|
+
...(route ? { route } : {}),
|
|
1474
|
+
citations: contextPackCitations(answerInput.contextPack, 8),
|
|
1475
|
+
considered,
|
|
1476
|
+
contextPack: answerInput.contextPack,
|
|
1477
|
+
providerUsed: providerName,
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1243
1480
|
/** "customers_customer_name" → "customers" for a question the user has to read. */
|
|
1244
1481
|
function humanizeDeicticDimension(dimension) {
|
|
1245
1482
|
const leaf = dimension.split(/[.:]/).pop() ?? dimension;
|
|
@@ -1264,6 +1501,18 @@ async function runAnswerLoop(input) {
|
|
|
1264
1501
|
const scopedContextPack = questionDomainScope.length > 0
|
|
1265
1502
|
? scopeContextPackToQuestionDomains(input.contextPack, questionDomainScope, input.manifest)
|
|
1266
1503
|
: input.contextPack;
|
|
1504
|
+
// The router has already chosen the physical exploratory closure. Treat it
|
|
1505
|
+
// as an execution authority boundary here, before any prompt is constructed
|
|
1506
|
+
// or provider is dispatched. Full retrieval stays on `input.contextPack`
|
|
1507
|
+
// for receipts and diagnostics only.
|
|
1508
|
+
const forcedExploratoryTier = input.selectedCascadeTier === 'exploratory_sql';
|
|
1509
|
+
const frozenSemanticRoute = frozenSemanticRouteForInput(input);
|
|
1510
|
+
const exploratoryClosureContextPack = forcedExploratoryTier
|
|
1511
|
+
? scopeContextPackToExploratoryCandidateClosure(scopedContextPack, input.exploratoryCandidateIds)
|
|
1512
|
+
: undefined;
|
|
1513
|
+
const executionContextPack = forcedExploratoryTier
|
|
1514
|
+
? exploratoryClosureContextPack
|
|
1515
|
+
: scopedContextPack;
|
|
1267
1516
|
// Select the RELEVANT skills (not all) for this question; keep pinned project
|
|
1268
1517
|
// skills (SQL conventions). Block hints still come from the full set so a
|
|
1269
1518
|
// preferred-block mapping is never lost.
|
|
@@ -1305,7 +1554,7 @@ async function runAnswerLoop(input) {
|
|
|
1305
1554
|
const semanticHits = kg.search({ query: question, ...searchScope, kinds: SEMANTIC_KINDS, limit: 12 });
|
|
1306
1555
|
const manifestHits = kg.search({ query: question, ...searchScope, kinds: MANIFEST_KINDS, limit: 12 });
|
|
1307
1556
|
const considered = mergeHits(artifactHits, semanticHits, manifestHits, kg.search({ query: question, domain, limit: 10 })).slice(0, 30);
|
|
1308
|
-
const schemaContext = schemaContextWithAllowedSqlContext(schemaContextWithinQuestionScope(input.schemaContext ?? [], input.contextPack,
|
|
1557
|
+
const schemaContext = schemaContextWithAllowedSqlContext(schemaContextWithinQuestionScope(input.schemaContext ?? [], input.contextPack, executionContextPack), executionContextPack);
|
|
1309
1558
|
const catalogRoute = input.contextPack?.routeDecision;
|
|
1310
1559
|
const baseQuestionPlan = input.contextPack?.questionPlan?.requestedShape
|
|
1311
1560
|
? input.contextPack.questionPlan
|
|
@@ -1321,12 +1570,60 @@ async function runAnswerLoop(input) {
|
|
|
1321
1570
|
// "exact certified" route into the generation prompt. The model should decide
|
|
1322
1571
|
// from compatible certified evidence, semantic members, and dbt/runtime
|
|
1323
1572
|
// columns—not copy a customer-grain worked example into a product-grain ask.
|
|
1324
|
-
const promptContextPack = contextPackForRequestedShape(
|
|
1573
|
+
const promptContextPack = contextPackForRequestedShape(executionContextPack, question, questionPlan, kg);
|
|
1325
1574
|
const repairBudgetState = createCascadeBudgetState(input.cascadeBudgetModel);
|
|
1326
|
-
|
|
1575
|
+
// A pre-freeze exploratory selection is already the router's result of
|
|
1576
|
+
// evaluating certified → semantic → relational eligibility. Do not let the
|
|
1577
|
+
// legacy answer loop independently rediscover a broad block or metric and
|
|
1578
|
+
// replace that decision before it has attempted the selected bounded SQL
|
|
1579
|
+
// path. Frozen tiers retain their existing plan binding behavior.
|
|
1580
|
+
if (forcedExploratoryTier && input.contextPack && !exploratoryClosureContextPack) {
|
|
1581
|
+
const text = 'The router-selected exploratory path no longer has a complete same-snapshot physical relation closure, so DQL did not send SQL generation or execute a query.';
|
|
1582
|
+
return {
|
|
1583
|
+
kind: 'no_answer',
|
|
1584
|
+
sourceTier: 'no_answer',
|
|
1585
|
+
certification: 'analyst_review_required',
|
|
1586
|
+
reviewStatus: 'none',
|
|
1587
|
+
confidence: 0,
|
|
1588
|
+
text,
|
|
1589
|
+
answer: text,
|
|
1590
|
+
refusalCode: 'grounding_gap',
|
|
1591
|
+
refusalDetails: { code: 'grounding_gap', message: text },
|
|
1592
|
+
citations: contextPackCitations(input.contextPack, 8),
|
|
1593
|
+
contextPack: input.contextPack,
|
|
1594
|
+
considered,
|
|
1595
|
+
providerUsed: provider.name,
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
const authoritativePlanBinding = !forcedExploratoryTier && input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1327
1599
|
? input.resolvedPlanExecutionBinding
|
|
1328
1600
|
: undefined;
|
|
1329
|
-
|
|
1601
|
+
// `freezeLegacySemanticSelection()` attaches an authoritative-shaped v2
|
|
1602
|
+
// plan for direct AnswerLoop compatibility. It is not the router-owned
|
|
1603
|
+
// semantic cascade decision. Only an explicit selected semantic tier may
|
|
1604
|
+
// make an adapter/graph miss terminal; direct callers retain the established
|
|
1605
|
+
// deterministic metric path, which still applies the ordinary SQL safety
|
|
1606
|
+
// validation before it can execute.
|
|
1607
|
+
const legacyDirectSemanticPlan = input.selectedCascadeTier === undefined
|
|
1608
|
+
&& input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1609
|
+
&& input.resolvedAnalyticalPlan.recommendedRoute === 'semantic'
|
|
1610
|
+
&& !frozenSemanticRoute;
|
|
1611
|
+
// A semantic freeze is an immutable execution authority, not a hint for the
|
|
1612
|
+
// legacy loop. In particular, projects that have indexed semantic metadata
|
|
1613
|
+
// but have not configured the corresponding local semantic adapter used to
|
|
1614
|
+
// continue into generated SQL. Stop here with the selected plan's typed
|
|
1615
|
+
// diagnostic instead; no provider, tool, or SQL work is allowed to replace
|
|
1616
|
+
// it after freeze.
|
|
1617
|
+
if (!forcedExploratoryTier && frozenSemanticRoute && !input.semanticLayer) {
|
|
1618
|
+
return frozenSemanticPlanFailureAnswer({
|
|
1619
|
+
answerInput: input,
|
|
1620
|
+
considered,
|
|
1621
|
+
providerName: provider.name,
|
|
1622
|
+
code: 'SEMANTIC_LAYER_REQUIRED',
|
|
1623
|
+
reason: `The pinned semantic adapter for ${frozenSemanticRoute.ref ?? 'the selected semantic plan'} is unavailable.`,
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
if (!forcedExploratoryTier && input.analyticalPeriodResolutionFailure) {
|
|
1330
1627
|
const failure = input.analyticalPeriodResolutionFailure;
|
|
1331
1628
|
const structuredFailureCode = failure.error && typeof failure.error === 'object'
|
|
1332
1629
|
&& typeof failure.error.code === 'string'
|
|
@@ -1362,7 +1659,7 @@ async function runAnswerLoop(input) {
|
|
|
1362
1659
|
providerUsed: provider.name,
|
|
1363
1660
|
};
|
|
1364
1661
|
}
|
|
1365
|
-
if (input.analyticalExecutionGraphFailure) {
|
|
1662
|
+
if (!forcedExploratoryTier && input.analyticalExecutionGraphFailure) {
|
|
1366
1663
|
const failure = input.analyticalExecutionGraphFailure;
|
|
1367
1664
|
const analyticalFailure = analyticalFailureForInput(input, {
|
|
1368
1665
|
error: { code: failure.code, message: failure.reason },
|
|
@@ -1387,7 +1684,7 @@ async function runAnswerLoop(input) {
|
|
|
1387
1684
|
providerUsed: provider.name,
|
|
1388
1685
|
};
|
|
1389
1686
|
}
|
|
1390
|
-
if (input.semanticGraphExecutionBinding?.status === 'blocked') {
|
|
1687
|
+
if (!forcedExploratoryTier && input.semanticGraphExecutionBinding?.status === 'blocked') {
|
|
1391
1688
|
const failure = input.semanticGraphExecutionBinding;
|
|
1392
1689
|
const analyticalFailure = analyticalFailureForInput(input, {
|
|
1393
1690
|
error: { code: failure.code, message: failure.reason },
|
|
@@ -1412,17 +1709,21 @@ async function runAnswerLoop(input) {
|
|
|
1412
1709
|
providerUsed: provider.name,
|
|
1413
1710
|
};
|
|
1414
1711
|
}
|
|
1415
|
-
if (input.semanticGraphExecutionBinding?.status === 'ready' && input.analyticalExecutionGraph) {
|
|
1712
|
+
if (!forcedExploratoryTier && input.semanticGraphExecutionBinding?.status === 'ready' && input.analyticalExecutionGraph) {
|
|
1416
1713
|
return executeSemanticAnalyticalGraph({
|
|
1417
1714
|
input,
|
|
1418
1715
|
binding: input.semanticGraphExecutionBinding,
|
|
1419
1716
|
graph: input.analyticalExecutionGraph,
|
|
1420
|
-
|
|
1717
|
+
// The execution registry identifies the adapter metric, but its compact
|
|
1718
|
+
// node payload may omit the relationship-path metadata that was frozen
|
|
1719
|
+
// on the RAP. Re-deriving aggregation proof from that projection makes
|
|
1720
|
+
// an already accepted native MetricFlow grouping fail post-freeze.
|
|
1721
|
+
capability: input.semanticGraphExecutionBinding.capability,
|
|
1421
1722
|
considered,
|
|
1422
1723
|
providerName: provider.name,
|
|
1423
1724
|
});
|
|
1424
1725
|
}
|
|
1425
|
-
if (input.governedAnalyticalGraphCompilation?.status === 'blocked') {
|
|
1726
|
+
if (!forcedExploratoryTier && input.governedAnalyticalGraphCompilation?.status === 'blocked') {
|
|
1426
1727
|
const failure = input.governedAnalyticalGraphCompilation;
|
|
1427
1728
|
const analyticalFailure = analyticalFailureForInput(input, {
|
|
1428
1729
|
error: { code: failure.code, message: failure.reason },
|
|
@@ -1447,7 +1748,7 @@ async function runAnswerLoop(input) {
|
|
|
1447
1748
|
providerUsed: provider.name,
|
|
1448
1749
|
};
|
|
1449
1750
|
}
|
|
1450
|
-
if (input.governedAnalyticalGraphCompilation?.status === 'compiled' && input.analyticalExecutionGraph) {
|
|
1751
|
+
if (!forcedExploratoryTier && input.governedAnalyticalGraphCompilation?.status === 'compiled' && input.analyticalExecutionGraph) {
|
|
1451
1752
|
return executeGovernedRelationalAnalyticalGraph({
|
|
1452
1753
|
input,
|
|
1453
1754
|
compilation: input.governedAnalyticalGraphCompilation,
|
|
@@ -1458,7 +1759,7 @@ async function runAnswerLoop(input) {
|
|
|
1458
1759
|
});
|
|
1459
1760
|
}
|
|
1460
1761
|
const governedRelationalCompilation = input.governedRelationalCompilation;
|
|
1461
|
-
if (governedRelationalCompilation?.status === 'blocked') {
|
|
1762
|
+
if (!forcedExploratoryTier && governedRelationalCompilation?.status === 'blocked') {
|
|
1462
1763
|
const analyticalFailure = analyticalFailureForInput(input, {
|
|
1463
1764
|
error: { code: governedRelationalCompilation.code, message: governedRelationalCompilation.reason },
|
|
1464
1765
|
phase: 'compilation',
|
|
@@ -1485,7 +1786,7 @@ async function runAnswerLoop(input) {
|
|
|
1485
1786
|
providerUsed: provider.name,
|
|
1486
1787
|
};
|
|
1487
1788
|
}
|
|
1488
|
-
if (governedRelationalCompilation?.status === 'compiled') {
|
|
1789
|
+
if (!forcedExploratoryTier && governedRelationalCompilation?.status === 'compiled') {
|
|
1489
1790
|
const dqlArtifact = renderGovernedRelationalDqlArtifact(governedRelationalCompilation);
|
|
1490
1791
|
let result;
|
|
1491
1792
|
let executionError;
|
|
@@ -1532,7 +1833,7 @@ async function runAnswerLoop(input) {
|
|
|
1532
1833
|
providerUsed: provider.name,
|
|
1533
1834
|
};
|
|
1534
1835
|
}
|
|
1535
|
-
if (authoritativePlanBinding?.status === 'blocked') {
|
|
1836
|
+
if (authoritativePlanBinding?.status === 'blocked' && !legacyDirectSemanticPlan) {
|
|
1536
1837
|
// An ambiguous singular reference is answerable the moment the user says
|
|
1537
1838
|
// which one they meant, so offer the candidates rather than dead-ending.
|
|
1538
1839
|
// The old refusal shipped the binder's internal sentence as the answer
|
|
@@ -1549,9 +1850,15 @@ async function runAnswerLoop(input) {
|
|
|
1549
1850
|
const text = clarificationOptions?.length
|
|
1550
1851
|
? `The previous answer listed ${choices.values.length} ${humanizeDeicticDimension(choices.dimension)}. Which one did you mean?`
|
|
1551
1852
|
: `The resolved analytical plan cannot execute safely: ${authoritativePlanBinding.reason}`;
|
|
1853
|
+
const frozenCertifiedPlan = input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1854
|
+
&& input.resolvedAnalyticalPlan.capability === 'certified_execution';
|
|
1855
|
+
const frozenCertifiedRef = input.resolvedAnalyticalPlan?.executionId;
|
|
1552
1856
|
return {
|
|
1553
1857
|
kind: 'no_answer',
|
|
1554
|
-
|
|
1858
|
+
// A binding/adapter failure after the router selected a certified
|
|
1859
|
+
// execution authority is still a certified-tier terminal. It must not
|
|
1860
|
+
// be relabelled as generated merely because no rows were produced.
|
|
1861
|
+
sourceTier: frozenCertifiedPlan ? 'certified_artifact' : 'no_answer',
|
|
1555
1862
|
certification: 'analyst_review_required',
|
|
1556
1863
|
reviewStatus: 'none',
|
|
1557
1864
|
confidence: 0,
|
|
@@ -1563,6 +1870,15 @@ async function runAnswerLoop(input) {
|
|
|
1563
1870
|
: 'grounding_gap',
|
|
1564
1871
|
refusalDetails: { code: authoritativePlanBinding.code, message: authoritativePlanBinding.reason },
|
|
1565
1872
|
...(clarificationOptions?.length ? { clarificationOptions } : {}),
|
|
1873
|
+
...(frozenCertifiedPlan ? {
|
|
1874
|
+
route: {
|
|
1875
|
+
tier: 'certified_block',
|
|
1876
|
+
label: frozenCertifiedRef
|
|
1877
|
+
? `The selected certified block ${frozenCertifiedRef} could not execute its frozen plan.`
|
|
1878
|
+
: 'The selected certified block could not execute its frozen plan.',
|
|
1879
|
+
...(frozenCertifiedRef ? { ref: frozenCertifiedRef } : {}),
|
|
1880
|
+
},
|
|
1881
|
+
} : {}),
|
|
1566
1882
|
citations: contextPackCitations(input.contextPack, 8),
|
|
1567
1883
|
considered,
|
|
1568
1884
|
contextPack: input.contextPack,
|
|
@@ -1593,24 +1909,26 @@ async function runAnswerLoop(input) {
|
|
|
1593
1909
|
? authoritativePlanBinding
|
|
1594
1910
|
: undefined;
|
|
1595
1911
|
const preferredSemanticMetric = authoritativeSemanticBinding?.metricNode
|
|
1596
|
-
?? (input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1912
|
+
?? (input.resolvedAnalyticalPlan?.mode === 'authoritative' && !legacyDirectSemanticPlan
|
|
1597
1913
|
? undefined
|
|
1598
1914
|
: resolvePreferredSemanticMetric([input.preferredExecutionId, ...(input.preferredEvidenceIds ?? [])], semanticMetricNodes, kg));
|
|
1599
1915
|
const semanticLayerForExec = input.semanticLayer;
|
|
1600
1916
|
const canExecuteSemanticMetricForMatch = input.canExecuteSemanticMetric
|
|
1601
1917
|
?? (semanticLayerForExec ? (name) => semanticLayerForExec.canComposeMetric(name) : undefined);
|
|
1602
|
-
let semanticMetricMatch =
|
|
1603
|
-
?
|
|
1604
|
-
:
|
|
1605
|
-
?
|
|
1606
|
-
:
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
? {
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1918
|
+
let semanticMetricMatch = forcedExploratoryTier
|
|
1919
|
+
? null
|
|
1920
|
+
: preferredSemanticMetric
|
|
1921
|
+
? { metric: preferredSemanticMetric, score: 1, basis: 'name' }
|
|
1922
|
+
: input.resolvedAnalyticalPlan?.mode === 'authoritative' && !legacyDirectSemanticPlan
|
|
1923
|
+
? null
|
|
1924
|
+
: await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
|
|
1925
|
+
measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
|
|
1926
|
+
...(canExecuteSemanticMetricForMatch ? { canExecute: canExecuteSemanticMetricForMatch } : {}),
|
|
1927
|
+
...(snapshotVectorMetricShortlist(input.contextPack).length
|
|
1928
|
+
? { vectorMetricShortlist: snapshotVectorMetricShortlist(input.contextPack) }
|
|
1929
|
+
: {}),
|
|
1930
|
+
...semanticMetricEmbeddingOptions(input.embeddingProvider),
|
|
1931
|
+
}).catch(() => null);
|
|
1614
1932
|
// Stage 1: certified artifact match. Blocks can be executed; dashboards,
|
|
1615
1933
|
// Apps, and notebooks are returned as governed citations/navigation targets.
|
|
1616
1934
|
const drilldownCertifiedHit = input.followUp?.kind === 'drilldown'
|
|
@@ -1630,8 +1948,10 @@ async function runAnswerLoop(input) {
|
|
|
1630
1948
|
&& objectNameInQuestion(question, node)
|
|
1631
1949
|
&& /\b(run|use|open|show|execute|certified|saved|block)\b/i.test(question));
|
|
1632
1950
|
});
|
|
1633
|
-
const shouldTryCertifiedRoute =
|
|
1634
|
-
|
|
1951
|
+
const shouldTryCertifiedRoute = !forcedExploratoryTier
|
|
1952
|
+
&& (shouldUseCertifiedRoute(catalogRoute, intent)
|
|
1953
|
+
|| questionPlan.mode === 'definition'
|
|
1954
|
+
|| explicitlyRequestedCertifiedBlock);
|
|
1635
1955
|
const catalogCertifiedHit = shouldTryCertifiedRoute
|
|
1636
1956
|
? certifiedHitFromContextPack(input.contextPack, kg)
|
|
1637
1957
|
: null;
|
|
@@ -1641,7 +1961,9 @@ async function runAnswerLoop(input) {
|
|
|
1641
1961
|
// metric, so a high-scoring `top_beverage_customers` catalog hit could answer a
|
|
1642
1962
|
// product-type → product-name flow request with customer rows.
|
|
1643
1963
|
const unsafeCatalogCertifiedHit = catalogCertifiedHit?.node.kind === 'block'
|
|
1644
|
-
&& !hasCertifiedNodeFit(question, questionPlan, catalogCertifiedHit.node
|
|
1964
|
+
&& !hasCertifiedNodeFit(question, questionPlan, catalogCertifiedHit.node, {
|
|
1965
|
+
uniqueExactExampleContract: hasUniqueExactCatalogExample(question, input.contextPack, catalogCertifiedHit.node),
|
|
1966
|
+
})
|
|
1645
1967
|
? null
|
|
1646
1968
|
: catalogCertifiedHit;
|
|
1647
1969
|
const fallbackCertifiedHit = shouldTryCertifiedRoute ? pickCertifiedArtifact({
|
|
@@ -1658,13 +1980,85 @@ async function runAnswerLoop(input) {
|
|
|
1658
1980
|
&& authoritativePlanBinding.kind === 'certified'
|
|
1659
1981
|
? authoritativePlanBinding
|
|
1660
1982
|
: undefined;
|
|
1983
|
+
const namedDefinitionArtifact = input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1984
|
+
? null
|
|
1985
|
+
: uniqueNamedCertifiedDefinitionArtifact(question, kg, authorizedDomains);
|
|
1661
1986
|
let artifactHit = authoritativeCertifiedBinding
|
|
1662
1987
|
? { node: authoritativeCertifiedBinding.node, score: 1 }
|
|
1663
1988
|
: input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1664
1989
|
? null
|
|
1665
|
-
: drilldownCertifiedHit ?? unsafeCatalogCertifiedHit
|
|
1990
|
+
: namedDefinitionArtifact ?? drilldownCertifiedHit ?? unsafeCatalogCertifiedHit
|
|
1666
1991
|
?? (catalogCertifiedHit ? null : fallbackCertifiedHit);
|
|
1667
1992
|
let certifiedExecutionFallback;
|
|
1993
|
+
// A named certified artifact-definition request is documentation about that
|
|
1994
|
+
// artifact, not a request to execute its SQL. The planner strips the
|
|
1995
|
+
// artifact identifier from its output contract, and this deterministic path
|
|
1996
|
+
// returns only authored metadata. It is deliberately narrower than a bare
|
|
1997
|
+
// "what is revenue" question: the user must name the certified block and use
|
|
1998
|
+
// an explicit definition form before execution is bypassed.
|
|
1999
|
+
if (artifactHit?.node.kind === 'block'
|
|
2000
|
+
&& artifactHit.node.status === 'certified'
|
|
2001
|
+
&& isUniqueNamedCertifiedDefinitionQuestion(question, artifactHit.node, kg, authorizedDomains)) {
|
|
2002
|
+
const artifact = artifactHit.node;
|
|
2003
|
+
const outputs = [...new Set([
|
|
2004
|
+
...(artifact.outputContract ?? []).map((output) => output.name),
|
|
2005
|
+
...(artifact.declaredOutputs ?? []),
|
|
2006
|
+
...(artifact.outputs ?? []).map((output) => output.name),
|
|
2007
|
+
].map((output) => output.trim()).filter(Boolean))].slice(0, 16);
|
|
2008
|
+
const metadataLines = [
|
|
2009
|
+
artifact.description ?? artifact.llmContext ?? 'No authored description is available for this certified artifact.',
|
|
2010
|
+
outputs.length > 0 ? `Declared outputs: ${outputs.join(', ')}.` : undefined,
|
|
2011
|
+
artifact.grain ? `Grain: ${artifact.grain}.` : undefined,
|
|
2012
|
+
artifact.owner ? `Owner: ${artifact.owner}.` : undefined,
|
|
2013
|
+
'This is certified artifact metadata, not a query result.',
|
|
2014
|
+
].filter((line) => Boolean(line));
|
|
2015
|
+
const citations = [{
|
|
2016
|
+
nodeId: artifact.nodeId,
|
|
2017
|
+
kind: artifact.kind,
|
|
2018
|
+
name: artifact.name,
|
|
2019
|
+
gitSha: artifact.gitSha,
|
|
2020
|
+
sourceTier: 'certified_artifact',
|
|
2021
|
+
provenance: artifact.provenance,
|
|
2022
|
+
}];
|
|
2023
|
+
const analysisPlan = buildAnalysisPlan({
|
|
2024
|
+
question,
|
|
2025
|
+
intent: 'definition_lookup',
|
|
2026
|
+
routeReason: 'The question explicitly requested the meaning of one named certified artifact; no SQL execution was requested.',
|
|
2027
|
+
selectedNodes: [artifact],
|
|
2028
|
+
schemaContext,
|
|
2029
|
+
});
|
|
2030
|
+
const text = `Certified artifact **${artifact.name}**\n\n${metadataLines.join('\n\n')}`;
|
|
2031
|
+
return {
|
|
2032
|
+
kind: 'certified',
|
|
2033
|
+
sourceTier: 'certified_artifact',
|
|
2034
|
+
certification: 'certified',
|
|
2035
|
+
reviewStatus: 'certified',
|
|
2036
|
+
confidence: 0.99,
|
|
2037
|
+
text,
|
|
2038
|
+
answer: text,
|
|
2039
|
+
block: artifact,
|
|
2040
|
+
sourceCertifiedBlock: artifact.name,
|
|
2041
|
+
trustLabel: 'certified',
|
|
2042
|
+
citations,
|
|
2043
|
+
memoryContext: input.memoryContext,
|
|
2044
|
+
analysisPlan,
|
|
2045
|
+
evidence: buildCertifiedEvidence({
|
|
2046
|
+
question,
|
|
2047
|
+
artifact,
|
|
2048
|
+
businessHits,
|
|
2049
|
+
semanticHits,
|
|
2050
|
+
manifestHits,
|
|
2051
|
+
considered,
|
|
2052
|
+
executorWasAvailable: false,
|
|
2053
|
+
citations,
|
|
2054
|
+
memoryContext: input.memoryContext ?? [],
|
|
2055
|
+
analysisPlan,
|
|
2056
|
+
}),
|
|
2057
|
+
contextPack: input.contextPack,
|
|
2058
|
+
considered,
|
|
2059
|
+
providerUsed: provider.name,
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
1668
2062
|
// Certified remains first when it actually covers the question. If the
|
|
1669
2063
|
// retrieved block does not fit but a governed semantic metric does, never
|
|
1670
2064
|
// let the broad catalog match pre-empt Lane 2.
|
|
@@ -1704,6 +2098,7 @@ async function runAnswerLoop(input) {
|
|
|
1704
2098
|
if (artifactHit && businessContextTerminal) {
|
|
1705
2099
|
let result;
|
|
1706
2100
|
let executionError;
|
|
2101
|
+
let executionFailureDetail;
|
|
1707
2102
|
if (artifactHit.node.kind === 'block' && input.executeCertifiedBlock) {
|
|
1708
2103
|
try {
|
|
1709
2104
|
result = await input.executeCertifiedBlock(artifactHit.node, {
|
|
@@ -1716,6 +2111,7 @@ async function runAnswerLoop(input) {
|
|
|
1716
2111
|
result = trimResultToRequestedTopN(result, questionPlan);
|
|
1717
2112
|
}
|
|
1718
2113
|
catch (err) {
|
|
2114
|
+
executionFailureDetail = analyticalErrorDetail(err);
|
|
1719
2115
|
executionError = err instanceof Error ? err.message : String(err);
|
|
1720
2116
|
}
|
|
1721
2117
|
}
|
|
@@ -1773,7 +2169,16 @@ async function runAnswerLoop(input) {
|
|
|
1773
2169
|
providerUsed: provider.name,
|
|
1774
2170
|
};
|
|
1775
2171
|
}
|
|
1776
|
-
const resultShapeWarnings = result
|
|
2172
|
+
const resultShapeWarnings = result
|
|
2173
|
+
? validateAnswerResultShape(questionPlan, result, {
|
|
2174
|
+
outputBindings: authoritativeCertifiedBinding
|
|
2175
|
+
? frozenCertifiedResultShapeBindings(input.resolvedAnalyticalPlan, artifactHit.node, questionPlan, {
|
|
2176
|
+
uniqueExactExampleContract: hasUniqueExactCatalogExample(question, input.contextPack, artifactHit.node),
|
|
2177
|
+
})
|
|
2178
|
+
: [],
|
|
2179
|
+
requireBoundMeasures: Boolean(authoritativeCertifiedBinding),
|
|
2180
|
+
}).warnings
|
|
2181
|
+
: [];
|
|
1777
2182
|
// When a certified block's execution was ATTEMPTED and FAILED, the answer
|
|
1778
2183
|
// cannot wear the certified badge — a failed run has no data to stand behind.
|
|
1779
2184
|
// Downgrade to analyst_review_required (the error is surfaced in the text).
|
|
@@ -1812,6 +2217,13 @@ async function runAnswerLoop(input) {
|
|
|
1812
2217
|
? undefined
|
|
1813
2218
|
: questionPlan.requestedShape.topN?.n);
|
|
1814
2219
|
const authoritativeCertifiedFailure = Boolean(authoritativeCertifiedBinding && executionError);
|
|
2220
|
+
const observabilityExecutionFailure = authoritativeCertifiedFailure
|
|
2221
|
+
? observabilityFailureForFrozenExecution(executionFailureDetail)
|
|
2222
|
+
: undefined;
|
|
2223
|
+
// A selected certified block which returns an incomplete tuple is a
|
|
2224
|
+
// same-tier terminal failure. It is not a reason to reinterpret meaning,
|
|
2225
|
+
// label the partial rows generated, or retry another route after freeze.
|
|
2226
|
+
const authoritativeCertifiedShapeFailure = Boolean(authoritativeCertifiedBinding && !executionError && resultShapeWarnings.length > 0);
|
|
1815
2227
|
const recoverableCertifiedFailure = artifactHit.node.kind === 'block'
|
|
1816
2228
|
&& executionError !== undefined
|
|
1817
2229
|
&& !authoritativeCertifiedFailure
|
|
@@ -1826,18 +2238,39 @@ async function runAnswerLoop(input) {
|
|
|
1826
2238
|
artifactHit = null;
|
|
1827
2239
|
}
|
|
1828
2240
|
else {
|
|
2241
|
+
const frozenCertifiedRoute = authoritativeCertifiedBinding
|
|
2242
|
+
? {
|
|
2243
|
+
tier: 'certified_block',
|
|
2244
|
+
label: certifiedShapePassed
|
|
2245
|
+
? `Answered from certified block ${artifactHit.node.name}`
|
|
2246
|
+
: `The selected certified block ${artifactHit.node.name} could not complete its frozen output contract.`,
|
|
2247
|
+
ref: artifactHit.node.name,
|
|
2248
|
+
}
|
|
2249
|
+
: undefined;
|
|
1829
2250
|
return {
|
|
1830
|
-
kind: authoritativeCertifiedFailure
|
|
1831
|
-
|
|
2251
|
+
kind: authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
|
|
2252
|
+
? 'no_answer'
|
|
2253
|
+
: certifiedShapePassed ? 'certified' : 'uncertified',
|
|
2254
|
+
// Preserve the selected certified authority even when its own
|
|
2255
|
+
// execution/result contract fails. The failure is terminal and has
|
|
2256
|
+
// review-required trust, but it is not generated SQL.
|
|
2257
|
+
sourceTier,
|
|
1832
2258
|
certification: certifiedShapePassed ? 'certified' : 'analyst_review_required',
|
|
1833
|
-
reviewStatus: authoritativeCertifiedFailure
|
|
2259
|
+
reviewStatus: authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
|
|
2260
|
+
? 'none'
|
|
2261
|
+
: certifiedShapePassed ? 'certified' : 'analyst_review_required',
|
|
1834
2262
|
confidence: certifiedShapePassed ? 0.95 : 0.45,
|
|
1835
2263
|
text,
|
|
1836
2264
|
answer: text,
|
|
1837
2265
|
block: artifactHit.node.kind === 'block' ? artifactHit.node : undefined,
|
|
1838
2266
|
result,
|
|
1839
2267
|
executionError,
|
|
1840
|
-
...(
|
|
2268
|
+
...(observabilityExecutionFailure ? { observabilityExecutionFailure } : {}),
|
|
2269
|
+
...(authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
|
|
2270
|
+
? { refusalCode: 'grounding_gap' }
|
|
2271
|
+
: {}),
|
|
2272
|
+
...(authoritativeCertifiedShapeFailure ? { certifiedResultShapeFailure: true } : {}),
|
|
2273
|
+
...(frozenCertifiedRoute ? { route: frozenCertifiedRoute } : {}),
|
|
1841
2274
|
sql: result?.sql,
|
|
1842
2275
|
dqlArtifact,
|
|
1843
2276
|
trustLabel: certifiedShapePassed ? input.contextPack?.trustLabel ?? 'certified' : 'mixed',
|
|
@@ -1875,7 +2308,7 @@ async function runAnswerLoop(input) {
|
|
|
1875
2308
|
// a governed metric can answer. Match by name + synonyms + measure family + hybrid
|
|
1876
2309
|
// rank over the FTS semantic hits, then ALL metric KG nodes (revenue ⇄
|
|
1877
2310
|
// cumulative_revenue). Certified-first is still preserved (checked above).
|
|
1878
|
-
const clarifyBeforeGeneration = shouldClarifyBeforeGeneration({
|
|
2311
|
+
const clarifyBeforeGeneration = !forcedExploratoryTier && shouldClarifyBeforeGeneration({
|
|
1879
2312
|
intent,
|
|
1880
2313
|
routeDecision: catalogRoute,
|
|
1881
2314
|
hasSemanticMetricMatch: Boolean(semanticMetricMatch),
|
|
@@ -1933,12 +2366,14 @@ async function runAnswerLoop(input) {
|
|
|
1933
2366
|
// wins over raw dbt manifest context; memory is appended last as advisory.
|
|
1934
2367
|
// A confident metric match forces the semantic tier even when FTS returned no
|
|
1935
2368
|
// semantic hits, so the governed metric (not refusal) answers the question.
|
|
1936
|
-
const activeTier =
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
? '
|
|
1941
|
-
:
|
|
2369
|
+
const activeTier = forcedExploratoryTier
|
|
2370
|
+
? 'dbt_manifest'
|
|
2371
|
+
: sourceTierFromContextPack(input.contextPack)
|
|
2372
|
+
?? (semanticHits.length > 0 || semanticMetricMatch
|
|
2373
|
+
? 'semantic_layer'
|
|
2374
|
+
: manifestHits.length > 0
|
|
2375
|
+
? 'dbt_manifest'
|
|
2376
|
+
: 'dbt_manifest');
|
|
1942
2377
|
const reviewRequiredArtifactHits = artifactHits
|
|
1943
2378
|
.filter((hit) => hit.score >= CERTIFIED_HIT_THRESHOLD && !isCertifiedHit(hit, kg))
|
|
1944
2379
|
.slice(0, 4);
|
|
@@ -1971,7 +2406,13 @@ async function runAnswerLoop(input) {
|
|
|
1971
2406
|
{ hits: businessHits.slice(0, 4), reserve: 3 },
|
|
1972
2407
|
{ hits: manifestHits, reserve: 3 },
|
|
1973
2408
|
], 14);
|
|
1974
|
-
|
|
2409
|
+
// A frozen exploratory selection gets its relation/column/proof context only
|
|
2410
|
+
// from `executionContextPack`. KG hit prose is useful diagnostics, but it
|
|
2411
|
+
// is not execution authority and must not tell a provider about another
|
|
2412
|
+
// same-snapshot relation it could then attempt to query.
|
|
2413
|
+
const contextNodes = forcedExploratoryTier
|
|
2414
|
+
? []
|
|
2415
|
+
: mergeNodes(followUpSourceBlock && input.followUp?.kind === 'drilldown' ? [followUpSourceBlock] : [], (contextHits.length > 0 ? contextHits : considered.slice(0, 6)).map((h) => h.node)).filter((node) => questionDomainScope.length === 0 || !node.domain || questionDomainScope.includes(node.domain));
|
|
1975
2416
|
const kgJoinPathHints = buildKgJoinPathHints(kg, contextNodes, questionPlan);
|
|
1976
2417
|
const contextBlocks = contextNodes.filter((node) => {
|
|
1977
2418
|
if (node.kind !== 'block')
|
|
@@ -1991,10 +2432,10 @@ async function runAnswerLoop(input) {
|
|
|
1991
2432
|
const messages = [
|
|
1992
2433
|
{ role: 'system', content: SYSTEM_PROMPT },
|
|
1993
2434
|
];
|
|
1994
|
-
const skillsPrompt = buildSkillsPrompt(selectedSkills, userId ?? null);
|
|
2435
|
+
const skillsPrompt = forcedExploratoryTier ? '' : buildSkillsPrompt(selectedSkills, userId ?? null);
|
|
1995
2436
|
if (skillsPrompt)
|
|
1996
2437
|
messages.push({ role: 'system', content: skillsPrompt });
|
|
1997
|
-
const analyticalPlan = input.manifest
|
|
2438
|
+
const analyticalPlan = !forcedExploratoryTier && input.manifest
|
|
1998
2439
|
? planAnalyticalPath(input.manifest, {
|
|
1999
2440
|
entityIds: inferAnalyticalEntityIds(question, contextNodes, input.manifest),
|
|
2000
2441
|
ownerDomain: input.domainContext?.activeDomain ?? input.domain,
|
|
@@ -2319,8 +2760,21 @@ async function runAnswerLoop(input) {
|
|
|
2319
2760
|
contextPack: input.contextPack,
|
|
2320
2761
|
considered,
|
|
2321
2762
|
providerUsed: provider.name,
|
|
2763
|
+
...(frozenSemanticRoute ? { route: frozenSemanticRoute } : {}),
|
|
2322
2764
|
};
|
|
2323
2765
|
}
|
|
2766
|
+
// The semantic branch above has exhausted the exact plan's compiler and
|
|
2767
|
+
// adapter choices. Do not let `metricFirst` or the ordinary generated
|
|
2768
|
+
// proposal branch reinterpret the question after a semantic freeze.
|
|
2769
|
+
if (frozenSemanticRoute && !semanticBridgeAnswer) {
|
|
2770
|
+
return frozenSemanticPlanFailureAnswer({
|
|
2771
|
+
answerInput: input,
|
|
2772
|
+
considered,
|
|
2773
|
+
providerName: provider.name,
|
|
2774
|
+
code: 'COMPILATION_FAILED',
|
|
2775
|
+
reason: `The pinned semantic adapter could not compose ${frozenSemanticRoute.ref ?? 'the selected semantic plan'} without changing the frozen plan.`,
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2324
2778
|
const metricFirst = semanticMetricMatch
|
|
2325
2779
|
? buildGovernedMetricFirstSql({
|
|
2326
2780
|
metric: semanticMetricMatch.metric,
|
|
@@ -2346,7 +2800,7 @@ async function runAnswerLoop(input) {
|
|
|
2346
2800
|
// (pre-validated, since the wrapper only restricts an already-certified result)
|
|
2347
2801
|
// and is labeled BELOW certified. Falls through to generation on any miss.
|
|
2348
2802
|
let certifiedAdaptation;
|
|
2349
|
-
if (!semanticBridgeAnswer && !metricFirst && input.executeGeneratedSql) {
|
|
2803
|
+
if (!forcedExploratoryTier && !semanticBridgeAnswer && !metricFirst && input.executeGeneratedSql) {
|
|
2350
2804
|
const fit = input.contextPack?.routeDecision?.blockFit;
|
|
2351
2805
|
const sourceBlock = input.contextPack?.allowedSqlContext?.sourceBlockSql?.[0];
|
|
2352
2806
|
if (fit && sourceBlock?.sql) {
|
|
@@ -2652,14 +3106,36 @@ async function runAnswerLoop(input) {
|
|
|
2652
3106
|
|| (input.contextPack?.allowedSqlContext?.relations.length ?? 0) > 0
|
|
2653
3107
|
|| (input.contextPack?.allowedSqlContext?.sourceBlockSql.length ?? 0) > 0
|
|
2654
3108
|
|| contextBlocks.length > 0;
|
|
3109
|
+
// A selected bounded-exploration RAP is stronger than the advisory legacy
|
|
3110
|
+
// analytical-path diagnosis. The latter may still call raw output terms a
|
|
3111
|
+
// semantic "missing dimension" even after the router has proven one safe
|
|
3112
|
+
// physical relation and frozen the exploratory tuple. Do not let that
|
|
3113
|
+
// advisory pre-freeze diagnosis suppress the one explicitly authorized
|
|
3114
|
+
// same-plan model-decline repair; the host re-validates the exact proposal
|
|
3115
|
+
// against the frozen closure before it can execute.
|
|
3116
|
+
const frozenExploratoryRepair = frozenExploratoryModelRepairAuthority(input);
|
|
2655
3117
|
if (!parsed.sql && !governedMetricAnswer && wantsGeneratedData && hasGeneratableContext
|
|
2656
|
-
&& (
|
|
3118
|
+
&& (frozenExploratoryRepair
|
|
3119
|
+
|| analyticalPlan?.safe !== false
|
|
3120
|
+
|| analyticalPlanAllowsDbtExploration(analyticalPlan))) {
|
|
2657
3121
|
try {
|
|
2658
3122
|
proposed = await generateProposalWithOptionalTools({
|
|
2659
3123
|
provider,
|
|
2660
|
-
messages: [...messages, {
|
|
2661
|
-
|
|
2662
|
-
|
|
3124
|
+
messages: [...messages, {
|
|
3125
|
+
role: 'system',
|
|
3126
|
+
content: [
|
|
3127
|
+
FORCE_JOIN_INSTRUCTION,
|
|
3128
|
+
...(frozenExploratoryRepair
|
|
3129
|
+
? [renderFrozenExploratoryRepairContract(input.resolvedAnalyticalPlan, questionPlan)]
|
|
3130
|
+
: []),
|
|
3131
|
+
].join('\n\n'),
|
|
3132
|
+
}],
|
|
3133
|
+
// A router-frozen exploratory plan may consume exactly one additional
|
|
3134
|
+
// provider transport to correct a model decline. The host carries the
|
|
3135
|
+
// immutable plan and output tuple into this retry; it is neither a
|
|
3136
|
+
// replan nor a route change. Legacy direct callers retain their
|
|
3137
|
+
// historical composition correction, but do not receive the typed
|
|
3138
|
+
// repair authority below.
|
|
2663
3139
|
tools: [],
|
|
2664
3140
|
questionPlan,
|
|
2665
3141
|
intent,
|
|
@@ -2668,6 +3144,13 @@ async function runAnswerLoop(input) {
|
|
|
2668
3144
|
analysisDepth: input.analysisDepth,
|
|
2669
3145
|
toolCalls: proposalToolCalls,
|
|
2670
3146
|
providerPayloadGuard: input.providerPayloadGuard,
|
|
3147
|
+
...(frozenExploratoryRepair
|
|
3148
|
+
? {
|
|
3149
|
+
dispatchPhase: 'repair',
|
|
3150
|
+
egressPurpose: 'repair_sql',
|
|
3151
|
+
maxProviderDispatches: 1,
|
|
3152
|
+
}
|
|
3153
|
+
: {}),
|
|
2671
3154
|
});
|
|
2672
3155
|
parsed = parseProposal(proposed);
|
|
2673
3156
|
}
|
|
@@ -2742,6 +3225,49 @@ async function runAnswerLoop(input) {
|
|
|
2742
3225
|
}
|
|
2743
3226
|
if (parsed.sql) {
|
|
2744
3227
|
const frozenPlan = input.resolvedAnalyticalPlan;
|
|
3228
|
+
// The result contract is host-owned even in tests or embedded callers
|
|
3229
|
+
// that provide their own execution adapter. Stop before capability
|
|
3230
|
+
// preparation when a generated alias is backed by a different physical
|
|
3231
|
+
// column than the frozen plan (for example `product_id AS order_id`).
|
|
3232
|
+
// The local runtime repeats this check while minting the capability and
|
|
3233
|
+
// persists the matching proof on its receipt.
|
|
3234
|
+
if (input.selectedCascadeTier === 'exploratory_sql' && frozenPlan) {
|
|
3235
|
+
const requiredOutputValidation = validateFrozenRequiredOutputProjection({
|
|
3236
|
+
plan: frozenPlan,
|
|
3237
|
+
sql: parsed.sql,
|
|
3238
|
+
...(input.semanticDriver ? { dialect: input.semanticDriver } : {}),
|
|
3239
|
+
});
|
|
3240
|
+
if (!requiredOutputValidation.ok) {
|
|
3241
|
+
const failedOutputs = [...new Set([
|
|
3242
|
+
...requiredOutputValidation.missingOutputs,
|
|
3243
|
+
...requiredOutputValidation.bindingMismatches,
|
|
3244
|
+
])];
|
|
3245
|
+
const message = `The generated exploratory query did not prove the frozen source binding for required output ${failedOutputs.join(', ')}, so DQL did not execute it.`;
|
|
3246
|
+
return {
|
|
3247
|
+
kind: 'no_answer',
|
|
3248
|
+
sourceTier: 'no_answer',
|
|
3249
|
+
certification: 'analyst_review_required',
|
|
3250
|
+
reviewStatus: 'none',
|
|
3251
|
+
confidence: 0,
|
|
3252
|
+
text: message,
|
|
3253
|
+
answer: message,
|
|
3254
|
+
refusalCode: 'policy_blocked',
|
|
3255
|
+
refusalDetails: {
|
|
3256
|
+
code: 'OUTPUT_BINDING_TUPLE_DRIFT',
|
|
3257
|
+
message,
|
|
3258
|
+
},
|
|
3259
|
+
validationWarnings: [
|
|
3260
|
+
'OUTPUT_BINDING_TUPLE_DRIFT',
|
|
3261
|
+
...failedOutputs.map((output) => `Frozen output binding not proven: ${output}`),
|
|
3262
|
+
],
|
|
3263
|
+
citations: contextPackCitations(input.contextPack, 8),
|
|
3264
|
+
memoryContext: input.memoryContext,
|
|
3265
|
+
contextPack: input.contextPack,
|
|
3266
|
+
considered,
|
|
3267
|
+
providerUsed: provider.name,
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
2745
3271
|
if (!governedMetricAnswer && frozenPlan?.mode === 'authoritative' && frozenPlan.schemaVersion === 2) {
|
|
2746
3272
|
const targetFingerprint = input.generatedProposalTargetFingerprint ?? '';
|
|
2747
3273
|
const validation = validateGeneratedAnalyticalProposal({
|
|
@@ -3345,7 +3871,15 @@ async function runAnswerLoop(input) {
|
|
|
3345
3871
|
let result;
|
|
3346
3872
|
let executionError;
|
|
3347
3873
|
let warehouseFailure;
|
|
3874
|
+
let exploratoryClosureDenied = false;
|
|
3875
|
+
let semanticFanoutProbeFailure;
|
|
3348
3876
|
let repairAttempts = 0;
|
|
3877
|
+
// A forced proposal may originate from the bounded analyst loop, but it
|
|
3878
|
+
// cannot bypass a router-owned exploratory decision. In that case the host
|
|
3879
|
+
// still prepares and freezes the exact validated SQL before execution.
|
|
3880
|
+
const exploratoryExecutionSelected = input.selectedCascadeTier === 'exploratory_sql';
|
|
3881
|
+
let initialExploratoryExecution;
|
|
3882
|
+
let exploratoryRepairExecution;
|
|
3349
3883
|
// Repair candidates that were generated but rejected before execution. These
|
|
3350
3884
|
// are diagnostics ABOUT the recovery attempt, not the reason the run failed,
|
|
3351
3885
|
// so they are reported alongside the original error rather than replacing it.
|
|
@@ -3360,7 +3894,7 @@ async function runAnswerLoop(input) {
|
|
|
3360
3894
|
executionError = deepCandidateExecutionError;
|
|
3361
3895
|
warehouseFailure = normalizeWarehouseSqlFailure(deepCandidateExecutionError, input.semanticDriver);
|
|
3362
3896
|
}
|
|
3363
|
-
const executeCurrentSql = () => {
|
|
3897
|
+
const executeCurrentSql = async (authorizationAttempt) => {
|
|
3364
3898
|
const requestedLimit = questionPlan.requestedShape.topN?.scope === 'per_group'
|
|
3365
3899
|
? 200
|
|
3366
3900
|
: questionPlan.requestedShape.topN?.n ?? 200;
|
|
@@ -3386,7 +3920,7 @@ async function runAnswerLoop(input) {
|
|
|
3386
3920
|
outputs: parsed.outputs,
|
|
3387
3921
|
});
|
|
3388
3922
|
const boundedArtifact = artifact ? { ...artifact, limit: requestedLimit } : undefined;
|
|
3389
|
-
if (input.forcedGeneratedProposal) {
|
|
3923
|
+
if (input.forcedGeneratedProposal && !exploratoryExecutionSelected) {
|
|
3390
3924
|
if (!input.agenticSqlExecutionCapability || !input.executeAgenticGeneratedSql) {
|
|
3391
3925
|
throw Object.assign(new Error('The analyst-approved SQL has no live execution capability, so DQL did not execute it.'), {
|
|
3392
3926
|
code: 'AGENTIC_EXECUTION_CAPABILITY_REQUIRED',
|
|
@@ -3394,27 +3928,63 @@ async function runAnswerLoop(input) {
|
|
|
3394
3928
|
}
|
|
3395
3929
|
return input.executeAgenticGeneratedSql(input.agenticSqlExecutionCapability, parsed.sql, boundedArtifact);
|
|
3396
3930
|
}
|
|
3931
|
+
if (exploratoryExecutionSelected) {
|
|
3932
|
+
if (!input.prepareExploratorySqlExecution || !input.executeAgenticGeneratedSql) {
|
|
3933
|
+
throw Object.assign(new Error('The selected exploratory SQL has no host-authorized execution capability, so DQL did not execute it.'), {
|
|
3934
|
+
code: 'EXPLORATORY_EXECUTION_CAPABILITY_REQUIRED',
|
|
3935
|
+
});
|
|
3936
|
+
}
|
|
3937
|
+
// This is the only point at which a router-selected exploratory proposal
|
|
3938
|
+
// crosses from validated text into executable authority. The first call
|
|
3939
|
+
// receives the original one-shot capability. A retryable warehouse
|
|
3940
|
+
// failure may mint exactly one *new* repair capability, explicitly bound
|
|
3941
|
+
// to the first SQL fingerprint; it never reopens meaning or routing.
|
|
3942
|
+
const prepared = authorizationAttempt
|
|
3943
|
+
? await input.prepareExploratorySqlExecution(parsed.sql, boundedArtifact, authorizationAttempt)
|
|
3944
|
+
: (initialExploratoryExecution ??= await input.prepareExploratorySqlExecution(parsed.sql, boundedArtifact));
|
|
3945
|
+
if (authorizationAttempt)
|
|
3946
|
+
exploratoryRepairExecution = prepared;
|
|
3947
|
+
return input.executeAgenticGeneratedSql(prepared.capability, parsed.sql, boundedArtifact);
|
|
3948
|
+
}
|
|
3397
3949
|
if (!input.executeGeneratedSql)
|
|
3398
3950
|
throw new Error('No generated SQL executor is configured.');
|
|
3399
3951
|
return input.executeGeneratedSql(parsed.sql, boundedArtifact);
|
|
3400
3952
|
};
|
|
3401
|
-
if ((input.executeGeneratedSql
|
|
3953
|
+
if ((input.executeGeneratedSql
|
|
3954
|
+
|| input.executeDqlArtifact
|
|
3955
|
+
|| (input.forcedGeneratedProposal && input.executeAgenticGeneratedSql)
|
|
3956
|
+
|| exploratoryExecutionSelected) && !result) {
|
|
3402
3957
|
// Fanout gate for native semantic direct-joins: a duplicate join-key row on
|
|
3403
3958
|
// the joined side multiplies fact rows BEFORE aggregation, so every summed
|
|
3404
3959
|
// value inflates silently (seen in the field as trillions-scale "governed"
|
|
3405
3960
|
// answers). Probe first; on structural contradiction, refuse with the cause
|
|
3406
3961
|
// instead of presenting wrong numbers — and never hand the error to SQL
|
|
3407
3962
|
// repair, which would regenerate the same multiplying join.
|
|
3408
|
-
let
|
|
3963
|
+
let semanticFanoutBlocked = false;
|
|
3409
3964
|
if (!executionError
|
|
3410
3965
|
&& !input.forcedGeneratedProposal
|
|
3411
|
-
&&
|
|
3966
|
+
&& !exploratoryExecutionSelected
|
|
3412
3967
|
&& semanticBridgeAnswer?.composeResult?.fanoutProbeSql
|
|
3413
3968
|
&& semanticBridgeAnswer.sql.trim() === parsed.sql?.trim()) {
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3969
|
+
// A DQL-artifact executor owns the semantic artifact, not an arbitrary
|
|
3970
|
+
// one-row structural SQL probe. If the host did not also provide the
|
|
3971
|
+
// read-only SQL executor needed for that probe, native direct joins fail
|
|
3972
|
+
// closed rather than treating an artifact handoff as proof of uniqueness.
|
|
3973
|
+
const fanoutProbe = input.executeGeneratedSql
|
|
3974
|
+
? await probeSemanticJoinFanout(semanticBridgeAnswer.composeResult.fanoutProbeSql, semanticBridgeAnswer.composeResult.tables, input.executeGeneratedSql)
|
|
3975
|
+
: {
|
|
3976
|
+
status: 'blocked',
|
|
3977
|
+
code: 'SEMANTIC_FANOUT_PROBE_ERROR',
|
|
3978
|
+
message: 'DQL did not execute this governed semantic answer because the host could not verify native join fanout before aggregation. Configure the semantic runtime or a read-only SQL probe executor, then retry.',
|
|
3979
|
+
};
|
|
3980
|
+
if (fanoutProbe.status === 'blocked') {
|
|
3981
|
+
// A governed semantic route freezes before execution. If DQL cannot
|
|
3982
|
+
// prove its native join is fanout-safe, it must fail closed rather than
|
|
3983
|
+
// execute, label the result governed, or quietly repair into a
|
|
3984
|
+
// different plan.
|
|
3985
|
+
executionError = fanoutProbe.message;
|
|
3986
|
+
semanticFanoutProbeFailure = fanoutProbe;
|
|
3987
|
+
semanticFanoutBlocked = true;
|
|
3418
3988
|
}
|
|
3419
3989
|
}
|
|
3420
3990
|
try {
|
|
@@ -3422,10 +3992,30 @@ async function runAnswerLoop(input) {
|
|
|
3422
3992
|
result = await executeCurrentSql();
|
|
3423
3993
|
}
|
|
3424
3994
|
catch (err) {
|
|
3995
|
+
const executionCode = err && typeof err === 'object'
|
|
3996
|
+
&& typeof err.code === 'string'
|
|
3997
|
+
? err.code
|
|
3998
|
+
: undefined;
|
|
3999
|
+
// A router-selected exploratory proposal that leaves its candidate
|
|
4000
|
+
// closure is not an executable draft. It is a pre-capability safety
|
|
4001
|
+
// denial: do not present its SQL/artifact as review-required work after
|
|
4002
|
+
// the host rejected it.
|
|
4003
|
+
exploratoryClosureDenied = exploratoryExecutionSelected
|
|
4004
|
+
&& executionCode === 'UNAUTHORIZED_SQL';
|
|
3425
4005
|
warehouseFailure = normalizeWarehouseSqlFailure(err, input.semanticDriver);
|
|
3426
4006
|
executionError = warehouseFailure.redactedMessage;
|
|
3427
4007
|
}
|
|
3428
|
-
if (executionError
|
|
4008
|
+
if (executionError
|
|
4009
|
+
// A host-supplied SQL proposal normally bypasses repair. A router-owned
|
|
4010
|
+
// exploratory proposal is the exception: its frozen plan may authorize
|
|
4011
|
+
// one corrected SQL statement after a retryable warehouse failure.
|
|
4012
|
+
&& (!input.forcedGeneratedProposal || exploratoryExecutionSelected)
|
|
4013
|
+
// A router-selected exploratory plan may receive one bounded
|
|
4014
|
+
// same-plan repair. The repair still goes through a fresh host
|
|
4015
|
+
// authorization; it does not reuse the consumed initial capability.
|
|
4016
|
+
&& (!exploratoryExecutionSelected || Boolean(initialExploratoryExecution))
|
|
4017
|
+
&& !semanticFanoutBlocked
|
|
4018
|
+
&& !authoritativeSemanticBinding) {
|
|
3429
4019
|
warehouseFailure ??= normalizeWarehouseSqlFailure(executionError, input.semanticDriver);
|
|
3430
4020
|
if (isRetryableGeneratedSqlError(warehouseFailure)) {
|
|
3431
4021
|
const localRepairSql = repairGeneratedSqlLocally(parsed.sql, executionError, schemaContext);
|
|
@@ -3450,7 +4040,13 @@ async function runAnswerLoop(input) {
|
|
|
3450
4040
|
],
|
|
3451
4041
|
};
|
|
3452
4042
|
try {
|
|
3453
|
-
result = await executeCurrentSql(
|
|
4043
|
+
result = await executeCurrentSql(exploratoryExecutionSelected
|
|
4044
|
+
? {
|
|
4045
|
+
version: 1,
|
|
4046
|
+
index: 1,
|
|
4047
|
+
parentSqlFingerprint: initialExploratoryExecution.freeze.sqlFingerprint,
|
|
4048
|
+
}
|
|
4049
|
+
: undefined);
|
|
3454
4050
|
executionError = undefined;
|
|
3455
4051
|
warehouseFailure = undefined;
|
|
3456
4052
|
}
|
|
@@ -3482,6 +4078,18 @@ async function runAnswerLoop(input) {
|
|
|
3482
4078
|
schemaContext,
|
|
3483
4079
|
signal: input.signal,
|
|
3484
4080
|
reasoningEffort: input.reasoningEffort,
|
|
4081
|
+
// Only the router-frozen exploratory lane has authority to spend
|
|
4082
|
+
// the exceptional post-warehouse repair transport. Legacy callers
|
|
4083
|
+
// keep their prior unlabelled repair behavior.
|
|
4084
|
+
...(exploratoryExecutionSelected
|
|
4085
|
+
&& Boolean(initialExploratoryExecution)
|
|
4086
|
+
&& frozenExploratoryModelRepairAuthority(input)
|
|
4087
|
+
? {
|
|
4088
|
+
dispatchPhase: 'repair',
|
|
4089
|
+
egressPurpose: 'repair_sql',
|
|
4090
|
+
maxProviderDispatches: 1,
|
|
4091
|
+
}
|
|
4092
|
+
: {}),
|
|
3485
4093
|
});
|
|
3486
4094
|
const repaired = parseProposal(repairedRaw);
|
|
3487
4095
|
if (repaired.sql) {
|
|
@@ -3500,7 +4108,12 @@ async function runAnswerLoop(input) {
|
|
|
3500
4108
|
repairNarrative = repaired.text?.trim() || undefined;
|
|
3501
4109
|
parsed.sql = repaired.sql;
|
|
3502
4110
|
parsed.viz = repaired.viz ?? parsed.viz;
|
|
3503
|
-
|
|
4111
|
+
// A repair fixes SQL bytes only. On the router-owned
|
|
4112
|
+
// exploratory lane, model prose or metadata must never mutate
|
|
4113
|
+
// the host-selected frame, route, outputs, or trust state after
|
|
4114
|
+
// the plan froze.
|
|
4115
|
+
if (!exploratoryExecutionSelected)
|
|
4116
|
+
applyParsedProposalMetadata(parsed, repaired);
|
|
3504
4117
|
contextValidation = {
|
|
3505
4118
|
ok: true,
|
|
3506
4119
|
warnings: [
|
|
@@ -3509,7 +4122,13 @@ async function runAnswerLoop(input) {
|
|
|
3509
4122
|
],
|
|
3510
4123
|
};
|
|
3511
4124
|
try {
|
|
3512
|
-
result = await executeCurrentSql(
|
|
4125
|
+
result = await executeCurrentSql(exploratoryExecutionSelected
|
|
4126
|
+
? {
|
|
4127
|
+
version: 1,
|
|
4128
|
+
index: 1,
|
|
4129
|
+
parentSqlFingerprint: initialExploratoryExecution.freeze.sqlFingerprint,
|
|
4130
|
+
}
|
|
4131
|
+
: undefined);
|
|
3513
4132
|
executionError = undefined;
|
|
3514
4133
|
warehouseFailure = undefined;
|
|
3515
4134
|
}
|
|
@@ -3536,10 +4155,17 @@ async function runAnswerLoop(input) {
|
|
|
3536
4155
|
: 'The question asks for a custom analysis, ranking, breakdown, comparison, or grain that should not be answered by a loose certified block match.'),
|
|
3537
4156
|
selectedNodes: contextNodes,
|
|
3538
4157
|
schemaContext,
|
|
3539
|
-
|
|
4158
|
+
// A router-selected physical-closure denial happens before a capability
|
|
4159
|
+
// exists. Its rejected bytes are diagnostics for the host, never a SQL
|
|
4160
|
+
// preview that a user can review, copy, or turn into a draft.
|
|
4161
|
+
sql: exploratoryClosureDenied || semanticFanoutProbeFailure ? undefined : parsed.sql,
|
|
3540
4162
|
suggestedViz: parsed.viz ?? 'table',
|
|
3541
4163
|
assumptions: [
|
|
3542
|
-
|
|
4164
|
+
...(exploratoryClosureDenied
|
|
4165
|
+
? ['The router-selected physical closure rejected the generated SQL before capability minting.']
|
|
4166
|
+
: semanticFanoutProbeFailure
|
|
4167
|
+
? ['The native semantic join could not be proven fanout-safe before plan freeze, so DQL did not execute or expose its SQL.']
|
|
4168
|
+
: ['The SQL preview is uncertified until an analyst reviews and promotes the DQL artifact.']),
|
|
3543
4169
|
...(certifiedExecutionFallback
|
|
3544
4170
|
? [`Certified block ${certifiedExecutionFallback.node.name} failed execution and was bypassed: ${certifiedExecutionFallback.error}`]
|
|
3545
4171
|
: []),
|
|
@@ -3563,6 +4189,23 @@ async function runAnswerLoop(input) {
|
|
|
3563
4189
|
}
|
|
3564
4190
|
}
|
|
3565
4191
|
const resultShape = result ? validateAnswerResultShape(questionPlan, result) : undefined;
|
|
4192
|
+
// A router-selected exploratory plan carries a host-bound output
|
|
4193
|
+
// projection. The ordinary generated-answer UX may show a partial result
|
|
4194
|
+
// with a warning, but a frozen exploratory plan may not: displaying it
|
|
4195
|
+
// would silently change the tuple DQL authorized. Keep the connector rows
|
|
4196
|
+
// out of the answer/artifact when a broken executor returns them anyway.
|
|
4197
|
+
const missingFrozenExploratoryOutputs = exploratoryExecutionSelected && result
|
|
4198
|
+
? missingFrozenRequiredOutputProjection(input.resolvedAnalyticalPlan, result)
|
|
4199
|
+
: [];
|
|
4200
|
+
const activeExploratoryOutputFreeze = exploratoryRepairExecution?.freeze ?? initialExploratoryExecution?.freeze;
|
|
4201
|
+
const missingFrozenExploratoryBindingProofs = exploratoryExecutionSelected && result
|
|
4202
|
+
? missingFrozenRequiredOutputBindingProof(input.resolvedAnalyticalPlan, activeExploratoryOutputFreeze)
|
|
4203
|
+
: [];
|
|
4204
|
+
const frozenExploratoryOutputFailures = [...new Set([
|
|
4205
|
+
...missingFrozenExploratoryOutputs,
|
|
4206
|
+
...missingFrozenExploratoryBindingProofs,
|
|
4207
|
+
])];
|
|
4208
|
+
const frozenExploratoryOutputContractFailure = frozenExploratoryOutputFailures.length > 0;
|
|
3566
4209
|
// ANY question whose SQL executed but dropped multiple requested columns used to
|
|
3567
4210
|
// REFUSE outright ("no governed answer"), throwing away a result that actually
|
|
3568
4211
|
// ran. Instead, SURFACE the partial result (review-required) with a warning that
|
|
@@ -3585,27 +4228,32 @@ async function runAnswerLoop(input) {
|
|
|
3585
4228
|
...repairNotes,
|
|
3586
4229
|
...(resultShape?.warnings ?? []),
|
|
3587
4230
|
...(partialShapeWarning ? [partialShapeWarning] : []),
|
|
4231
|
+
...(frozenExploratoryOutputContractFailure
|
|
4232
|
+
? [`The executed exploratory result violated the frozen output contract: ${frozenExploratoryOutputFailures.join(', ')}.`]
|
|
4233
|
+
: []),
|
|
3588
4234
|
...(topNTrimNote ? [topNTrimNote] : []),
|
|
3589
4235
|
...(executionError ? ['The preview execution error must be reviewed before reuse.'] : []),
|
|
3590
4236
|
];
|
|
3591
4237
|
const generatedOutputs = parsed.outputs?.length ? parsed.outputs : resultColumnNames(result);
|
|
3592
4238
|
const generatedRequestedFilters = mergeProposalStringLists(input.followUp?.filters, parsed.requestedFilters);
|
|
3593
4239
|
const generatedRequestedDimensions = mergeProposalStringLists(input.followUp?.dimensions, parsed.requestedDimensions);
|
|
3594
|
-
const baseDqlArtifact =
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
4240
|
+
const baseDqlArtifact = exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure
|
|
4241
|
+
? undefined
|
|
4242
|
+
: result?.dqlArtifact ?? semanticBridgeAnswer?.dqlArtifact ?? buildGeneratedSqlDqlArtifact({
|
|
4243
|
+
question,
|
|
4244
|
+
sql: parsed.sql,
|
|
4245
|
+
intent,
|
|
4246
|
+
domain,
|
|
4247
|
+
followUp: input.followUp,
|
|
4248
|
+
contextPack: input.contextPack,
|
|
4249
|
+
sourceBlock: followUpSourceBlock ?? undefined,
|
|
4250
|
+
sourceDqlArtifact: input.followUp?.priorDqlArtifact,
|
|
4251
|
+
proposedEntity: parsed.proposedEntity,
|
|
4252
|
+
requestedFilters: generatedRequestedFilters,
|
|
4253
|
+
requestedDimensions: generatedRequestedDimensions,
|
|
4254
|
+
validationWarnings,
|
|
4255
|
+
outputs: generatedOutputs,
|
|
4256
|
+
});
|
|
3609
4257
|
const requestedTopN = questionPlan.requestedShape.topN?.scope === 'per_group'
|
|
3610
4258
|
? undefined
|
|
3611
4259
|
: questionPlan.requestedShape.topN?.n;
|
|
@@ -3618,7 +4266,7 @@ async function runAnswerLoop(input) {
|
|
|
3618
4266
|
: undefined;
|
|
3619
4267
|
let draftBlock;
|
|
3620
4268
|
let draftCaptureError;
|
|
3621
|
-
if (input.captureGeneratedDraft && parsed.sql) {
|
|
4269
|
+
if (!exploratoryClosureDenied && !semanticFanoutProbeFailure && !frozenExploratoryOutputContractFailure && input.captureGeneratedDraft && parsed.sql) {
|
|
3622
4270
|
try {
|
|
3623
4271
|
draftBlock = await input.captureGeneratedDraft({
|
|
3624
4272
|
question,
|
|
@@ -3665,32 +4313,53 @@ async function runAnswerLoop(input) {
|
|
|
3665
4313
|
: undefined;
|
|
3666
4314
|
const certifiedMetricAnswer = semanticMetricCertification === 'certified' || semanticMetricCertification === 'reviewed';
|
|
3667
4315
|
const governedMetricExecutionFailure = governedMetricAnswer && Boolean(executionError);
|
|
4316
|
+
const terminalExecutionFailure = governedMetricExecutionFailure || exploratoryClosureDenied || frozenExploratoryOutputContractFailure;
|
|
4317
|
+
const semanticFanoutTraceError = semanticFanoutProbeFailure
|
|
4318
|
+
? Object.assign(new Error(semanticFanoutProbeFailure.message), {
|
|
4319
|
+
code: semanticFanoutProbeFailure.code,
|
|
4320
|
+
details: { phase: 'validation' },
|
|
4321
|
+
})
|
|
4322
|
+
: executionError;
|
|
4323
|
+
const terminalFailureMessage = frozenExploratoryOutputContractFailure
|
|
4324
|
+
? `The exploratory execution result did not retain proven frozen output binding ${frozenExploratoryOutputFailures.join(', ')}, so DQL did not display it.`
|
|
4325
|
+
: exploratoryClosureDenied
|
|
4326
|
+
? 'The generated SQL referenced a relation outside the router-selected physical closure, so DQL did not execute it.'
|
|
4327
|
+
: semanticFanoutProbeFailure?.message;
|
|
3668
4328
|
const finalSemanticExecutionTrace = semanticTraceAfterExecution(semanticExecutionTrace, {
|
|
3669
4329
|
executed: Boolean(result),
|
|
3670
|
-
...(
|
|
4330
|
+
...(semanticFanoutTraceError ? { error: semanticFanoutTraceError } : {}),
|
|
3671
4331
|
...(result ? { result } : {}),
|
|
3672
4332
|
});
|
|
3673
4333
|
return {
|
|
3674
|
-
kind:
|
|
3675
|
-
sourceTier:
|
|
3676
|
-
certification:
|
|
3677
|
-
reviewStatus:
|
|
4334
|
+
kind: terminalExecutionFailure ? 'no_answer' : 'uncertified',
|
|
4335
|
+
sourceTier: terminalExecutionFailure ? 'no_answer' : governedMetricAnswer ? 'semantic_layer' : activeTier,
|
|
4336
|
+
certification: terminalExecutionFailure ? 'analyst_review_required' : governedMetricAnswer ? 'governed' : 'ai_generated',
|
|
4337
|
+
reviewStatus: terminalExecutionFailure ? 'none' : governedMetricAnswer ? 'governed' : 'draft_ready',
|
|
3678
4338
|
semanticMetricCertification,
|
|
3679
|
-
confidence:
|
|
3680
|
-
text: generatedText,
|
|
3681
|
-
answer: generatedText,
|
|
3682
|
-
proposedSql: parsed.sql,
|
|
3683
|
-
|
|
3684
|
-
result,
|
|
4339
|
+
confidence: terminalExecutionFailure ? 0 : certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
|
|
4340
|
+
text: terminalFailureMessage ?? generatedText,
|
|
4341
|
+
answer: terminalFailureMessage ?? generatedText,
|
|
4342
|
+
...(exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure ? {} : { proposedSql: parsed.sql, sql: parsed.sql }),
|
|
4343
|
+
result: frozenExploratoryOutputContractFailure ? undefined : result,
|
|
3685
4344
|
executionError,
|
|
3686
4345
|
...(warehouseFailure ? { warehouseFailure } : {}),
|
|
3687
4346
|
...(finalSemanticExecutionTrace ? { semanticExecutionTrace: finalSemanticExecutionTrace } : {}),
|
|
3688
|
-
...(
|
|
4347
|
+
...(terminalExecutionFailure ? {
|
|
4348
|
+
refusalCode: semanticFanoutProbeFailure ? 'policy_blocked' : 'grounding_gap',
|
|
4349
|
+
} : {}),
|
|
4350
|
+
...(semanticFanoutProbeFailure ? {
|
|
4351
|
+
refusalDetails: {
|
|
4352
|
+
code: semanticFanoutProbeFailure.code,
|
|
4353
|
+
message: semanticFanoutProbeFailure.message,
|
|
4354
|
+
},
|
|
4355
|
+
} : {}),
|
|
3689
4356
|
suggestedViz: parsed.viz ?? 'table',
|
|
3690
|
-
dqlArtifact: answerDqlArtifact,
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
4357
|
+
...(exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure ? {} : { dqlArtifact: answerDqlArtifact }),
|
|
4358
|
+
...(exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure ? {} : {
|
|
4359
|
+
draftBlock,
|
|
4360
|
+
draftBlockId: draftBlock?.path,
|
|
4361
|
+
promoteCommand: draftBlock ? `dql certify --from-draft ${draftBlock.path}` : undefined,
|
|
4362
|
+
}),
|
|
3694
4363
|
trustLabel: input.contextPack?.trustLabel,
|
|
3695
4364
|
sourceCertifiedBlock,
|
|
3696
4365
|
contextPackId: input.contextPack?.id,
|
|
@@ -3728,6 +4397,8 @@ async function runAnswerLoop(input) {
|
|
|
3728
4397
|
considered,
|
|
3729
4398
|
providerUsed: provider.name,
|
|
3730
4399
|
aggregationSafetyProof,
|
|
4400
|
+
...(initialExploratoryExecution ? { exploratoryExecutionFreeze: initialExploratoryExecution.freeze } : {}),
|
|
4401
|
+
...(exploratoryRepairExecution ? { exploratoryRepairExecutionFreeze: exploratoryRepairExecution.freeze } : {}),
|
|
3731
4402
|
// Carry the governed metric match so the exit point can name a
|
|
3732
4403
|
// `semantic_metric` route (spec 17, part C).
|
|
3733
4404
|
_semanticMetricMatch: governedMetricAnswer ? semanticMetricMatch ?? undefined : undefined,
|
|
@@ -3767,6 +4438,70 @@ function resultColumnNames(result) {
|
|
|
3767
4438
|
.filter(Boolean);
|
|
3768
4439
|
return columns?.length ? columns : undefined;
|
|
3769
4440
|
}
|
|
4441
|
+
/**
|
|
4442
|
+
* Check only the host-bound aliases from a frozen plan. This is intentionally
|
|
4443
|
+
* stricter than the conversational shape helper: synonym matching is useful
|
|
4444
|
+
* for ordinary narration, but it cannot substitute a requested order/product
|
|
4445
|
+
* identifier after exploratory SQL has been authorized.
|
|
4446
|
+
*/
|
|
4447
|
+
function missingFrozenRequiredOutputProjection(plan, result) {
|
|
4448
|
+
const required = plan?.outputContract?.requiredOutputs ?? [];
|
|
4449
|
+
if (required.length === 0)
|
|
4450
|
+
return [];
|
|
4451
|
+
const columns = new Set((resultColumnNames(result) ?? []).map(canonicalFrozenOutputAlias));
|
|
4452
|
+
return required.flatMap((binding) => {
|
|
4453
|
+
const alias = binding.outputName
|
|
4454
|
+
?? binding.qualifiedId?.split(/[:.]/).at(-1)
|
|
4455
|
+
?? binding.requested;
|
|
4456
|
+
const normalized = canonicalFrozenOutputAlias(alias);
|
|
4457
|
+
return normalized && columns.has(normalized) ? [] : [alias];
|
|
4458
|
+
});
|
|
4459
|
+
}
|
|
4460
|
+
/**
|
|
4461
|
+
* A column name in a returned result does not prove where it came from. The
|
|
4462
|
+
* result is accepted only when the server-owned authorization receipt carries
|
|
4463
|
+
* the exact frozen output-source binding that was parser-validated for the SQL
|
|
4464
|
+
* handed to the connector. Old persisted receipts remain readable; they
|
|
4465
|
+
* simply cannot authorize a new result for a plan that has explicit outputs.
|
|
4466
|
+
*/
|
|
4467
|
+
function missingFrozenRequiredOutputBindingProof(plan, freeze) {
|
|
4468
|
+
const required = plan?.outputContract?.requiredOutputs ?? [];
|
|
4469
|
+
if (required.length === 0)
|
|
4470
|
+
return [];
|
|
4471
|
+
const expected = frozenRequiredOutputBindingProofsForPlan(plan);
|
|
4472
|
+
const missingSourceBindingAliases = required
|
|
4473
|
+
.filter((binding) => !expected.some((proof) => canonicalFrozenOutputAlias(proof.outputName)
|
|
4474
|
+
=== canonicalFrozenOutputAlias(binding.outputName
|
|
4475
|
+
?? binding.qualifiedId?.split(/[:.]/).at(-1)
|
|
4476
|
+
?? binding.requested)))
|
|
4477
|
+
.map((binding) => binding.outputName
|
|
4478
|
+
?? binding.qualifiedId?.split(/[:.]/).at(-1)
|
|
4479
|
+
?? binding.requested);
|
|
4480
|
+
const actual = freeze?.requiredOutputBindings ?? [];
|
|
4481
|
+
const missingProofAliases = expected
|
|
4482
|
+
.filter((proof) => !actual.some((candidate) => sameFrozenRequiredOutputBindingProof(candidate, proof)))
|
|
4483
|
+
.map((proof) => proof.outputName);
|
|
4484
|
+
return [...new Set([...missingSourceBindingAliases, ...missingProofAliases])];
|
|
4485
|
+
}
|
|
4486
|
+
function sameFrozenRequiredOutputBindingProof(actual, expected) {
|
|
4487
|
+
return actual.version === 1
|
|
4488
|
+
&& actual.qualifiedId === expected.qualifiedId
|
|
4489
|
+
&& canonicalFrozenOutputAlias(actual.outputName) === canonicalFrozenOutputAlias(expected.outputName)
|
|
4490
|
+
&& canonicalFrozenSourceIdentifier(actual.relation) === canonicalFrozenSourceIdentifier(expected.relation)
|
|
4491
|
+
&& canonicalFrozenSourceIdentifier(actual.column) === canonicalFrozenSourceIdentifier(expected.column);
|
|
4492
|
+
}
|
|
4493
|
+
function canonicalFrozenOutputAlias(value) {
|
|
4494
|
+
return value.toLowerCase()
|
|
4495
|
+
.replace(/["`]/g, '')
|
|
4496
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
4497
|
+
.replace(/^_+|_+$/g, '');
|
|
4498
|
+
}
|
|
4499
|
+
function canonicalFrozenSourceIdentifier(value) {
|
|
4500
|
+
return value.toLowerCase()
|
|
4501
|
+
.replace(/["`\[\]]/g, '')
|
|
4502
|
+
.replace(/\s*\.\s*/g, '.')
|
|
4503
|
+
.trim();
|
|
4504
|
+
}
|
|
3770
4505
|
function analyticalCapabilityForPlan(plan, contextPack, registry = []) {
|
|
3771
4506
|
const selected = normalizeMetricCapabilityContract(plan.selectedCapability);
|
|
3772
4507
|
if (selected) {
|
|
@@ -4133,8 +4868,11 @@ async function executeSemanticAnalyticalGraph(input) {
|
|
|
4133
4868
|
});
|
|
4134
4869
|
aggregationSafetyProof = proof;
|
|
4135
4870
|
if (proof.status !== 'safe') {
|
|
4136
|
-
return analyticalGraphFailureAnswer(input, '
|
|
4137
|
-
|
|
4871
|
+
return analyticalGraphFailureAnswer(input, 'COMPILATION_FAILED', semanticAggregationSafetyFailure(proof), {
|
|
4872
|
+
// This is compiler/plan validation before a warehouse statement is
|
|
4873
|
+
// dispatched. It must be represented as semantic.compile, never as
|
|
4874
|
+
// result normalization or a SQL execution failure.
|
|
4875
|
+
phase: 'compilation',
|
|
4138
4876
|
dqlArtifact: composed.dqlArtifact,
|
|
4139
4877
|
compiledSql: composed.sql,
|
|
4140
4878
|
semanticExecutionTrace,
|
|
@@ -4160,11 +4898,25 @@ async function executeSemanticAnalyticalGraph(input) {
|
|
|
4160
4898
|
}
|
|
4161
4899
|
catch (error) {
|
|
4162
4900
|
const message = error instanceof Error ? error.message : String(error);
|
|
4901
|
+
// A missing local execution target fails at the host's pre-execution
|
|
4902
|
+
// boundary. It must not be recast as a semantic compiler failure merely
|
|
4903
|
+
// because the semantic graph owns the callback. Preserve the narrow,
|
|
4904
|
+
// producer-tagged fact for the durable Ask receipt; it does not alter
|
|
4905
|
+
// the frozen route, trust, or user-facing analytical failure.
|
|
4906
|
+
const observabilityExecutionFailure = observabilityFailureForFrozenExecution(analyticalErrorDetail(error));
|
|
4163
4907
|
return analyticalGraphFailureAnswer(input, 'EXECUTION_FAILED', message, {
|
|
4164
4908
|
phase: 'execution',
|
|
4165
4909
|
dqlArtifact: composed.dqlArtifact,
|
|
4166
4910
|
compiledSql: composed.sql,
|
|
4167
4911
|
semanticExecutionTrace: semanticTraceAfterExecution(semanticExecutionTrace, { executed: false, error }),
|
|
4912
|
+
// The exact aggregation proof already passed before the connector
|
|
4913
|
+
// call. Preserve it so presentation never recasts a warehouse binder
|
|
4914
|
+
// error as a failed semantic aggregation proof.
|
|
4915
|
+
aggregationSafetyProof,
|
|
4916
|
+
// This producer-tagged fact is diagnostic only: it does not alter
|
|
4917
|
+
// routing, trust, or the immutable frozen plan.
|
|
4918
|
+
warehouseFailure: normalizeWarehouseSqlFailure(error, input.input.semanticDriver),
|
|
4919
|
+
...(observabilityExecutionFailure ? { observabilityExecutionFailure } : {}),
|
|
4168
4920
|
});
|
|
4169
4921
|
}
|
|
4170
4922
|
const normalized = normalizeAnalyticalSourceResult({
|
|
@@ -4594,6 +5346,10 @@ function analyticalGraphFailureAnswer(input, code, reason, artifacts = {}) {
|
|
|
4594
5346
|
...(artifacts.dqlArtifact ? { dqlArtifact: artifacts.dqlArtifact } : {}),
|
|
4595
5347
|
...(artifacts.semanticExecutionTrace ? { semanticExecutionTrace: artifacts.semanticExecutionTrace } : {}),
|
|
4596
5348
|
...(artifacts.aggregationSafetyProof ? { aggregationSafetyProof: artifacts.aggregationSafetyProof } : {}),
|
|
5349
|
+
...(artifacts.warehouseFailure ? { warehouseFailure: artifacts.warehouseFailure } : {}),
|
|
5350
|
+
...(artifacts.observabilityExecutionFailure
|
|
5351
|
+
? { observabilityExecutionFailure: artifacts.observabilityExecutionFailure }
|
|
5352
|
+
: {}),
|
|
4597
5353
|
citations: contextPackCitations(input.input.contextPack, 8),
|
|
4598
5354
|
considered: input.considered,
|
|
4599
5355
|
contextPack: input.input.contextPack,
|
|
@@ -4765,6 +5521,38 @@ Rules:
|
|
|
4765
5521
|
// combined dataset — show them separately" refusal into the join the user asked
|
|
4766
5522
|
// for, while still allowing an honest refusal if context is truly missing.
|
|
4767
5523
|
const FORCE_JOIN_INSTRUCTION = `Your previous attempt declined to produce SQL. Re-read the supplied schema, metadata, and any "Knowledge graph join routes": this question CAN be answered by joining the grounded tables along their documented keys. Do NOT refuse, and do NOT suggest showing the datasets separately — compose ONE read-only SELECT/WITH that joins the relevant tables to answer it directly, following the JSON contract from rule 3. State the grain and the exact join path in the summary. Only if a required table, column, or join key is truly absent from the supplied context may you still ask a clarifying question.`;
|
|
5524
|
+
/**
|
|
5525
|
+
* A model-decline correction is only a repair when the router already froze a
|
|
5526
|
+
* bounded exploratory RAP and the host supplied the matching capability
|
|
5527
|
+
* mint/consume closures. Anything weaker is a new planning attempt and must
|
|
5528
|
+
* not borrow the repair transport allowance.
|
|
5529
|
+
*/
|
|
5530
|
+
function frozenExploratoryModelRepairAuthority(input) {
|
|
5531
|
+
const plan = input.resolvedAnalyticalPlan;
|
|
5532
|
+
return input.selectedCascadeTier === 'exploratory_sql'
|
|
5533
|
+
&& plan?.mode === 'authoritative'
|
|
5534
|
+
&& plan.capability === 'bounded_exploration'
|
|
5535
|
+
&& Boolean(plan.planId && plan.fingerprint && plan.snapshotId)
|
|
5536
|
+
&& Boolean(input.prepareExploratorySqlExecution && input.executeAgenticGeneratedSql);
|
|
5537
|
+
}
|
|
5538
|
+
/**
|
|
5539
|
+
* The corrective prompt repeats only host-owned tuple facts. It never asks
|
|
5540
|
+
* the model to choose a new route, relation closure, output contract, or
|
|
5541
|
+
* trust label; SQL authorization re-proves those facts after the response.
|
|
5542
|
+
*/
|
|
5543
|
+
function renderFrozenExploratoryRepairContract(plan, questionPlan) {
|
|
5544
|
+
const requiredOutputs = plan.outputContract?.requiredOutputs?.filter((output) => output.status === 'resolved' && output.outputName && output.qualifiedId)
|
|
5545
|
+
.map((output) => `${output.outputName} <- ${output.qualifiedId}`)
|
|
5546
|
+
?? [];
|
|
5547
|
+
return [
|
|
5548
|
+
'This is the one permitted correction for an already frozen, review-required exploratory plan.',
|
|
5549
|
+
renderRequestedShapeForRepair(questionPlan),
|
|
5550
|
+
requiredOutputs.length > 0
|
|
5551
|
+
? `Frozen required output bindings (preserve exactly): ${requiredOutputs.join('; ')}`
|
|
5552
|
+
: 'Preserve the frozen requested tuple exactly.',
|
|
5553
|
+
'Use only the relations and columns already supplied in the bounded context. Do not add a relation, change the ranking, remove an output, change the limit, or alter the analytical meaning. The host will validate the same frozen snapshot and closure before execution.',
|
|
5554
|
+
].join('\n');
|
|
5555
|
+
}
|
|
4768
5556
|
/**
|
|
4769
5557
|
* Produces the prompt-facing subset of a broad local context pack. Global
|
|
4770
5558
|
* records remain available, while a record explicitly owned by another domain
|
|
@@ -4816,6 +5604,147 @@ function scopeContextPackToQuestionDomains(contextPack, domains, manifest) {
|
|
|
4816
5604
|
},
|
|
4817
5605
|
};
|
|
4818
5606
|
}
|
|
5607
|
+
/**
|
|
5608
|
+
* Make the router-selected exploratory physical closure the only SQL authority
|
|
5609
|
+
* visible to generation and execution. The broad context pack remains on the
|
|
5610
|
+
* run as diagnostics, but must not make a same-snapshot neighbouring relation
|
|
5611
|
+
* executable merely because it ranked well for the surrounding question.
|
|
5612
|
+
*
|
|
5613
|
+
* Matching is canonical-ID only. In particular this deliberately does not
|
|
5614
|
+
* fall back to relation/model leaf names: duplicate `orders` relations across
|
|
5615
|
+
* schemas/domains must not inherit each other's proof.
|
|
5616
|
+
*/
|
|
5617
|
+
export function scopeContextPackToExploratoryCandidateClosure(contextPack, candidateIds) {
|
|
5618
|
+
if (!contextPack || !candidateIds?.length)
|
|
5619
|
+
return undefined;
|
|
5620
|
+
const requestedIds = new Set(candidateIds.map(normalizeExploratoryCandidateIdentity).filter(Boolean));
|
|
5621
|
+
if (requestedIds.size === 0)
|
|
5622
|
+
return undefined;
|
|
5623
|
+
const intersects = (values) => {
|
|
5624
|
+
for (const value of values) {
|
|
5625
|
+
const normalized = normalizeExploratoryCandidateIdentity(value ?? '');
|
|
5626
|
+
if (normalized && requestedIds.has(normalized))
|
|
5627
|
+
return true;
|
|
5628
|
+
}
|
|
5629
|
+
return false;
|
|
5630
|
+
};
|
|
5631
|
+
const objectIdentities = (object) => {
|
|
5632
|
+
const payload = object.payload ?? {};
|
|
5633
|
+
return [
|
|
5634
|
+
object.objectKey,
|
|
5635
|
+
object.fullName,
|
|
5636
|
+
payload.qualifiedId,
|
|
5637
|
+
payload.uniqueId,
|
|
5638
|
+
payload.id,
|
|
5639
|
+
payload.localId,
|
|
5640
|
+
payload.relation,
|
|
5641
|
+
payload.table,
|
|
5642
|
+
payload.model,
|
|
5643
|
+
...metadataStringValues(payload.sourceObjects),
|
|
5644
|
+
...metadataStringValues(payload.sourceRelations),
|
|
5645
|
+
...metadataStringValues(payload.tableDependencies),
|
|
5646
|
+
].filter((value) => typeof value === 'string');
|
|
5647
|
+
};
|
|
5648
|
+
const objectRelationReferences = (object) => {
|
|
5649
|
+
const payload = object.payload ?? {};
|
|
5650
|
+
return [
|
|
5651
|
+
payload.relation,
|
|
5652
|
+
payload.table,
|
|
5653
|
+
payload.model,
|
|
5654
|
+
...metadataStringValues(payload.sourceObjects),
|
|
5655
|
+
...metadataStringValues(payload.sourceRelations),
|
|
5656
|
+
...metadataStringValues(payload.tableDependencies),
|
|
5657
|
+
].filter((value) => typeof value === 'string');
|
|
5658
|
+
};
|
|
5659
|
+
const directObjects = contextPack.objects.filter((object) => intersects(objectIdentities(object)));
|
|
5660
|
+
const requestedRelations = new Set();
|
|
5661
|
+
for (const object of directObjects) {
|
|
5662
|
+
for (const relation of objectRelationReferences(object)) {
|
|
5663
|
+
const normalized = normalizeExploratoryCandidateIdentity(relation);
|
|
5664
|
+
if (normalized)
|
|
5665
|
+
requestedRelations.add(normalized);
|
|
5666
|
+
}
|
|
5667
|
+
}
|
|
5668
|
+
for (const relation of contextPack.allowedSqlContext.relations) {
|
|
5669
|
+
if (intersects([relation.objectKey, relation.relation])) {
|
|
5670
|
+
requestedRelations.add(normalizeExploratoryCandidateIdentity(relation.relation));
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
const relations = contextPack.allowedSqlContext.relations.filter((relation) => {
|
|
5674
|
+
const normalized = normalizeExploratoryCandidateIdentity(relation.relation);
|
|
5675
|
+
return requestedIds.has(normalized)
|
|
5676
|
+
|| requestedRelations.has(normalized)
|
|
5677
|
+
|| Boolean(relation.objectKey && directObjects.some((object) => object.objectKey === relation.objectKey));
|
|
5678
|
+
});
|
|
5679
|
+
// A router-selected exploratory attempt without a physical relation is not
|
|
5680
|
+
// executable. Returning undefined is intentionally fail-closed; callers may
|
|
5681
|
+
// preserve the full pack for receipts but may not use it as SQL authority.
|
|
5682
|
+
if (relations.length === 0)
|
|
5683
|
+
return undefined;
|
|
5684
|
+
const relationIds = new Set(relations.map((relation) => normalizeExploratoryCandidateIdentity(relation.relation)));
|
|
5685
|
+
const relationshipEndpointIds = new Set();
|
|
5686
|
+
for (const object of directObjects.filter((object) => object.objectType === 'relationship')) {
|
|
5687
|
+
const payload = object.payload ?? {};
|
|
5688
|
+
for (const endpoint of [payload.from, payload.to]) {
|
|
5689
|
+
if (typeof endpoint === 'string') {
|
|
5690
|
+
const normalized = normalizeExploratoryCandidateIdentity(endpoint);
|
|
5691
|
+
if (normalized)
|
|
5692
|
+
relationshipEndpointIds.add(normalized);
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
const objects = contextPack.objects.filter((object) => {
|
|
5697
|
+
if (directObjects.includes(object))
|
|
5698
|
+
return true;
|
|
5699
|
+
const identities = objectIdentities(object).map(normalizeExploratoryCandidateIdentity);
|
|
5700
|
+
if (identities.some((identity) => relationIds.has(identity) || relationshipEndpointIds.has(identity)))
|
|
5701
|
+
return true;
|
|
5702
|
+
return objectRelationReferences(object)
|
|
5703
|
+
.map(normalizeExploratoryCandidateIdentity)
|
|
5704
|
+
.some((identity) => relationIds.has(identity));
|
|
5705
|
+
});
|
|
5706
|
+
const objectKeys = new Set(objects.map((object) => object.objectKey));
|
|
5707
|
+
const relationKeys = new Set(relations.map((relation) => normalizeRelationKey(relation.relation)));
|
|
5708
|
+
const selectedRelations = contextPack.retrievalDiagnostics.selectedRelations?.filter((relation) => relationKeys.has(normalizeRelationKey(relation.relation)));
|
|
5709
|
+
const selectedJoinPaths = contextPack.retrievalDiagnostics.selectedJoinPaths?.filter((path) => relationKeys.has(normalizeRelationKey(path.leftRelation))
|
|
5710
|
+
&& relationKeys.has(normalizeRelationKey(path.rightRelation)));
|
|
5711
|
+
return {
|
|
5712
|
+
...contextPack,
|
|
5713
|
+
focusObjectKey: contextPack.focusObjectKey && objectKeys.has(contextPack.focusObjectKey)
|
|
5714
|
+
? contextPack.focusObjectKey
|
|
5715
|
+
: objects[0]?.objectKey ?? null,
|
|
5716
|
+
objects,
|
|
5717
|
+
skills: [],
|
|
5718
|
+
edges: contextPack.edges.filter((edge) => objectKeys.has(edge.fromKey) && objectKeys.has(edge.toKey)),
|
|
5719
|
+
citations: contextPack.citations.filter((citation) => objectKeys.has(citation.objectKey)),
|
|
5720
|
+
evidenceSummaries: contextPack.evidenceSummaries.filter((summary) => !summary.objectKey || objectKeys.has(summary.objectKey)),
|
|
5721
|
+
evidenceRoles: contextPack.evidenceRoles.filter((role) => objectKeys.has(role.objectKey)),
|
|
5722
|
+
allowedSqlContext: {
|
|
5723
|
+
relations,
|
|
5724
|
+
// Authored block SQL is not a physical exploratory authority. The
|
|
5725
|
+
// selected dbt/runtime relation closure above is the whole prompt/SQL
|
|
5726
|
+
// boundary for this tier.
|
|
5727
|
+
sourceBlockSql: [],
|
|
5728
|
+
},
|
|
5729
|
+
retrievalDiagnostics: {
|
|
5730
|
+
...contextPack.retrievalDiagnostics,
|
|
5731
|
+
selectedObjects: objects.length,
|
|
5732
|
+
selectedEvidence: contextPack.retrievalDiagnostics.selectedEvidence.filter((evidence) => objectKeys.has(evidence.objectKey)),
|
|
5733
|
+
selectedRelations,
|
|
5734
|
+
selectedJoinPaths,
|
|
5735
|
+
schemaShapeCandidates: contextPack.retrievalDiagnostics.schemaShapeCandidates?.filter((candidate) => objectKeys.has(candidate.objectKey)),
|
|
5736
|
+
certifiedCandidateFits: contextPack.retrievalDiagnostics.certifiedCandidateFits.filter((candidate) => objectKeys.has(candidate.objectKey)),
|
|
5737
|
+
},
|
|
5738
|
+
};
|
|
5739
|
+
}
|
|
5740
|
+
function metadataStringValues(value) {
|
|
5741
|
+
return Array.isArray(value)
|
|
5742
|
+
? value.filter((item) => typeof item === 'string')
|
|
5743
|
+
: [];
|
|
5744
|
+
}
|
|
5745
|
+
function normalizeExploratoryCandidateIdentity(value) {
|
|
5746
|
+
return value.trim().replace(/["`\[\]]/g, '').replace(/\s*\.\s*/g, '.').toLowerCase();
|
|
5747
|
+
}
|
|
4819
5748
|
/**
|
|
4820
5749
|
* Build the prompt-facing evidence pack after enforcing the certified output
|
|
4821
5750
|
* contract. Incompatible blocks remain in the returned answer's `considered`
|
|
@@ -4831,7 +5760,13 @@ function contextPackForRequestedShape(contextPack, question, questionPlan, kg) {
|
|
|
4831
5760
|
const blockName = object.name || object.objectKey.replace(/^dql:block:/, '');
|
|
4832
5761
|
const node = kg.getNode(`block:${blockName}`);
|
|
4833
5762
|
if (node?.kind === 'block') {
|
|
4834
|
-
const fit = evaluateCertifiedBlockFit({
|
|
5763
|
+
const fit = evaluateCertifiedBlockFit({
|
|
5764
|
+
question,
|
|
5765
|
+
plan: questionPlan,
|
|
5766
|
+
block: node,
|
|
5767
|
+
exactExampleMatch: hasUniqueExactCatalogExample(question, contextPack, node),
|
|
5768
|
+
uniqueExactExampleContract: hasUniqueExactCatalogExample(question, contextPack, node),
|
|
5769
|
+
});
|
|
4835
5770
|
if (fit.kind === 'context_only' || fit.kind === 'not_applicable') {
|
|
4836
5771
|
incompatibleBlockKeys.add(object.objectKey);
|
|
4837
5772
|
}
|
|
@@ -5384,7 +6319,6 @@ function renderContextPackForPrompt(contextPack, budget) {
|
|
|
5384
6319
|
: '';
|
|
5385
6320
|
const sourceSql = renderSourceBlockSqlContext(contextPack, budget);
|
|
5386
6321
|
return [
|
|
5387
|
-
`context_pack_id: ${contextPack.id}`,
|
|
5388
6322
|
`trust_label: ${contextPack.trustLabel}`,
|
|
5389
6323
|
contextPack.trustLabelInfo ? `trust_label_canonical: ${contextPack.trustLabelInfo.display}` : '',
|
|
5390
6324
|
renderDomainBriefingForPrompt(contextPack).trim(),
|
|
@@ -5678,6 +6612,72 @@ function certifiedHitFromContextPack(contextPack, kg) {
|
|
|
5678
6612
|
const node = nodeId ? kg.getNode(nodeId) : null;
|
|
5679
6613
|
return node ? { node, score: 1, snippet: object.snippet } : null;
|
|
5680
6614
|
}
|
|
6615
|
+
/**
|
|
6616
|
+
* Preserve the catalog's unique exact-example proof when the answer boundary
|
|
6617
|
+
* rechecks the frozen block. The context pack is the same retrieval snapshot
|
|
6618
|
+
* that produced `exactObjectKey`; broad KG search is intentionally not used to
|
|
6619
|
+
* manufacture example uniqueness.
|
|
6620
|
+
*/
|
|
6621
|
+
function hasUniqueExactCatalogExample(question, contextPack, node) {
|
|
6622
|
+
const normalizedQuestion = normalizeQuestion(question);
|
|
6623
|
+
if (!normalizedQuestion || !contextPack)
|
|
6624
|
+
return false;
|
|
6625
|
+
const matches = contextPack.objects.filter((object) => {
|
|
6626
|
+
if (object.objectType !== 'dql_block')
|
|
6627
|
+
return false;
|
|
6628
|
+
if (object.status !== 'certified' && object.status !== 'approved' && object.payload?.certification !== 'certified')
|
|
6629
|
+
return false;
|
|
6630
|
+
const examples = Array.isArray(object.payload?.examples) ? object.payload.examples : [];
|
|
6631
|
+
return examples.some((example) => example
|
|
6632
|
+
&& typeof example === 'object'
|
|
6633
|
+
&& normalizeQuestion(String(example.question ?? '')) === normalizedQuestion);
|
|
6634
|
+
});
|
|
6635
|
+
return matches.length === 1 && normalizeQuestion(matches[0].name) === normalizeQuestion(node.name);
|
|
6636
|
+
}
|
|
6637
|
+
/**
|
|
6638
|
+
* A metadata-only definition answer is safe only for one certified artifact
|
|
6639
|
+
* named exactly by the user. Do not let FTS ranking decide this: a generic
|
|
6640
|
+
* definition phrase must never select a neighbouring block. Payload aliases
|
|
6641
|
+
* are authored block aliases; descriptions and tags are deliberately excluded.
|
|
6642
|
+
*/
|
|
6643
|
+
function uniqueNamedCertifiedDefinitionArtifact(question, kg, authorizedDomains) {
|
|
6644
|
+
const subject = namedArtifactDefinitionSubject(question);
|
|
6645
|
+
if (!subject)
|
|
6646
|
+
return null;
|
|
6647
|
+
const matches = kg.getNodesByKind('block', 1000).filter((node) => {
|
|
6648
|
+
if (node.status !== 'certified')
|
|
6649
|
+
return false;
|
|
6650
|
+
if (authorizedDomains.length > 0 && node.domain && !authorizedDomains.includes(node.domain))
|
|
6651
|
+
return false;
|
|
6652
|
+
return certifiedArtifactNames(node).some((name) => normalizeQuestion(name) === subject);
|
|
6653
|
+
});
|
|
6654
|
+
return matches.length === 1 ? { node: matches[0], score: 1, snippet: undefined } : null;
|
|
6655
|
+
}
|
|
6656
|
+
function isUniqueNamedCertifiedDefinitionQuestion(question, node, kg, authorizedDomains) {
|
|
6657
|
+
const selected = uniqueNamedCertifiedDefinitionArtifact(question, kg, authorizedDomains);
|
|
6658
|
+
return selected?.node.nodeId === node.nodeId;
|
|
6659
|
+
}
|
|
6660
|
+
function namedArtifactDefinitionSubject(question) {
|
|
6661
|
+
const match = /^\s*what\s+does\s+(?:the\s+)?(.+?)\s+(?:measure|mean|define|represent)\s*[?.!]*\s*$/i.exec(question);
|
|
6662
|
+
if (!match?.[1])
|
|
6663
|
+
return undefined;
|
|
6664
|
+
// Remove an optional explicit artifact noun while preserving the exact
|
|
6665
|
+
// authored identifier. `top_customers block` and `top_customers` therefore
|
|
6666
|
+
// resolve identically, while arbitrary definition questions have no subject
|
|
6667
|
+
// match and remain on the normal route.
|
|
6668
|
+
const subject = match[1]
|
|
6669
|
+
.replace(/\b(?:certified\s+)?(?:block|artifact|model|metric)\b/gi, ' ')
|
|
6670
|
+
.replace(/\s+/g, ' ')
|
|
6671
|
+
.trim();
|
|
6672
|
+
const normalized = normalizeQuestion(subject);
|
|
6673
|
+
return normalized || undefined;
|
|
6674
|
+
}
|
|
6675
|
+
function certifiedArtifactNames(node) {
|
|
6676
|
+
const aliases = Array.isArray(node.payload?.aliases)
|
|
6677
|
+
? node.payload.aliases.filter((alias) => typeof alias === 'string')
|
|
6678
|
+
: [];
|
|
6679
|
+
return [node.name, ...aliases];
|
|
6680
|
+
}
|
|
5681
6681
|
/**
|
|
5682
6682
|
* Reduce a possibly-composed prompt to the user's actual question for display.
|
|
5683
6683
|
* Callers (e.g. the Build-SQL-draft modal) sometimes prepend a multi-line instruction
|
|
@@ -6765,7 +7765,12 @@ function isBusinessDefinitionQuestion(question) {
|
|
|
6765
7765
|
}
|
|
6766
7766
|
/** Definition that may terminate with documentation instead of executing data. */
|
|
6767
7767
|
function isPureBusinessDefinitionQuestion(question) {
|
|
6768
|
-
return questionTypeFromText(question) === 'definition'
|
|
7768
|
+
return questionTypeFromText(question) === 'definition'
|
|
7769
|
+
// `questionTypeFromText` intentionally recognizes ranking before generic
|
|
7770
|
+
// definitions, so a named artifact such as `top_customers` would otherwise
|
|
7771
|
+
// look like a ranking request even in the explicit "what does … measure?"
|
|
7772
|
+
// form. Object-name matching remains mandatory at the terminal site.
|
|
7773
|
+
|| /^\s*what\s+does\s+(?:the\s+)?[^?.!]{1,160}\s+(?:measure|mean|define|represent)\s*[?.!]*\s*$/i.test(question);
|
|
6769
7774
|
}
|
|
6770
7775
|
function isBreakdownOrDrilldownQuestion(question) {
|
|
6771
7776
|
return /\b(break\s*down|breakdown|drill\s*(?:down|into)|slice|segment|split|by\s+[a-z][\w\s-]{1,40})\b/i.test(question);
|
|
@@ -6828,12 +7833,21 @@ function hasCertifiedNodeFit(question, plan, node, options = {}) {
|
|
|
6828
7833
|
plan,
|
|
6829
7834
|
block: node,
|
|
6830
7835
|
exactExampleMatch: exactExampleMatch || exactObjectRequest,
|
|
7836
|
+
uniqueExactExampleContract: options.uniqueExactExampleContract === true,
|
|
6831
7837
|
definitionLookup,
|
|
6832
7838
|
});
|
|
6833
7839
|
// Delegate the termination decision to THE single authority instead of a
|
|
6834
7840
|
// loop-local re-derivation (the seams between such copies produced certified
|
|
6835
7841
|
// answers that ignored member-scoped follow-ups).
|
|
6836
|
-
return certifiedTerminationVerdict({
|
|
7842
|
+
return certifiedTerminationVerdict({
|
|
7843
|
+
fit,
|
|
7844
|
+
// A named `what does <artifact> measure?` request is an artifact metadata
|
|
7845
|
+
// lookup, not a query-result claim. The termination authority still
|
|
7846
|
+
// rejects missing declared measures and unsupported filters before this
|
|
7847
|
+
// narrow bypass is considered.
|
|
7848
|
+
...(definitionLookup ? { bypass: 'definition_lookup' } : {}),
|
|
7849
|
+
allowInferredContract: options.allowInferredContract,
|
|
7850
|
+
}).allow;
|
|
6837
7851
|
}
|
|
6838
7852
|
function objectNameInQuestion(question, node) {
|
|
6839
7853
|
const questionText = normalizeQuestion(question);
|
|
@@ -7248,6 +8262,11 @@ async function requestSqlRepair(input) {
|
|
|
7248
8262
|
// bumping would let an internal preview-repair exceed that cap. Escalation-level
|
|
7249
8263
|
// repairs bump-then-clamp in the host (resolveRunReasoningEffort).
|
|
7250
8264
|
reasoningEffort: input.reasoningEffort,
|
|
8265
|
+
...(input.dispatchPhase ? { dispatchPhase: input.dispatchPhase } : {}),
|
|
8266
|
+
...(input.egressPurpose ? { egressPurpose: input.egressPurpose } : {}),
|
|
8267
|
+
...(input.maxProviderDispatches !== undefined
|
|
8268
|
+
? { maxProviderDispatches: input.maxProviderDispatches }
|
|
8269
|
+
: {}),
|
|
7251
8270
|
});
|
|
7252
8271
|
}
|
|
7253
8272
|
async function generateProposalWithOptionalTools(input) {
|
|
@@ -7255,6 +8274,11 @@ async function generateProposalWithOptionalTools(input) {
|
|
|
7255
8274
|
const options = {
|
|
7256
8275
|
signal: input.signal,
|
|
7257
8276
|
reasoningEffort: input.reasoningEffort,
|
|
8277
|
+
...(input.dispatchPhase ? { dispatchPhase: input.dispatchPhase } : {}),
|
|
8278
|
+
...(input.egressPurpose ? { egressPurpose: input.egressPurpose } : {}),
|
|
8279
|
+
...(input.maxProviderDispatches !== undefined
|
|
8280
|
+
? { maxProviderDispatches: input.maxProviderDispatches }
|
|
8281
|
+
: {}),
|
|
7258
8282
|
};
|
|
7259
8283
|
// No tools → plain generation (nothing for the loop to drive).
|
|
7260
8284
|
if (tools.length === 0) {
|
|
@@ -7651,10 +8675,25 @@ function isRetryableGeneratedSqlError(failure) {
|
|
|
7651
8675
|
return failure.retryDisposition === 'model_repair';
|
|
7652
8676
|
}
|
|
7653
8677
|
/**
|
|
7654
|
-
* Only SQL-shape/binding failures may leave
|
|
7655
|
-
* authorization, cancellation, and timeout failures would affect
|
|
7656
|
-
* route and must remain terminal rather than triggering wasteful
|
|
8678
|
+
* Only SQL-shape/binding failures may leave a frozen governed lane.
|
|
8679
|
+
* Connectivity, authorization, cancellation, and timeout failures would affect
|
|
8680
|
+
* every query route and must remain terminal rather than triggering wasteful
|
|
8681
|
+
* provider work.
|
|
7657
8682
|
*/
|
|
8683
|
+
function observabilityFailureForFrozenExecution(detail) {
|
|
8684
|
+
// Do not infer this from user-facing error text. The local runtime tags the
|
|
8685
|
+
// physical pre-execution boundary; this projection only carries that exact
|
|
8686
|
+
// typed fact into the redacted trace after a governed plan has frozen.
|
|
8687
|
+
if (detail?.origin !== 'host' || detail.stage !== 'execute' || detail.code !== 'connection_not_configured') {
|
|
8688
|
+
return undefined;
|
|
8689
|
+
}
|
|
8690
|
+
return {
|
|
8691
|
+
version: 1,
|
|
8692
|
+
phase: 'execution',
|
|
8693
|
+
cause: 'connection_not_configured',
|
|
8694
|
+
safeAction: 'configure_connection',
|
|
8695
|
+
};
|
|
8696
|
+
}
|
|
7658
8697
|
function isRetryableCertifiedExecutionError(error) {
|
|
7659
8698
|
return /\b(?:binder|parser|catalog)\s+error\b/i.test(error)
|
|
7660
8699
|
// DuckDB says "ambiguous reference"; Snowflake says "ambiguous column name".
|
|
@@ -8619,29 +9658,45 @@ function uniqueAssets(assets) {
|
|
|
8619
9658
|
return Array.from(byId.values());
|
|
8620
9659
|
}
|
|
8621
9660
|
/**
|
|
8622
|
-
* Execute the semantic layer's fanout probe
|
|
8623
|
-
*
|
|
8624
|
-
*
|
|
8625
|
-
*
|
|
9661
|
+
* Execute the semantic layer's fanout probe before a governed native semantic
|
|
9662
|
+
* join freezes. A native join that cannot be checked is not a governed-safe
|
|
9663
|
+
* join: fail closed with a redacted typed failure instead of executing a query
|
|
9664
|
+
* that could multiply aggregates. MetricFlow/dbt Cloud paths have their own
|
|
9665
|
+
* compiler guarantees and do not use this native direct-join guard.
|
|
8626
9666
|
*/
|
|
8627
9667
|
export async function probeSemanticJoinFanout(probeSql, joinedTables, executeSql) {
|
|
8628
9668
|
try {
|
|
8629
9669
|
const payload = await executeSql(probeSql);
|
|
8630
9670
|
const counts = parseFanoutProbeCounts(payload);
|
|
8631
|
-
if (!counts || counts.base <= 0
|
|
8632
|
-
return
|
|
9671
|
+
if (!counts || counts.base <= 0) {
|
|
9672
|
+
return {
|
|
9673
|
+
status: 'blocked',
|
|
9674
|
+
code: 'SEMANTIC_FANOUT_PROBE_UNPARSEABLE',
|
|
9675
|
+
message: 'DQL did not execute this governed semantic answer because its join-safety probe did not return verifiable row counts. Check the declared relationship or run the metric through MetricFlow / dbt Cloud, then retry.',
|
|
9676
|
+
};
|
|
9677
|
+
}
|
|
9678
|
+
if (counts.joined <= counts.base)
|
|
9679
|
+
return { status: 'safe' };
|
|
8633
9680
|
const factor = counts.joined / counts.base;
|
|
8634
9681
|
const tables = joinedTables.filter(Boolean).join(', ');
|
|
8635
|
-
return
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
9682
|
+
return {
|
|
9683
|
+
status: 'blocked',
|
|
9684
|
+
code: 'SEMANTIC_FANOUT_DUPLICATE_KEY',
|
|
9685
|
+
message: [
|
|
9686
|
+
`Blocked a governed semantic answer whose join inflates results: joining ${tables || 'the declared tables'}`,
|
|
9687
|
+
`turned ${counts.base.toLocaleString('en-US')} base rows into ${counts.joined.toLocaleString('en-US')} (×${factor >= 10 ? Math.round(factor) : factor.toFixed(1)}).`,
|
|
9688
|
+
'The declared join key is not unique on the joined side, so every aggregated value would be multiplied.',
|
|
9689
|
+
'Deduplicate the joined model (for example, filter a slowly-changing dimension to current records)',
|
|
9690
|
+
'or execute this metric through MetricFlow / dbt Cloud, then retry. No numbers were shown because they would be wrong.',
|
|
9691
|
+
].join(' '),
|
|
9692
|
+
};
|
|
8642
9693
|
}
|
|
8643
9694
|
catch {
|
|
8644
|
-
return
|
|
9695
|
+
return {
|
|
9696
|
+
status: 'blocked',
|
|
9697
|
+
code: 'SEMANTIC_FANOUT_PROBE_ERROR',
|
|
9698
|
+
message: 'DQL did not execute this governed semantic answer because it could not verify join fanout before aggregation. Check the declared relationship or run the metric through MetricFlow / dbt Cloud, then retry.',
|
|
9699
|
+
};
|
|
8645
9700
|
}
|
|
8646
9701
|
}
|
|
8647
9702
|
function parseFanoutProbeCounts(payload) {
|