@duckcodeailabs/dql-agent 1.14.2 → 1.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-run-engine.d.ts +239 -4
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +2924 -85
- 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 +51 -3
- 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 +591 -11
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +9 -1
- package/dist/agentic/analyst-loop.d.ts.map +1 -1
- package/dist/agentic/analyst-loop.js +204 -4
- 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/ledger-tools.d.ts.map +1 -1
- package/dist/agentic/ledger-tools.js +8 -1
- package/dist/agentic/ledger-tools.js.map +1 -1
- package/dist/agentic/orchestrator-policy.d.ts +3 -1
- package/dist/agentic/orchestrator-policy.d.ts.map +1 -1
- package/dist/agentic/orchestrator-policy.js +1 -1
- package/dist/agentic/orchestrator-policy.js.map +1 -1
- package/dist/agentic/research-agent.d.ts.map +1 -1
- package/dist/agentic/research-agent.js +10 -1
- package/dist/agentic/research-agent.js.map +1 -1
- package/dist/agentic/sql-authorization.d.ts +8 -0
- package/dist/agentic/sql-authorization.d.ts.map +1 -1
- package/dist/agentic/sql-authorization.js +3 -0
- package/dist/agentic/sql-authorization.js.map +1 -1
- package/dist/agentic/tool-loop.d.ts +13 -1
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +441 -32
- package/dist/agentic/tool-loop.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 +42 -1
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +490 -26
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +1259 -9
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +970 -84
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +230 -10
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +1182 -85
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +27 -0
- package/dist/answer-shape.d.ts.map +1 -1
- package/dist/answer-shape.js +55 -0
- 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 +523 -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 +1089 -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 +380 -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/ask-runtime/ask-agent-runtime-v2.d.ts +850 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts.map +1 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js +1716 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts +95 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.js +8280 -0
- package/dist/ask-runtime/ask-analyst-runtime.js.map +1 -0
- package/dist/conversation/session-store.d.ts +31 -0
- package/dist/conversation/session-store.d.ts.map +1 -1
- package/dist/conversation/session-store.js +38 -0
- package/dist/conversation/session-store.js.map +1 -1
- package/dist/conversation/snapshot.d.ts +4 -1
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +85 -3
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/conversation/turn-trust.d.ts.map +1 -1
- package/dist/conversation/turn-trust.js +2 -0
- package/dist/conversation/turn-trust.js.map +1 -1
- package/dist/embeddings/provider.d.ts +1 -0
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js +18 -0
- package/dist/embeddings/provider.js.map +1 -1
- 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 +246 -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 +21 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -7
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +87 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +2 -2
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.js +32 -2
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +207 -6
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +439 -53
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +78 -7
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts +14 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +29 -2
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +28 -2
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +373 -16
- 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 +28 -11
- package/dist/provider-egress.d.ts.map +1 -1
- package/dist/provider-egress.js +72 -22
- package/dist/provider-egress.js.map +1 -1
- package/dist/providers/claude.d.ts +2 -2
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +201 -37
- 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.d.ts +2 -2
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +213 -43
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +131 -1
- 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/requirement-clauses.d.ts +23 -0
- package/dist/requirement-clauses.d.ts.map +1 -0
- package/dist/requirement-clauses.js +70 -0
- package/dist/requirement-clauses.js.map +1 -0
- package/dist/resolved-analytical-plan.d.ts +18 -4
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +953 -50
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +118 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +2481 -232
- package/dist/router.js.map +1 -1
- package/dist/semantic-bridge/compose.d.ts.map +1 -1
- package/dist/semantic-bridge/compose.js +23 -4
- package/dist/semantic-bridge/compose.js.map +1 -1
- package/package.json +4 -4
package/dist/answer-loop.js
CHANGED
|
@@ -35,13 +35,13 @@ 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';
|
|
42
42
|
import { compactSqlSnippet, extractSimpleSelectShape, selectExpressionOutputName, } from './metadata/sql-shape.js';
|
|
43
43
|
import { composeSemanticQueryForQuestion, composeSemanticQueryFromCompiledMembers, composeSemanticQueryFromMembers, renderSemanticDqlArtifact, semanticDqlArtifactName, } from './semantic-bridge/compose.js';
|
|
44
|
-
import {
|
|
44
|
+
import { runAgenticToolLoopDetailed } from './agentic/tool-loop.js';
|
|
45
45
|
import { buildSemanticStageTools } from './agentic/toolset.js';
|
|
46
46
|
import { deriveAgenticTrust } from './agentic/answer-contract.js';
|
|
47
47
|
import { selectSemanticMembersViaLlm } from './semantic-bridge/member-select.js';
|
|
@@ -384,6 +384,572 @@ function certifiedInvocationInputs(block, plan) {
|
|
|
384
384
|
}
|
|
385
385
|
return Object.keys(parameters).length > 0 ? { parameters, parameterSources } : {};
|
|
386
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Build the one typed certified-block invocation used by both the ordinary
|
|
389
|
+
* answer loop and the authoritative V2 exact-fit path. Keeping this at the
|
|
390
|
+
* DQL boundary is important: a zero-provider Tier 1 shortcut may avoid
|
|
391
|
+
* conversational planning, but it must not avoid declared parameter binding,
|
|
392
|
+
* validation, or the normal overall top-N execution bound.
|
|
393
|
+
*/
|
|
394
|
+
export function buildCertifiedBlockInvocationInput(block, plan, question) {
|
|
395
|
+
return {
|
|
396
|
+
question,
|
|
397
|
+
...certifiedInvocationInputs(block, plan),
|
|
398
|
+
...(plan.requestedShape.topN?.scope === 'per_group'
|
|
399
|
+
? {}
|
|
400
|
+
: plan.requestedShape.topN?.n
|
|
401
|
+
? { rowLimit: plan.requestedShape.topN.n }
|
|
402
|
+
: {}),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Prove that an immutable certified block may satisfy a question-driven
|
|
407
|
+
* overall top-N result contract without conversational planning. A declared
|
|
408
|
+
* limit parameter alone is not enough: the authored SQL must order rows and
|
|
409
|
+
* consume that exact parameter in its LIMIT clause. This prevents a host
|
|
410
|
+
* from slicing arbitrary connector order and presenting it as certified.
|
|
411
|
+
*/
|
|
412
|
+
export function certifiedBlockProvesRequestedTopN(block, plan, options = {}) {
|
|
413
|
+
const topN = plan.requestedShape.topN;
|
|
414
|
+
if (!topN)
|
|
415
|
+
return true;
|
|
416
|
+
// The artifact is loaded from the snapshot-bound KG, but that persisted
|
|
417
|
+
// payload still crosses an older JSON schema boundary. Treat a malformed
|
|
418
|
+
// parameter contract as *not proved*, never as a reason to crash the Ask
|
|
419
|
+
// run or to infer an unordered ranking from connector row order.
|
|
420
|
+
if (topN.scope !== 'overall' || !block || block.kind !== 'block')
|
|
421
|
+
return false;
|
|
422
|
+
const limitParameters = (Array.isArray(block.parameters) ? block.parameters : []).flatMap((parameter) => {
|
|
423
|
+
if (!parameter || typeof parameter.name !== 'string' || parameter.name.trim() === '')
|
|
424
|
+
return [];
|
|
425
|
+
return parameter.name === 'top_n' || parameter.binding?.kind === 'limit'
|
|
426
|
+
? [parameter]
|
|
427
|
+
: [];
|
|
428
|
+
});
|
|
429
|
+
if (typeof block.sql !== 'string')
|
|
430
|
+
return false;
|
|
431
|
+
const outerClauses = scanOutermostTopNClauses(block.sql);
|
|
432
|
+
if (!outerClauses)
|
|
433
|
+
return false;
|
|
434
|
+
const { orderBy, limitValue } = outerClauses;
|
|
435
|
+
// There are exactly two safe ways to prove the overall row bound:
|
|
436
|
+
//
|
|
437
|
+
// 1. The immutable artifact consumes its own declared top-N parameter in
|
|
438
|
+
// the outer LIMIT clause (the ordinary certified contract); or
|
|
439
|
+
// 2. The artifact intentionally has no outer LIMIT and the local host owns
|
|
440
|
+
// a frozen, typed execution row limit at the read-only SQL boundary.
|
|
441
|
+
//
|
|
442
|
+
// We never treat a fixed, driver-style, compound, or stale outer LIMIT as
|
|
443
|
+
// equivalent to the requested value. In particular, an existing fixed
|
|
444
|
+
// LIMIT prevents the host executor from appending the frozen limit, so it
|
|
445
|
+
// cannot prove a different user-requested top N.
|
|
446
|
+
const limitUsesDeclaredParameter = limitParameters.length === 1
|
|
447
|
+
&& typeof limitValue === 'string'
|
|
448
|
+
&& outerLimitUsesDeclaredTopNParameter(limitValue, limitParameters[0].name);
|
|
449
|
+
const hostOwnsFrozenRowLimit = limitValue === undefined
|
|
450
|
+
&& Number.isInteger(options.hostEnforcedRowLimit)
|
|
451
|
+
&& options.hostEnforcedRowLimit === topN.n
|
|
452
|
+
&& options.hostEnforcedRowLimit > 0;
|
|
453
|
+
if (!limitUsesDeclaredParameter && !hostOwnsFrozenRowLimit)
|
|
454
|
+
return false;
|
|
455
|
+
// A question-driven ranking is only exact when the *primary* authored sort
|
|
456
|
+
// expression proves the requested measure. Finding `revenue DESC` later in
|
|
457
|
+
// `ORDER BY customer_name ASC, revenue DESC` is not sufficient: the result
|
|
458
|
+
// is primarily alphabetical, not a top-revenue result. Do not infer a
|
|
459
|
+
// ranking measure from row shape or candidate tags; that would turn an
|
|
460
|
+
// unproven certified artifact into a false exact answer.
|
|
461
|
+
const requestedMeasures = plan.requestedShape.measures
|
|
462
|
+
.map(normalizedTopNMetricId)
|
|
463
|
+
.filter(Boolean);
|
|
464
|
+
// An omitted measure is not normally a license to infer a ranking from a
|
|
465
|
+
// certified block. The narrow exception is server-owned evidence that this
|
|
466
|
+
// is either a direct certified question/title/alias match *or* the one
|
|
467
|
+
// complete certified tuple admitted by this immutable retrieval snapshot.
|
|
468
|
+
// In both cases the artifact's own primary non-dimension output supplies
|
|
469
|
+
// the authored default ranking contract. A provider cannot manufacture
|
|
470
|
+
// either flag from text or a card count.
|
|
471
|
+
const useAuthoredRankingDefault = requestedMeasures.length === 0
|
|
472
|
+
&& (options.exactCertifiedQuestionMatch === true
|
|
473
|
+
|| options.uniqueCompleteCertifiedFit === true);
|
|
474
|
+
if (requestedMeasures.length === 0 && !useAuthoredRankingDefault)
|
|
475
|
+
return false;
|
|
476
|
+
const firstOrderExpression = splitTopLevelSqlList(orderBy)[0];
|
|
477
|
+
if (!firstOrderExpression)
|
|
478
|
+
return false;
|
|
479
|
+
const firstOrder = parseTopNOrderExpression(firstOrderExpression);
|
|
480
|
+
if (!firstOrder)
|
|
481
|
+
return false;
|
|
482
|
+
const requiredDirection = plan.requestedShape.rankingDirection === 'bottom' ? 'asc' : 'desc';
|
|
483
|
+
if (firstOrder.direction !== requiredDirection)
|
|
484
|
+
return false;
|
|
485
|
+
const orderMetricIds = topNOrderMetricIds(firstOrder.expression, block);
|
|
486
|
+
if (requestedMeasures.length > 0) {
|
|
487
|
+
return requestedMeasures.some((measure) => orderMetricIds.has(measure));
|
|
488
|
+
}
|
|
489
|
+
return [...authoredTopNRankingMetricIds(block)].some((metric) => orderMetricIds.has(metric));
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* The implicit-ranking exception still needs an authored metric, not merely a
|
|
493
|
+
* sortable dimension. Keep this proof inside the immutable block contract:
|
|
494
|
+
* declared outputs, compiler output lineage, and typed output roles are all
|
|
495
|
+
* captured with the artifact. If an older block cannot distinguish its
|
|
496
|
+
* measures from dimensions, fail closed into the Ask planner.
|
|
497
|
+
*/
|
|
498
|
+
function authoredTopNRankingMetricIds(block) {
|
|
499
|
+
const dimensionIds = new Set((block.dimensions ?? []).map(normalizedTopNMetricId).filter(Boolean));
|
|
500
|
+
const outputRoles = new Map((block.outputContract ?? [])
|
|
501
|
+
.filter((output) => typeof output?.name === 'string')
|
|
502
|
+
.map((output) => [normalizedTopNMetricId(output.name), String(output.role ?? '').toLowerCase()]));
|
|
503
|
+
const outputIds = new Set([
|
|
504
|
+
...(block.declaredOutputs ?? []),
|
|
505
|
+
...(block.outputs ?? []).map((output) => output.name),
|
|
506
|
+
...(block.outputContract ?? []).map((output) => output.name),
|
|
507
|
+
].map(normalizedTopNMetricId).filter(Boolean));
|
|
508
|
+
const metrics = new Set();
|
|
509
|
+
for (const outputId of outputIds) {
|
|
510
|
+
const role = outputRoles.get(outputId) ?? '';
|
|
511
|
+
if (dimensionIds.has(outputId) || /(?:dimension|entity|label|attribute)/.test(role))
|
|
512
|
+
continue;
|
|
513
|
+
metrics.add(outputId);
|
|
514
|
+
}
|
|
515
|
+
return metrics;
|
|
516
|
+
}
|
|
517
|
+
function outerLimitUsesDeclaredTopNParameter(limitValue, parameterName) {
|
|
518
|
+
const escapedParameter = parameterName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
519
|
+
// This proof is about an authored DQL artifact, not a connector SQL
|
|
520
|
+
// statement. Accept only the two compiler-recognized interpolation forms
|
|
521
|
+
// (`${name}` and `{name}`); driver placeholders, bare identifiers, fixed
|
|
522
|
+
// values, and compound expressions do not prove that the requested typed
|
|
523
|
+
// top-N binding controls the outer result contract.
|
|
524
|
+
return new RegExp(`^(?:\\$\\{\\s*${escapedParameter}\\s*\\}|\\{\\s*${escapedParameter}\\s*\\})$`, 'i').test(topNVisibleSql(limitValue).trim());
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Locate the outer SELECT/ORDER BY/LIMIT clauses without treating text in a
|
|
528
|
+
* CTE, subquery, quoted identifier/string, or comment as part of the answer
|
|
529
|
+
* contract. A ranking proof must be about the query that actually returns the
|
|
530
|
+
* block rows, never an unused inner query that happens to mention revenue.
|
|
531
|
+
*/
|
|
532
|
+
function scanOutermostTopNClauses(sql) {
|
|
533
|
+
const tokens = topLevelSqlTokens(sql);
|
|
534
|
+
if (!tokens)
|
|
535
|
+
return undefined;
|
|
536
|
+
let orderByIndex = -1;
|
|
537
|
+
for (let index = 0; index + 1 < tokens.length; index += 1) {
|
|
538
|
+
if (tokens[index].text === 'order' && tokens[index + 1].text === 'by')
|
|
539
|
+
orderByIndex = index;
|
|
540
|
+
}
|
|
541
|
+
if (orderByIndex < 0)
|
|
542
|
+
return undefined;
|
|
543
|
+
let selectIndex = -1;
|
|
544
|
+
for (let index = 0; index < orderByIndex; index += 1) {
|
|
545
|
+
if (tokens[index].text === 'select')
|
|
546
|
+
selectIndex = index;
|
|
547
|
+
}
|
|
548
|
+
if (selectIndex < 0)
|
|
549
|
+
return undefined;
|
|
550
|
+
let fromIndex = -1;
|
|
551
|
+
for (let index = selectIndex + 1; index < orderByIndex; index += 1) {
|
|
552
|
+
if (tokens[index].text === 'from') {
|
|
553
|
+
fromIndex = index;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (fromIndex < 0)
|
|
558
|
+
return undefined;
|
|
559
|
+
let limitIndex = -1;
|
|
560
|
+
for (let index = orderByIndex + 2; index < tokens.length; index += 1) {
|
|
561
|
+
if (tokens[index].text === 'limit') {
|
|
562
|
+
limitIndex = index;
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const orderByEnd = limitIndex >= 0 ? tokens[limitIndex].start : sql.length;
|
|
567
|
+
// The outer ORDER BY must directly govern the outer LIMIT when one exists.
|
|
568
|
+
// A set operation, second SELECT, or a second ORDER BY in between is too
|
|
569
|
+
// complex for this exact shortcut and correctly falls back to the bounded
|
|
570
|
+
// Ask tool runtime. The same guard applies through end-of-query for an
|
|
571
|
+
// intentionally unbounded artifact whose host owns the frozen row limit.
|
|
572
|
+
if (tokens.slice(orderByIndex + 2, limitIndex >= 0 ? limitIndex : tokens.length).some((token) => token.text === 'select' || token.text === 'union' || token.text === 'intersect' || token.text === 'except')) {
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
const afterLimit = limitIndex >= 0 ? tokens.find((token, index) => index > limitIndex && (token.text === 'offset' || token.text === 'fetch' || token.text === 'for'))?.start ?? sql.length : sql.length;
|
|
576
|
+
const selectList = sql.slice(tokens[selectIndex].end, tokens[fromIndex].start).trim();
|
|
577
|
+
const orderBy = sql.slice(tokens[orderByIndex + 1].end, orderByEnd).trim();
|
|
578
|
+
const limitValue = limitIndex >= 0
|
|
579
|
+
? sql.slice(tokens[limitIndex].end, afterLimit).replace(/;\s*$/, '').trim()
|
|
580
|
+
: undefined;
|
|
581
|
+
if (!selectList || !orderBy || (limitIndex >= 0 && !limitValue))
|
|
582
|
+
return undefined;
|
|
583
|
+
return {
|
|
584
|
+
selectList,
|
|
585
|
+
orderBy,
|
|
586
|
+
...(limitValue ? { limitValue } : {}),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Tokenize only depth-zero SQL words. The state machine deliberately removes
|
|
591
|
+
* comments and literals from consideration while retaining positional ranges
|
|
592
|
+
* against the original SQL for clause slicing.
|
|
593
|
+
*/
|
|
594
|
+
function topLevelSqlTokens(sql) {
|
|
595
|
+
const tokens = [];
|
|
596
|
+
let depth = 0;
|
|
597
|
+
let quote;
|
|
598
|
+
let lineComment = false;
|
|
599
|
+
let blockCommentDepth = 0;
|
|
600
|
+
for (let index = 0; index < sql.length; index += 1) {
|
|
601
|
+
const character = sql[index];
|
|
602
|
+
const next = sql[index + 1];
|
|
603
|
+
if (lineComment) {
|
|
604
|
+
if (character === '\n' || character === '\r')
|
|
605
|
+
lineComment = false;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (blockCommentDepth > 0) {
|
|
609
|
+
if (character === '/' && next === '*') {
|
|
610
|
+
blockCommentDepth += 1;
|
|
611
|
+
index += 1;
|
|
612
|
+
}
|
|
613
|
+
else if (character === '*' && next === '/') {
|
|
614
|
+
blockCommentDepth -= 1;
|
|
615
|
+
index += 1;
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (quote) {
|
|
620
|
+
if (quote === ']' && character === ']') {
|
|
621
|
+
if (next === ']')
|
|
622
|
+
index += 1;
|
|
623
|
+
else
|
|
624
|
+
quote = undefined;
|
|
625
|
+
}
|
|
626
|
+
else if (quote !== ']' && character === quote) {
|
|
627
|
+
if (next === quote)
|
|
628
|
+
index += 1;
|
|
629
|
+
else
|
|
630
|
+
quote = undefined;
|
|
631
|
+
}
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (character === '-' && next === '-') {
|
|
635
|
+
lineComment = true;
|
|
636
|
+
index += 1;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (character === '/' && next === '*') {
|
|
640
|
+
blockCommentDepth = 1;
|
|
641
|
+
index += 1;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (character === "'" || character === '"' || character === '`') {
|
|
645
|
+
quote = character;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (character === '[') {
|
|
649
|
+
quote = ']';
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (character === '(') {
|
|
653
|
+
depth += 1;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (character === ')') {
|
|
657
|
+
if (depth === 0)
|
|
658
|
+
return undefined;
|
|
659
|
+
depth -= 1;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (depth !== 0 || !/[A-Za-z_]/.test(character))
|
|
663
|
+
continue;
|
|
664
|
+
const start = index;
|
|
665
|
+
index += 1;
|
|
666
|
+
while (index < sql.length && /[A-Za-z0-9_$]/.test(sql[index]))
|
|
667
|
+
index += 1;
|
|
668
|
+
tokens.push({ text: sql.slice(start, index).toLowerCase(), start, end: index });
|
|
669
|
+
index -= 1;
|
|
670
|
+
}
|
|
671
|
+
// A line comment is valid through EOF. Unterminated string, bracket, block
|
|
672
|
+
// comment, or parenthesis state is not a trustworthy exact-proof input.
|
|
673
|
+
return depth === 0 && !quote && blockCommentDepth === 0 ? tokens : undefined;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Split an authored comma-separated SQL list without treating a function
|
|
677
|
+
* argument, quoted string, or quoted identifier as a second ORDER BY key.
|
|
678
|
+
* This intentionally stays small and fails closed for malformed SQL; DQL's
|
|
679
|
+
* compiler is still the authority for execution syntax.
|
|
680
|
+
*/
|
|
681
|
+
function splitTopLevelSqlList(value) {
|
|
682
|
+
const expressions = [];
|
|
683
|
+
let start = 0;
|
|
684
|
+
let depth = 0;
|
|
685
|
+
let quote;
|
|
686
|
+
let lineComment = false;
|
|
687
|
+
let blockCommentDepth = 0;
|
|
688
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
689
|
+
const character = value[index];
|
|
690
|
+
const next = value[index + 1];
|
|
691
|
+
if (lineComment) {
|
|
692
|
+
if (character === '\n' || character === '\r')
|
|
693
|
+
lineComment = false;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (blockCommentDepth > 0) {
|
|
697
|
+
if (character === '/' && next === '*') {
|
|
698
|
+
blockCommentDepth += 1;
|
|
699
|
+
index += 1;
|
|
700
|
+
}
|
|
701
|
+
else if (character === '*' && next === '/') {
|
|
702
|
+
blockCommentDepth -= 1;
|
|
703
|
+
index += 1;
|
|
704
|
+
}
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (quote) {
|
|
708
|
+
if (quote === ']' && character === ']') {
|
|
709
|
+
if (next === ']')
|
|
710
|
+
index += 1;
|
|
711
|
+
else
|
|
712
|
+
quote = undefined;
|
|
713
|
+
}
|
|
714
|
+
else if (quote !== ']' && character === quote) {
|
|
715
|
+
// SQL escapes an in-string quote by doubling it. Keep scanning inside
|
|
716
|
+
// the quoted value rather than mistaking a later comma for a list
|
|
717
|
+
// separator.
|
|
718
|
+
if (value[index + 1] === quote) {
|
|
719
|
+
index += 1;
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
quote = undefined;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (character === '-' && next === '-') {
|
|
728
|
+
lineComment = true;
|
|
729
|
+
index += 1;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (character === '/' && next === '*') {
|
|
733
|
+
blockCommentDepth = 1;
|
|
734
|
+
index += 1;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (character === "'" || character === '"' || character === '`') {
|
|
738
|
+
quote = character;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (character === '[') {
|
|
742
|
+
quote = ']';
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (character === '(') {
|
|
746
|
+
depth += 1;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (character === ')' && depth > 0) {
|
|
750
|
+
depth -= 1;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (character === ',' && depth === 0) {
|
|
754
|
+
const expression = value.slice(start, index).trim();
|
|
755
|
+
if (!expression)
|
|
756
|
+
return [];
|
|
757
|
+
expressions.push(expression);
|
|
758
|
+
start = index + 1;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (quote || blockCommentDepth > 0 || depth !== 0)
|
|
762
|
+
return [];
|
|
763
|
+
const finalExpression = value.slice(start).trim();
|
|
764
|
+
return finalExpression ? [...expressions, finalExpression] : [];
|
|
765
|
+
}
|
|
766
|
+
function parseTopNOrderExpression(expression) {
|
|
767
|
+
const visible = topNVisibleSql(expression);
|
|
768
|
+
const match = visible.match(/^(.*?)(?:\s+(asc|desc))(?:\s+nulls\s+(?:first|last))?\s*$/i);
|
|
769
|
+
if (!match?.[1] || !match[2])
|
|
770
|
+
return undefined;
|
|
771
|
+
return {
|
|
772
|
+
expression: expression.slice(0, match[1].length).trim(),
|
|
773
|
+
direction: match[2].toLowerCase(),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/** Preserve SQL positions while blanking literals/comments that cannot prove a ranking contract. */
|
|
777
|
+
function topNVisibleSql(value) {
|
|
778
|
+
let output = '';
|
|
779
|
+
let quote;
|
|
780
|
+
let lineComment = false;
|
|
781
|
+
let blockCommentDepth = 0;
|
|
782
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
783
|
+
const character = value[index];
|
|
784
|
+
const next = value[index + 1];
|
|
785
|
+
const blank = () => { output += character === '\n' || character === '\r' ? character : ' '; };
|
|
786
|
+
if (lineComment) {
|
|
787
|
+
blank();
|
|
788
|
+
if (character === '\n' || character === '\r')
|
|
789
|
+
lineComment = false;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (blockCommentDepth > 0) {
|
|
793
|
+
blank();
|
|
794
|
+
if (character === '/' && next === '*') {
|
|
795
|
+
blockCommentDepth += 1;
|
|
796
|
+
index += 1;
|
|
797
|
+
output += ' ';
|
|
798
|
+
}
|
|
799
|
+
else if (character === '*' && next === '/') {
|
|
800
|
+
blockCommentDepth -= 1;
|
|
801
|
+
index += 1;
|
|
802
|
+
output += ' ';
|
|
803
|
+
}
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (quote === "'") {
|
|
807
|
+
blank();
|
|
808
|
+
if (character === "'") {
|
|
809
|
+
if (next === "'") {
|
|
810
|
+
index += 1;
|
|
811
|
+
output += ' ';
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
quote = undefined;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (quote) {
|
|
820
|
+
// Keep quoted identifiers intact for identifier normalization while the
|
|
821
|
+
// outer-clause scanner itself ignores their contents.
|
|
822
|
+
output += character;
|
|
823
|
+
if (quote === ']' && character === ']') {
|
|
824
|
+
if (next === ']') {
|
|
825
|
+
index += 1;
|
|
826
|
+
output += next;
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
quote = undefined;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
else if (quote !== ']' && character === quote) {
|
|
833
|
+
if (next === quote) {
|
|
834
|
+
index += 1;
|
|
835
|
+
output += next;
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
quote = undefined;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
if (character === '-' && next === '-') {
|
|
844
|
+
output += ' ';
|
|
845
|
+
index += 1;
|
|
846
|
+
lineComment = true;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (character === '/' && next === '*') {
|
|
850
|
+
output += ' ';
|
|
851
|
+
index += 1;
|
|
852
|
+
blockCommentDepth = 1;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (character === "'") {
|
|
856
|
+
output += ' ';
|
|
857
|
+
quote = character;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (character === '"' || character === '`') {
|
|
861
|
+
output += character;
|
|
862
|
+
quote = character;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (character === '[') {
|
|
866
|
+
output += character;
|
|
867
|
+
quote = ']';
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
output += character;
|
|
871
|
+
}
|
|
872
|
+
return output;
|
|
873
|
+
}
|
|
874
|
+
/** Normalize an identifier or business measure into its opaque-free SQL key. */
|
|
875
|
+
function normalizedTopNMetricId(value) {
|
|
876
|
+
return value
|
|
877
|
+
.trim()
|
|
878
|
+
.replace(/^['"`\[]|['"`\]]$/g, '')
|
|
879
|
+
.split('.')
|
|
880
|
+
.at(-1)
|
|
881
|
+
.trim()
|
|
882
|
+
.replace(/^['"`\[]|['"`\]]$/g, '')
|
|
883
|
+
.toLowerCase()
|
|
884
|
+
.replace(/[^a-z0-9]+/g, '');
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Resolve the primary ORDER BY expression through selected-output aliases and
|
|
888
|
+
* captured output lineage. Qualified columns (`c.revenue`), SQL aliases
|
|
889
|
+
* (`SUM(o.revenue) AS revenue` / `ORDER BY revenue`), and compiler-captured
|
|
890
|
+
* output sources all converge on the same normalized key. Unknown or
|
|
891
|
+
* ambiguous expressions deliberately yield no proof.
|
|
892
|
+
*/
|
|
893
|
+
function topNOrderMetricIds(expression, block) {
|
|
894
|
+
const keys = topNIdentifierIds(expression);
|
|
895
|
+
const selectedAliases = selectedSqlOutputAliases(block.sql);
|
|
896
|
+
const outputLineage = block.outputs ?? [];
|
|
897
|
+
for (const key of [...keys]) {
|
|
898
|
+
const selectedExpression = selectedAliases.get(key);
|
|
899
|
+
if (selectedExpression) {
|
|
900
|
+
for (const selectedKey of topNIdentifierIds(selectedExpression))
|
|
901
|
+
keys.add(selectedKey);
|
|
902
|
+
}
|
|
903
|
+
for (const output of outputLineage) {
|
|
904
|
+
if (normalizedTopNMetricId(output.name) !== key)
|
|
905
|
+
continue;
|
|
906
|
+
for (const source of output.sources ?? []) {
|
|
907
|
+
const sourceKey = normalizedTopNMetricId(source.column);
|
|
908
|
+
if (sourceKey)
|
|
909
|
+
keys.add(sourceKey);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return keys;
|
|
914
|
+
}
|
|
915
|
+
function topNIdentifierIds(expression) {
|
|
916
|
+
const keys = new Set();
|
|
917
|
+
// Quoted identifiers remain intact, while literals/comments were blanked.
|
|
918
|
+
// Therefore a string such as `'revenue'` cannot prove a ranking measure.
|
|
919
|
+
const identifierPattern = /(?:\[[^\]]+\]|"(?:""|[^"])+"|`(?:``|[^`])+`|[A-Za-z_][A-Za-z0-9_$]*)/g;
|
|
920
|
+
for (const identifier of topNVisibleSql(expression).match(identifierPattern) ?? []) {
|
|
921
|
+
const key = normalizedTopNMetricId(identifier);
|
|
922
|
+
if (key && !TOP_N_SQL_WORDS.has(key))
|
|
923
|
+
keys.add(key);
|
|
924
|
+
}
|
|
925
|
+
return keys;
|
|
926
|
+
}
|
|
927
|
+
function selectedSqlOutputAliases(sql) {
|
|
928
|
+
if (!sql)
|
|
929
|
+
return new Map();
|
|
930
|
+
const select = scanOutermostTopNClauses(sql)?.selectList;
|
|
931
|
+
if (!select)
|
|
932
|
+
return new Map();
|
|
933
|
+
const aliases = new Map();
|
|
934
|
+
for (const projection of splitTopLevelSqlList(select)) {
|
|
935
|
+
const visibleProjection = topNVisibleSql(projection);
|
|
936
|
+
const explicitAlias = visibleProjection.match(/^(.*?)\s+as\s+((?:\[[^\]]+\])|(?:"(?:""|[^"])+")|(?:`(?:``|[^`])+`)|(?:[A-Za-z_][A-Za-z0-9_$]*))\s*$/i);
|
|
937
|
+
const implicitAlias = explicitAlias
|
|
938
|
+
? undefined
|
|
939
|
+
: visibleProjection.match(/^(.*?)\s+((?:\[[^\]]+\])|(?:"(?:""|[^"])+")|(?:`(?:``|[^`])+`)|(?:[A-Za-z_][A-Za-z0-9_$]*))\s*$/i);
|
|
940
|
+
const expressionLength = (explicitAlias?.[1] ?? implicitAlias?.[1] ?? visibleProjection).length;
|
|
941
|
+
const expression = projection.slice(0, expressionLength).trim();
|
|
942
|
+
const alias = (explicitAlias?.[2] ?? implicitAlias?.[2] ?? projection).trim();
|
|
943
|
+
const aliasId = normalizedTopNMetricId(alias);
|
|
944
|
+
if (aliasId)
|
|
945
|
+
aliases.set(aliasId, expression);
|
|
946
|
+
}
|
|
947
|
+
return aliases;
|
|
948
|
+
}
|
|
949
|
+
const TOP_N_SQL_WORDS = new Set([
|
|
950
|
+
'asc', 'desc', 'nulls', 'first', 'last', 'sum', 'avg', 'average', 'count',
|
|
951
|
+
'min', 'max', 'cast', 'coalesce', 'case', 'when', 'then', 'else', 'end',
|
|
952
|
+
]);
|
|
387
953
|
/**
|
|
388
954
|
* Return only output aliases that the selected certified artifact itself
|
|
389
955
|
* declares. This is deliberately built after the router has frozen the plan:
|
|
@@ -625,6 +1191,41 @@ export function renderContextValidationRefusalForUser(code, machineError, member
|
|
|
625
1191
|
: 'I could not prepare a governed query from the retrieved metadata. Name the specific metric or table and how to break it down, and I can generate a review-required draft.';
|
|
626
1192
|
}
|
|
627
1193
|
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Did validation fail because something the USER asked for is not modeled?
|
|
1196
|
+
*
|
|
1197
|
+
* "What region does he belong to" against a warehouse with no customer→region
|
|
1198
|
+
* path fails as `unknown_column: region`. Reported as a grounding gap that
|
|
1199
|
+
* reads "Not enough context to answer safely", which is untrue and unhelpful:
|
|
1200
|
+
* no amount of extra context will produce a column the business has never
|
|
1201
|
+
* modeled, and the user is left to guess whether to rephrase, re-ask, or give
|
|
1202
|
+
* up. The truthful answer names the gap and offers the way forward.
|
|
1203
|
+
*
|
|
1204
|
+
* Deliberately narrow. A column the MODEL invented is a grounding failure and
|
|
1205
|
+
* must keep its existing code — the distinction between "you asked for
|
|
1206
|
+
* something we don't have" and "the draft referenced something that isn't
|
|
1207
|
+
* there" is the whole safety value of this classification.
|
|
1208
|
+
*/
|
|
1209
|
+
function requestedDimensionModelingGap(code, offending, requestedTerms) {
|
|
1210
|
+
if (code !== 'unknown_column' && code !== 'unknown_relation')
|
|
1211
|
+
return undefined;
|
|
1212
|
+
const identifier = offending?.column ?? offending?.relation;
|
|
1213
|
+
if (!identifier)
|
|
1214
|
+
return undefined;
|
|
1215
|
+
const canonical = (value) => value
|
|
1216
|
+
.split(/[.:]/).pop()
|
|
1217
|
+
.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
1218
|
+
const target = canonical(identifier);
|
|
1219
|
+
if (!target)
|
|
1220
|
+
return undefined;
|
|
1221
|
+
const requested = requestedTerms
|
|
1222
|
+
.map(canonical)
|
|
1223
|
+
.filter((term) => term.length > 2);
|
|
1224
|
+
const matched = requested.find((term) => term === target
|
|
1225
|
+
|| target.split(' ').includes(term)
|
|
1226
|
+
|| term.split(' ').includes(target));
|
|
1227
|
+
return matched ? identifier.split(/[.:]/).pop() : undefined;
|
|
1228
|
+
}
|
|
628
1229
|
function refusalCodeForValidation(code) {
|
|
629
1230
|
if (code === 'unknown_relation' || code === 'unknown_column' || code === 'insufficient_context' || code === 'missing_baseline') {
|
|
630
1231
|
return 'grounding_gap';
|
|
@@ -1024,7 +1625,9 @@ export async function answer(input) {
|
|
|
1024
1625
|
// Cross-result computations are intentionally independent of the KG and
|
|
1025
1626
|
// warehouse. Preserve this earliest exit before building any execution
|
|
1026
1627
|
// registry so lightweight follow-up callers do not need runtime services.
|
|
1027
|
-
const earlyCrossResult =
|
|
1628
|
+
const earlyCrossResult = normalizedInput.skipCrossResultComputation
|
|
1629
|
+
? null
|
|
1630
|
+
: tryCrossResultAnswer(normalizedInput);
|
|
1028
1631
|
if (earlyCrossResult) {
|
|
1029
1632
|
return {
|
|
1030
1633
|
...earlyCrossResult,
|
|
@@ -1161,9 +1764,17 @@ export async function answer(input) {
|
|
|
1161
1764
|
const analyticalExecutionGraph = analyticalGraphBuild?.status === 'ready'
|
|
1162
1765
|
? analyticalGraphBuild.graph
|
|
1163
1766
|
: undefined;
|
|
1767
|
+
// A v2 plan can also be synthesized for legacy/direct AnswerLoop callers so
|
|
1768
|
+
// they can opt into the deterministic compiler. That compatibility bridge is
|
|
1769
|
+
// not a router freeze: it must not turn a missing optional graph/adapter into
|
|
1770
|
+
// a terminal answer before the established, safe legacy semantic execution
|
|
1771
|
+
// path is considered. Router-selected semantic plans carry the explicit
|
|
1772
|
+
// `selectedCascadeTier` signal and remain immutable/terminal below.
|
|
1773
|
+
const routerFrozenSemanticPlan = Boolean(frozenSemanticRouteForInput(normalizedInput));
|
|
1164
1774
|
const analyticalExecutionGraphFailure = normalizedInput.resolvedAnalyticalPlan?.schemaVersion === 2
|
|
1165
1775
|
&& !multiMetricPlan
|
|
1166
1776
|
&& !generatedProposalPlan
|
|
1777
|
+
&& routerFrozenSemanticPlan
|
|
1167
1778
|
? analyticalGraphBuild?.status === 'blocked'
|
|
1168
1779
|
? {
|
|
1169
1780
|
code: analyticalGraphBuild.code,
|
|
@@ -1248,7 +1859,23 @@ export async function answer(input) {
|
|
|
1248
1859
|
}
|
|
1249
1860
|
: compiledInput;
|
|
1250
1861
|
const loopResult = await runAnswerLoop(executionInput);
|
|
1251
|
-
|
|
1862
|
+
let result = applyHollowAnswerGate(validateCertifiedAnalyticalGraphAnswer(loopResult, executionInput.analyticalExecutionGraph, executionInput.resolvedAnalyticalPlan?.analyticalFrame));
|
|
1863
|
+
const frozenSemanticRoute = frozenSemanticRouteForInput(executionInput);
|
|
1864
|
+
// A final defensive boundary keeps future return sites from accidentally
|
|
1865
|
+
// restoring the old semantic -> generated fallthrough. Do not relabel an
|
|
1866
|
+
// actual generated result as semantic: replace it with an inspectable,
|
|
1867
|
+
// same-tier terminal failure instead.
|
|
1868
|
+
const returnedNonSemanticRoute = frozenSemanticRoute && ((result.route !== undefined && result.route.tier !== 'semantic_metric')
|
|
1869
|
+
|| (result.route === undefined && result.kind !== 'no_answer' && result.sourceTier !== 'semantic_layer'));
|
|
1870
|
+
if (returnedNonSemanticRoute && frozenSemanticRoute) {
|
|
1871
|
+
result = frozenSemanticPlanFailureAnswer({
|
|
1872
|
+
answerInput: executionInput,
|
|
1873
|
+
considered: result.considered ?? [],
|
|
1874
|
+
providerName: executionInput.provider.name,
|
|
1875
|
+
code: 'EXECUTION_GRAPH_MISMATCH',
|
|
1876
|
+
reason: `The pinned semantic plan ${frozenSemanticRoute.ref ?? 'selected by the router'} returned a non-semantic answer route. DQL did not substitute generated SQL.`,
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1252
1879
|
// Attach the canonical trust label once, at the single exit point, so every
|
|
1253
1880
|
// return site inside runAnswerLoop stays untouched and backward compatible.
|
|
1254
1881
|
// Freshness-aware trust: for a certified answer, fold the source block's data
|
|
@@ -1256,7 +1883,7 @@ export async function answer(input) {
|
|
|
1256
1883
|
// stale data" / "Certified · upstream failed". Non-certified or fresh answers
|
|
1257
1884
|
// are unaffected.
|
|
1258
1885
|
const { _semanticMetricMatch, ...publicResult } = result;
|
|
1259
|
-
const chosenRoute = result.route ?? deriveAiRoute(result, _semanticMetricMatch);
|
|
1886
|
+
const chosenRoute = frozenSemanticRoute ?? result.route ?? deriveAiRoute(result, _semanticMetricMatch);
|
|
1260
1887
|
// P0 — record the high-level action this turn warranted, so callers can route
|
|
1261
1888
|
// (compose_app → app build, investigate → research) and the UI can show the
|
|
1262
1889
|
// agent's reasoning. Computed once at the single exit from the finished answer.
|
|
@@ -1390,6 +2017,69 @@ function deriveAiRoute(result, metricMatch) {
|
|
|
1390
2017
|
? { tier: 'generated_sql', label: 'Prepared review-required DQL artifact with SQL preview.' }
|
|
1391
2018
|
: { tier: 'generated_sql', label: 'Prepared review-required SQL preview.' };
|
|
1392
2019
|
}
|
|
2020
|
+
/**
|
|
2021
|
+
* The router owns the cascade decision. Once it freezes a semantic plan, the
|
|
2022
|
+
* answer loop may either execute that exact plan or return a semantic-tier
|
|
2023
|
+
* terminal failure. It must never let an unavailable semantic adapter fall
|
|
2024
|
+
* through to the legacy generated-SQL lane merely because the latter can still
|
|
2025
|
+
* construct a preview.
|
|
2026
|
+
*/
|
|
2027
|
+
function frozenSemanticRouteForInput(input) {
|
|
2028
|
+
const plan = input.resolvedAnalyticalPlan;
|
|
2029
|
+
// `freezeLegacySemanticSelection()` upgrades older direct AnswerLoop callers
|
|
2030
|
+
// to a typed plan so they can use the deterministic semantic compiler. That
|
|
2031
|
+
// migration is not the router's immutable cascade freeze. The server-owned
|
|
2032
|
+
// cascade explicitly carries `selectedCascadeTier: 'semantic'`; only that
|
|
2033
|
+
// signal activates the no-downgrade boundary below. Otherwise direct legacy
|
|
2034
|
+
// callers retain their established deterministic semantic behavior.
|
|
2035
|
+
if (input.selectedCascadeTier !== 'semantic'
|
|
2036
|
+
|| plan?.mode !== 'authoritative'
|
|
2037
|
+
|| plan.capability !== 'semantic_execution')
|
|
2038
|
+
return undefined;
|
|
2039
|
+
const executionId = plan.executionId ?? plan.selectedConceptIds[0];
|
|
2040
|
+
return {
|
|
2041
|
+
tier: 'semantic_metric',
|
|
2042
|
+
label: executionId
|
|
2043
|
+
? `Frozen semantic plan ${executionId}`
|
|
2044
|
+
: 'Frozen semantic plan',
|
|
2045
|
+
...(executionId ? { ref: executionId } : {}),
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
function frozenSemanticPlanFailureAnswer(input) {
|
|
2049
|
+
const { answerInput, considered, providerName, code, reason } = input;
|
|
2050
|
+
const route = frozenSemanticRouteForInput(answerInput);
|
|
2051
|
+
// Callers use this only for an authoritative semantic plan. Keep the
|
|
2052
|
+
// fallback defensive so an internal misuse still produces a safe no-answer.
|
|
2053
|
+
const analyticalFailure = analyticalFailureForInput(answerInput, {
|
|
2054
|
+
error: { code, message: reason },
|
|
2055
|
+
phase: 'compilation',
|
|
2056
|
+
failedBindings: answerInput.resolvedAnalyticalPlan?.executionId
|
|
2057
|
+
? [{
|
|
2058
|
+
qualifiedId: answerInput.resolvedAnalyticalPlan.executionId,
|
|
2059
|
+
role: 'metric',
|
|
2060
|
+
reasonCode: code,
|
|
2061
|
+
}]
|
|
2062
|
+
: undefined,
|
|
2063
|
+
});
|
|
2064
|
+
return {
|
|
2065
|
+
kind: 'no_answer',
|
|
2066
|
+
sourceTier: 'no_answer',
|
|
2067
|
+
certification: 'analyst_review_required',
|
|
2068
|
+
reviewStatus: 'none',
|
|
2069
|
+
confidence: 0,
|
|
2070
|
+
text: analyticalFailure.message,
|
|
2071
|
+
answer: analyticalFailure.message,
|
|
2072
|
+
executionError: analyticalFailure.message,
|
|
2073
|
+
refusalCode: 'grounding_gap',
|
|
2074
|
+
refusalDetails: { code, message: analyticalFailure.message },
|
|
2075
|
+
analyticalFailure,
|
|
2076
|
+
...(route ? { route } : {}),
|
|
2077
|
+
citations: contextPackCitations(answerInput.contextPack, 8),
|
|
2078
|
+
considered,
|
|
2079
|
+
contextPack: answerInput.contextPack,
|
|
2080
|
+
providerUsed: providerName,
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
1393
2083
|
/** "customers_customer_name" → "customers" for a question the user has to read. */
|
|
1394
2084
|
function humanizeDeicticDimension(dimension) {
|
|
1395
2085
|
const leaf = dimension.split(/[.:]/).pop() ?? dimension;
|
|
@@ -1419,6 +2109,7 @@ async function runAnswerLoop(input) {
|
|
|
1419
2109
|
// or provider is dispatched. Full retrieval stays on `input.contextPack`
|
|
1420
2110
|
// for receipts and diagnostics only.
|
|
1421
2111
|
const forcedExploratoryTier = input.selectedCascadeTier === 'exploratory_sql';
|
|
2112
|
+
const frozenSemanticRoute = frozenSemanticRouteForInput(input);
|
|
1422
2113
|
const exploratoryClosureContextPack = forcedExploratoryTier
|
|
1423
2114
|
? scopeContextPackToExploratoryCandidateClosure(scopedContextPack, input.exploratoryCandidateIds)
|
|
1424
2115
|
: undefined;
|
|
@@ -1510,6 +2201,31 @@ async function runAnswerLoop(input) {
|
|
|
1510
2201
|
const authoritativePlanBinding = !forcedExploratoryTier && input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1511
2202
|
? input.resolvedPlanExecutionBinding
|
|
1512
2203
|
: undefined;
|
|
2204
|
+
// `freezeLegacySemanticSelection()` attaches an authoritative-shaped v2
|
|
2205
|
+
// plan for direct AnswerLoop compatibility. It is not the router-owned
|
|
2206
|
+
// semantic cascade decision. Only an explicit selected semantic tier may
|
|
2207
|
+
// make an adapter/graph miss terminal; direct callers retain the established
|
|
2208
|
+
// deterministic metric path, which still applies the ordinary SQL safety
|
|
2209
|
+
// validation before it can execute.
|
|
2210
|
+
const legacyDirectSemanticPlan = input.selectedCascadeTier === undefined
|
|
2211
|
+
&& input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
2212
|
+
&& input.resolvedAnalyticalPlan.recommendedRoute === 'semantic'
|
|
2213
|
+
&& !frozenSemanticRoute;
|
|
2214
|
+
// A semantic freeze is an immutable execution authority, not a hint for the
|
|
2215
|
+
// legacy loop. In particular, projects that have indexed semantic metadata
|
|
2216
|
+
// but have not configured the corresponding local semantic adapter used to
|
|
2217
|
+
// continue into generated SQL. Stop here with the selected plan's typed
|
|
2218
|
+
// diagnostic instead; no provider, tool, or SQL work is allowed to replace
|
|
2219
|
+
// it after freeze.
|
|
2220
|
+
if (!forcedExploratoryTier && frozenSemanticRoute && !input.semanticLayer) {
|
|
2221
|
+
return frozenSemanticPlanFailureAnswer({
|
|
2222
|
+
answerInput: input,
|
|
2223
|
+
considered,
|
|
2224
|
+
providerName: provider.name,
|
|
2225
|
+
code: 'SEMANTIC_LAYER_REQUIRED',
|
|
2226
|
+
reason: `The pinned semantic adapter for ${frozenSemanticRoute.ref ?? 'the selected semantic plan'} is unavailable.`,
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
1513
2229
|
if (!forcedExploratoryTier && input.analyticalPeriodResolutionFailure) {
|
|
1514
2230
|
const failure = input.analyticalPeriodResolutionFailure;
|
|
1515
2231
|
const structuredFailureCode = failure.error && typeof failure.error === 'object'
|
|
@@ -1601,7 +2317,11 @@ async function runAnswerLoop(input) {
|
|
|
1601
2317
|
input,
|
|
1602
2318
|
binding: input.semanticGraphExecutionBinding,
|
|
1603
2319
|
graph: input.analyticalExecutionGraph,
|
|
1604
|
-
|
|
2320
|
+
// The execution registry identifies the adapter metric, but its compact
|
|
2321
|
+
// node payload may omit the relationship-path metadata that was frozen
|
|
2322
|
+
// on the RAP. Re-deriving aggregation proof from that projection makes
|
|
2323
|
+
// an already accepted native MetricFlow grouping fail post-freeze.
|
|
2324
|
+
capability: input.semanticGraphExecutionBinding.capability,
|
|
1605
2325
|
considered,
|
|
1606
2326
|
providerName: provider.name,
|
|
1607
2327
|
});
|
|
@@ -1716,7 +2436,7 @@ async function runAnswerLoop(input) {
|
|
|
1716
2436
|
providerUsed: provider.name,
|
|
1717
2437
|
};
|
|
1718
2438
|
}
|
|
1719
|
-
if (authoritativePlanBinding?.status === 'blocked') {
|
|
2439
|
+
if (authoritativePlanBinding?.status === 'blocked' && !legacyDirectSemanticPlan) {
|
|
1720
2440
|
// An ambiguous singular reference is answerable the moment the user says
|
|
1721
2441
|
// which one they meant, so offer the candidates rather than dead-ending.
|
|
1722
2442
|
// The old refusal shipped the binder's internal sentence as the answer
|
|
@@ -1792,7 +2512,7 @@ async function runAnswerLoop(input) {
|
|
|
1792
2512
|
? authoritativePlanBinding
|
|
1793
2513
|
: undefined;
|
|
1794
2514
|
const preferredSemanticMetric = authoritativeSemanticBinding?.metricNode
|
|
1795
|
-
?? (input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
2515
|
+
?? (input.resolvedAnalyticalPlan?.mode === 'authoritative' && !legacyDirectSemanticPlan
|
|
1796
2516
|
? undefined
|
|
1797
2517
|
: resolvePreferredSemanticMetric([input.preferredExecutionId, ...(input.preferredEvidenceIds ?? [])], semanticMetricNodes, kg));
|
|
1798
2518
|
const semanticLayerForExec = input.semanticLayer;
|
|
@@ -1802,7 +2522,7 @@ async function runAnswerLoop(input) {
|
|
|
1802
2522
|
? null
|
|
1803
2523
|
: preferredSemanticMetric
|
|
1804
2524
|
? { metric: preferredSemanticMetric, score: 1, basis: 'name' }
|
|
1805
|
-
: input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
2525
|
+
: input.resolvedAnalyticalPlan?.mode === 'authoritative' && !legacyDirectSemanticPlan
|
|
1806
2526
|
? null
|
|
1807
2527
|
: await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
|
|
1808
2528
|
measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
|
|
@@ -1981,18 +2701,14 @@ async function runAnswerLoop(input) {
|
|
|
1981
2701
|
if (artifactHit && businessContextTerminal) {
|
|
1982
2702
|
let result;
|
|
1983
2703
|
let executionError;
|
|
2704
|
+
let executionFailureDetail;
|
|
1984
2705
|
if (artifactHit.node.kind === 'block' && input.executeCertifiedBlock) {
|
|
1985
2706
|
try {
|
|
1986
|
-
result = await input.executeCertifiedBlock(artifactHit.node,
|
|
1987
|
-
|
|
1988
|
-
...certifiedInvocationInputs(artifactHit.node, questionPlan),
|
|
1989
|
-
rowLimit: questionPlan.requestedShape.topN?.scope === 'per_group'
|
|
1990
|
-
? undefined
|
|
1991
|
-
: questionPlan.requestedShape.topN?.n,
|
|
1992
|
-
});
|
|
1993
|
-
result = trimResultToRequestedTopN(result, questionPlan);
|
|
2707
|
+
result = await input.executeCertifiedBlock(artifactHit.node, buildCertifiedBlockInvocationInput(artifactHit.node, questionPlan, question));
|
|
2708
|
+
result = trimCertifiedBlockResultToRequestedTopN(result, questionPlan);
|
|
1994
2709
|
}
|
|
1995
2710
|
catch (err) {
|
|
2711
|
+
executionFailureDetail = analyticalErrorDetail(err);
|
|
1996
2712
|
executionError = err instanceof Error ? err.message : String(err);
|
|
1997
2713
|
}
|
|
1998
2714
|
}
|
|
@@ -2098,6 +2814,9 @@ async function runAnswerLoop(input) {
|
|
|
2098
2814
|
? undefined
|
|
2099
2815
|
: questionPlan.requestedShape.topN?.n);
|
|
2100
2816
|
const authoritativeCertifiedFailure = Boolean(authoritativeCertifiedBinding && executionError);
|
|
2817
|
+
const observabilityExecutionFailure = authoritativeCertifiedFailure
|
|
2818
|
+
? observabilityFailureForFrozenExecution(executionFailureDetail)
|
|
2819
|
+
: undefined;
|
|
2101
2820
|
// A selected certified block which returns an incomplete tuple is a
|
|
2102
2821
|
// same-tier terminal failure. It is not a reason to reinterpret meaning,
|
|
2103
2822
|
// label the partial rows generated, or retry another route after freeze.
|
|
@@ -2143,6 +2862,7 @@ async function runAnswerLoop(input) {
|
|
|
2143
2862
|
block: artifactHit.node.kind === 'block' ? artifactHit.node : undefined,
|
|
2144
2863
|
result,
|
|
2145
2864
|
executionError,
|
|
2865
|
+
...(observabilityExecutionFailure ? { observabilityExecutionFailure } : {}),
|
|
2146
2866
|
...(authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
|
|
2147
2867
|
? { refusalCode: 'grounding_gap' }
|
|
2148
2868
|
: {}),
|
|
@@ -2637,8 +3357,21 @@ async function runAnswerLoop(input) {
|
|
|
2637
3357
|
contextPack: input.contextPack,
|
|
2638
3358
|
considered,
|
|
2639
3359
|
providerUsed: provider.name,
|
|
3360
|
+
...(frozenSemanticRoute ? { route: frozenSemanticRoute } : {}),
|
|
2640
3361
|
};
|
|
2641
3362
|
}
|
|
3363
|
+
// The semantic branch above has exhausted the exact plan's compiler and
|
|
3364
|
+
// adapter choices. Do not let `metricFirst` or the ordinary generated
|
|
3365
|
+
// proposal branch reinterpret the question after a semantic freeze.
|
|
3366
|
+
if (frozenSemanticRoute && !semanticBridgeAnswer) {
|
|
3367
|
+
return frozenSemanticPlanFailureAnswer({
|
|
3368
|
+
answerInput: input,
|
|
3369
|
+
considered,
|
|
3370
|
+
providerName: provider.name,
|
|
3371
|
+
code: 'COMPILATION_FAILED',
|
|
3372
|
+
reason: `The pinned semantic adapter could not compose ${frozenSemanticRoute.ref ?? 'the selected semantic plan'} without changing the frozen plan.`,
|
|
3373
|
+
});
|
|
3374
|
+
}
|
|
2642
3375
|
const metricFirst = semanticMetricMatch
|
|
2643
3376
|
? buildGovernedMetricFirstSql({
|
|
2644
3377
|
metric: semanticMetricMatch.metric,
|
|
@@ -2841,9 +3574,15 @@ async function runAnswerLoop(input) {
|
|
|
2841
3574
|
confidence: 0,
|
|
2842
3575
|
text,
|
|
2843
3576
|
answer: text,
|
|
2844
|
-
|
|
3577
|
+
// DQL stopping itself is not the provider failing. Reporting both as
|
|
3578
|
+
// `provider_error` put "The AI provider did not respond" above a body
|
|
3579
|
+
// that said the opposite, and sent the reader to check provider health
|
|
3580
|
+
// for a run that ended on an internal ceiling.
|
|
3581
|
+
refusalCode: admissionDeclined || orchestrationBudget ? 'orchestration_budget_exhausted' : 'provider_error',
|
|
2845
3582
|
refusalDetails: {
|
|
2846
|
-
code:
|
|
3583
|
+
code: admissionDeclined
|
|
3584
|
+
? 'RUN_DEADLINE_INSUFFICIENT'
|
|
3585
|
+
: orchestrationBudget ? 'orchestration_budget_exhausted' : 'provider_error',
|
|
2847
3586
|
message: orchestrationBudget ? `${text} (${err.message})` : text,
|
|
2848
3587
|
},
|
|
2849
3588
|
citations: [],
|
|
@@ -2970,14 +3709,36 @@ async function runAnswerLoop(input) {
|
|
|
2970
3709
|
|| (input.contextPack?.allowedSqlContext?.relations.length ?? 0) > 0
|
|
2971
3710
|
|| (input.contextPack?.allowedSqlContext?.sourceBlockSql.length ?? 0) > 0
|
|
2972
3711
|
|| contextBlocks.length > 0;
|
|
3712
|
+
// A selected bounded-exploration RAP is stronger than the advisory legacy
|
|
3713
|
+
// analytical-path diagnosis. The latter may still call raw output terms a
|
|
3714
|
+
// semantic "missing dimension" even after the router has proven one safe
|
|
3715
|
+
// physical relation and frozen the exploratory tuple. Do not let that
|
|
3716
|
+
// advisory pre-freeze diagnosis suppress the one explicitly authorized
|
|
3717
|
+
// same-plan model-decline repair; the host re-validates the exact proposal
|
|
3718
|
+
// against the frozen closure before it can execute.
|
|
3719
|
+
const frozenExploratoryRepair = frozenExploratoryModelRepairAuthority(input);
|
|
2973
3720
|
if (!parsed.sql && !governedMetricAnswer && wantsGeneratedData && hasGeneratableContext
|
|
2974
|
-
&& (
|
|
3721
|
+
&& (frozenExploratoryRepair
|
|
3722
|
+
|| analyticalPlan?.safe !== false
|
|
3723
|
+
|| analyticalPlanAllowsDbtExploration(analyticalPlan))) {
|
|
2975
3724
|
try {
|
|
2976
3725
|
proposed = await generateProposalWithOptionalTools({
|
|
2977
3726
|
provider,
|
|
2978
|
-
messages: [...messages, {
|
|
2979
|
-
|
|
2980
|
-
|
|
3727
|
+
messages: [...messages, {
|
|
3728
|
+
role: 'system',
|
|
3729
|
+
content: [
|
|
3730
|
+
FORCE_JOIN_INSTRUCTION,
|
|
3731
|
+
...(frozenExploratoryRepair
|
|
3732
|
+
? [renderFrozenExploratoryRepairContract(input.resolvedAnalyticalPlan, questionPlan)]
|
|
3733
|
+
: []),
|
|
3734
|
+
].join('\n\n'),
|
|
3735
|
+
}],
|
|
3736
|
+
// A router-frozen exploratory plan may consume exactly one additional
|
|
3737
|
+
// provider transport to correct a model decline. The host carries the
|
|
3738
|
+
// immutable plan and output tuple into this retry; it is neither a
|
|
3739
|
+
// replan nor a route change. Legacy direct callers retain their
|
|
3740
|
+
// historical composition correction, but do not receive the typed
|
|
3741
|
+
// repair authority below.
|
|
2981
3742
|
tools: [],
|
|
2982
3743
|
questionPlan,
|
|
2983
3744
|
intent,
|
|
@@ -2986,6 +3747,13 @@ async function runAnswerLoop(input) {
|
|
|
2986
3747
|
analysisDepth: input.analysisDepth,
|
|
2987
3748
|
toolCalls: proposalToolCalls,
|
|
2988
3749
|
providerPayloadGuard: input.providerPayloadGuard,
|
|
3750
|
+
...(frozenExploratoryRepair
|
|
3751
|
+
? {
|
|
3752
|
+
dispatchPhase: 'repair',
|
|
3753
|
+
egressPurpose: 'repair_sql',
|
|
3754
|
+
maxProviderDispatches: 1,
|
|
3755
|
+
}
|
|
3756
|
+
: {}),
|
|
2989
3757
|
});
|
|
2990
3758
|
parsed = parseProposal(proposed);
|
|
2991
3759
|
}
|
|
@@ -3060,6 +3828,49 @@ async function runAnswerLoop(input) {
|
|
|
3060
3828
|
}
|
|
3061
3829
|
if (parsed.sql) {
|
|
3062
3830
|
const frozenPlan = input.resolvedAnalyticalPlan;
|
|
3831
|
+
// The result contract is host-owned even in tests or embedded callers
|
|
3832
|
+
// that provide their own execution adapter. Stop before capability
|
|
3833
|
+
// preparation when a generated alias is backed by a different physical
|
|
3834
|
+
// column than the frozen plan (for example `product_id AS order_id`).
|
|
3835
|
+
// The local runtime repeats this check while minting the capability and
|
|
3836
|
+
// persists the matching proof on its receipt.
|
|
3837
|
+
if (input.selectedCascadeTier === 'exploratory_sql' && frozenPlan) {
|
|
3838
|
+
const requiredOutputValidation = validateFrozenRequiredOutputProjection({
|
|
3839
|
+
plan: frozenPlan,
|
|
3840
|
+
sql: parsed.sql,
|
|
3841
|
+
...(input.semanticDriver ? { dialect: input.semanticDriver } : {}),
|
|
3842
|
+
});
|
|
3843
|
+
if (!requiredOutputValidation.ok) {
|
|
3844
|
+
const failedOutputs = [...new Set([
|
|
3845
|
+
...requiredOutputValidation.missingOutputs,
|
|
3846
|
+
...requiredOutputValidation.bindingMismatches,
|
|
3847
|
+
])];
|
|
3848
|
+
const message = `The generated exploratory query did not prove the frozen source binding for required output ${failedOutputs.join(', ')}, so DQL did not execute it.`;
|
|
3849
|
+
return {
|
|
3850
|
+
kind: 'no_answer',
|
|
3851
|
+
sourceTier: 'no_answer',
|
|
3852
|
+
certification: 'analyst_review_required',
|
|
3853
|
+
reviewStatus: 'none',
|
|
3854
|
+
confidence: 0,
|
|
3855
|
+
text: message,
|
|
3856
|
+
answer: message,
|
|
3857
|
+
refusalCode: 'policy_blocked',
|
|
3858
|
+
refusalDetails: {
|
|
3859
|
+
code: 'OUTPUT_BINDING_TUPLE_DRIFT',
|
|
3860
|
+
message,
|
|
3861
|
+
},
|
|
3862
|
+
validationWarnings: [
|
|
3863
|
+
'OUTPUT_BINDING_TUPLE_DRIFT',
|
|
3864
|
+
...failedOutputs.map((output) => `Frozen output binding not proven: ${output}`),
|
|
3865
|
+
],
|
|
3866
|
+
citations: contextPackCitations(input.contextPack, 8),
|
|
3867
|
+
memoryContext: input.memoryContext,
|
|
3868
|
+
contextPack: input.contextPack,
|
|
3869
|
+
considered,
|
|
3870
|
+
providerUsed: provider.name,
|
|
3871
|
+
};
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3063
3874
|
if (!governedMetricAnswer && frozenPlan?.mode === 'authoritative' && frozenPlan.schemaVersion === 2) {
|
|
3064
3875
|
const targetFingerprint = input.generatedProposalTargetFingerprint ?? '';
|
|
3065
3876
|
const validation = validateGeneratedAnalyticalProposal({
|
|
@@ -3499,7 +4310,13 @@ async function runAnswerLoop(input) {
|
|
|
3499
4310
|
// Business-language chat text; the validator's machine message (with
|
|
3500
4311
|
// relation ids and tool guidance for the repair prompt) stays in
|
|
3501
4312
|
// refusalDetails + validationWarnings for the Inspect surface.
|
|
3502
|
-
|
|
4313
|
+
// A dimension the user explicitly asked for that the business has not
|
|
4314
|
+
// modeled is a modeling gap, not a retrieval gap. Say so, and say what
|
|
4315
|
+
// would let the question be answered.
|
|
4316
|
+
const unmodeledRequestedDimension = requestedDimensionModelingGap(contextValidation.code, contextValidation.offending, [...questionPlan.dimensionTerms, ...(input.followUp?.dimensions ?? [])]);
|
|
4317
|
+
const text = unmodeledRequestedDimension
|
|
4318
|
+
? `"${unmodeledRequestedDimension}" is not modeled. Which governed dimension should I use instead?`
|
|
4319
|
+
: renderContextValidationRefusalForUser(contextValidation.code, contextValidation.error, input.followUp?.memberBindings, contextValidation.aggregationSafetyProof?.issueCodes);
|
|
3503
4320
|
const analysisPlan = buildAnalysisPlan({
|
|
3504
4321
|
question,
|
|
3505
4322
|
intent,
|
|
@@ -3521,7 +4338,9 @@ async function runAnswerLoop(input) {
|
|
|
3521
4338
|
reviewStatus: 'none',
|
|
3522
4339
|
confidence: 0.15,
|
|
3523
4340
|
text,
|
|
3524
|
-
refusalCode:
|
|
4341
|
+
refusalCode: unmodeledRequestedDimension
|
|
4342
|
+
? 'modeling_gap'
|
|
4343
|
+
: refusalCodeForValidation(contextValidation.code),
|
|
3525
4344
|
refusalDetails: {
|
|
3526
4345
|
code: contextValidation.code ?? 'insufficient_context',
|
|
3527
4346
|
message: contextValidation.error,
|
|
@@ -3664,12 +4483,14 @@ async function runAnswerLoop(input) {
|
|
|
3664
4483
|
let executionError;
|
|
3665
4484
|
let warehouseFailure;
|
|
3666
4485
|
let exploratoryClosureDenied = false;
|
|
4486
|
+
let semanticFanoutProbeFailure;
|
|
3667
4487
|
let repairAttempts = 0;
|
|
3668
4488
|
// A forced proposal may originate from the bounded analyst loop, but it
|
|
3669
4489
|
// cannot bypass a router-owned exploratory decision. In that case the host
|
|
3670
4490
|
// still prepares and freezes the exact validated SQL before execution.
|
|
3671
4491
|
const exploratoryExecutionSelected = input.selectedCascadeTier === 'exploratory_sql';
|
|
3672
|
-
let
|
|
4492
|
+
let initialExploratoryExecution;
|
|
4493
|
+
let exploratoryRepairExecution;
|
|
3673
4494
|
// Repair candidates that were generated but rejected before execution. These
|
|
3674
4495
|
// are diagnostics ABOUT the recovery attempt, not the reason the run failed,
|
|
3675
4496
|
// so they are reported alongside the original error rather than replacing it.
|
|
@@ -3684,7 +4505,7 @@ async function runAnswerLoop(input) {
|
|
|
3684
4505
|
executionError = deepCandidateExecutionError;
|
|
3685
4506
|
warehouseFailure = normalizeWarehouseSqlFailure(deepCandidateExecutionError, input.semanticDriver);
|
|
3686
4507
|
}
|
|
3687
|
-
const executeCurrentSql = async () => {
|
|
4508
|
+
const executeCurrentSql = async (authorizationAttempt) => {
|
|
3688
4509
|
const requestedLimit = questionPlan.requestedShape.topN?.scope === 'per_group'
|
|
3689
4510
|
? 200
|
|
3690
4511
|
: questionPlan.requestedShape.topN?.n ?? 200;
|
|
@@ -3725,17 +4546,23 @@ async function runAnswerLoop(input) {
|
|
|
3725
4546
|
});
|
|
3726
4547
|
}
|
|
3727
4548
|
// This is the only point at which a router-selected exploratory proposal
|
|
3728
|
-
// crosses from validated text into executable authority. The
|
|
3729
|
-
//
|
|
3730
|
-
//
|
|
3731
|
-
|
|
3732
|
-
|
|
4549
|
+
// crosses from validated text into executable authority. The first call
|
|
4550
|
+
// receives the original one-shot capability. A retryable warehouse
|
|
4551
|
+
// failure may mint exactly one *new* repair capability, explicitly bound
|
|
4552
|
+
// to the first SQL fingerprint; it never reopens meaning or routing.
|
|
4553
|
+
const prepared = authorizationAttempt
|
|
4554
|
+
? await input.prepareExploratorySqlExecution(parsed.sql, boundedArtifact, authorizationAttempt)
|
|
4555
|
+
: (initialExploratoryExecution ??= await input.prepareExploratorySqlExecution(parsed.sql, boundedArtifact));
|
|
4556
|
+
if (authorizationAttempt)
|
|
4557
|
+
exploratoryRepairExecution = prepared;
|
|
4558
|
+
return input.executeAgenticGeneratedSql(prepared.capability, parsed.sql, boundedArtifact);
|
|
3733
4559
|
}
|
|
3734
4560
|
if (!input.executeGeneratedSql)
|
|
3735
4561
|
throw new Error('No generated SQL executor is configured.');
|
|
3736
4562
|
return input.executeGeneratedSql(parsed.sql, boundedArtifact);
|
|
3737
4563
|
};
|
|
3738
4564
|
if ((input.executeGeneratedSql
|
|
4565
|
+
|| input.executeDqlArtifact
|
|
3739
4566
|
|| (input.forcedGeneratedProposal && input.executeAgenticGeneratedSql)
|
|
3740
4567
|
|| exploratoryExecutionSelected) && !result) {
|
|
3741
4568
|
// Fanout gate for native semantic direct-joins: a duplicate join-key row on
|
|
@@ -3744,17 +4571,31 @@ async function runAnswerLoop(input) {
|
|
|
3744
4571
|
// answers). Probe first; on structural contradiction, refuse with the cause
|
|
3745
4572
|
// instead of presenting wrong numbers — and never hand the error to SQL
|
|
3746
4573
|
// repair, which would regenerate the same multiplying join.
|
|
3747
|
-
let
|
|
4574
|
+
let semanticFanoutBlocked = false;
|
|
3748
4575
|
if (!executionError
|
|
3749
4576
|
&& !input.forcedGeneratedProposal
|
|
3750
4577
|
&& !exploratoryExecutionSelected
|
|
3751
|
-
&& input.executeGeneratedSql
|
|
3752
4578
|
&& semanticBridgeAnswer?.composeResult?.fanoutProbeSql
|
|
3753
4579
|
&& semanticBridgeAnswer.sql.trim() === parsed.sql?.trim()) {
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
4580
|
+
// A DQL-artifact executor owns the semantic artifact, not an arbitrary
|
|
4581
|
+
// one-row structural SQL probe. If the host did not also provide the
|
|
4582
|
+
// read-only SQL executor needed for that probe, native direct joins fail
|
|
4583
|
+
// closed rather than treating an artifact handoff as proof of uniqueness.
|
|
4584
|
+
const fanoutProbe = input.executeGeneratedSql
|
|
4585
|
+
? await probeSemanticJoinFanout(semanticBridgeAnswer.composeResult.fanoutProbeSql, semanticBridgeAnswer.composeResult.tables, input.executeGeneratedSql)
|
|
4586
|
+
: {
|
|
4587
|
+
status: 'blocked',
|
|
4588
|
+
code: 'SEMANTIC_FANOUT_PROBE_ERROR',
|
|
4589
|
+
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.',
|
|
4590
|
+
};
|
|
4591
|
+
if (fanoutProbe.status === 'blocked') {
|
|
4592
|
+
// A governed semantic route freezes before execution. If DQL cannot
|
|
4593
|
+
// prove its native join is fanout-safe, it must fail closed rather than
|
|
4594
|
+
// execute, label the result governed, or quietly repair into a
|
|
4595
|
+
// different plan.
|
|
4596
|
+
executionError = fanoutProbe.message;
|
|
4597
|
+
semanticFanoutProbeFailure = fanoutProbe;
|
|
4598
|
+
semanticFanoutBlocked = true;
|
|
3758
4599
|
}
|
|
3759
4600
|
}
|
|
3760
4601
|
try {
|
|
@@ -3776,9 +4617,15 @@ async function runAnswerLoop(input) {
|
|
|
3776
4617
|
executionError = warehouseFailure.redactedMessage;
|
|
3777
4618
|
}
|
|
3778
4619
|
if (executionError
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
4620
|
+
// A host-supplied SQL proposal normally bypasses repair. A router-owned
|
|
4621
|
+
// exploratory proposal is the exception: its frozen plan may authorize
|
|
4622
|
+
// one corrected SQL statement after a retryable warehouse failure.
|
|
4623
|
+
&& (!input.forcedGeneratedProposal || exploratoryExecutionSelected)
|
|
4624
|
+
// A router-selected exploratory plan may receive one bounded
|
|
4625
|
+
// same-plan repair. The repair still goes through a fresh host
|
|
4626
|
+
// authorization; it does not reuse the consumed initial capability.
|
|
4627
|
+
&& (!exploratoryExecutionSelected || Boolean(initialExploratoryExecution))
|
|
4628
|
+
&& !semanticFanoutBlocked
|
|
3782
4629
|
&& !authoritativeSemanticBinding) {
|
|
3783
4630
|
warehouseFailure ??= normalizeWarehouseSqlFailure(executionError, input.semanticDriver);
|
|
3784
4631
|
if (isRetryableGeneratedSqlError(warehouseFailure)) {
|
|
@@ -3804,7 +4651,13 @@ async function runAnswerLoop(input) {
|
|
|
3804
4651
|
],
|
|
3805
4652
|
};
|
|
3806
4653
|
try {
|
|
3807
|
-
result = await executeCurrentSql(
|
|
4654
|
+
result = await executeCurrentSql(exploratoryExecutionSelected
|
|
4655
|
+
? {
|
|
4656
|
+
version: 1,
|
|
4657
|
+
index: 1,
|
|
4658
|
+
parentSqlFingerprint: initialExploratoryExecution.freeze.sqlFingerprint,
|
|
4659
|
+
}
|
|
4660
|
+
: undefined);
|
|
3808
4661
|
executionError = undefined;
|
|
3809
4662
|
warehouseFailure = undefined;
|
|
3810
4663
|
}
|
|
@@ -3836,6 +4689,18 @@ async function runAnswerLoop(input) {
|
|
|
3836
4689
|
schemaContext,
|
|
3837
4690
|
signal: input.signal,
|
|
3838
4691
|
reasoningEffort: input.reasoningEffort,
|
|
4692
|
+
// Only the router-frozen exploratory lane has authority to spend
|
|
4693
|
+
// the exceptional post-warehouse repair transport. Legacy callers
|
|
4694
|
+
// keep their prior unlabelled repair behavior.
|
|
4695
|
+
...(exploratoryExecutionSelected
|
|
4696
|
+
&& Boolean(initialExploratoryExecution)
|
|
4697
|
+
&& frozenExploratoryModelRepairAuthority(input)
|
|
4698
|
+
? {
|
|
4699
|
+
dispatchPhase: 'repair',
|
|
4700
|
+
egressPurpose: 'repair_sql',
|
|
4701
|
+
maxProviderDispatches: 1,
|
|
4702
|
+
}
|
|
4703
|
+
: {}),
|
|
3839
4704
|
});
|
|
3840
4705
|
const repaired = parseProposal(repairedRaw);
|
|
3841
4706
|
if (repaired.sql) {
|
|
@@ -3854,7 +4719,12 @@ async function runAnswerLoop(input) {
|
|
|
3854
4719
|
repairNarrative = repaired.text?.trim() || undefined;
|
|
3855
4720
|
parsed.sql = repaired.sql;
|
|
3856
4721
|
parsed.viz = repaired.viz ?? parsed.viz;
|
|
3857
|
-
|
|
4722
|
+
// A repair fixes SQL bytes only. On the router-owned
|
|
4723
|
+
// exploratory lane, model prose or metadata must never mutate
|
|
4724
|
+
// the host-selected frame, route, outputs, or trust state after
|
|
4725
|
+
// the plan froze.
|
|
4726
|
+
if (!exploratoryExecutionSelected)
|
|
4727
|
+
applyParsedProposalMetadata(parsed, repaired);
|
|
3858
4728
|
contextValidation = {
|
|
3859
4729
|
ok: true,
|
|
3860
4730
|
warnings: [
|
|
@@ -3863,7 +4733,13 @@ async function runAnswerLoop(input) {
|
|
|
3863
4733
|
],
|
|
3864
4734
|
};
|
|
3865
4735
|
try {
|
|
3866
|
-
result = await executeCurrentSql(
|
|
4736
|
+
result = await executeCurrentSql(exploratoryExecutionSelected
|
|
4737
|
+
? {
|
|
4738
|
+
version: 1,
|
|
4739
|
+
index: 1,
|
|
4740
|
+
parentSqlFingerprint: initialExploratoryExecution.freeze.sqlFingerprint,
|
|
4741
|
+
}
|
|
4742
|
+
: undefined);
|
|
3867
4743
|
executionError = undefined;
|
|
3868
4744
|
warehouseFailure = undefined;
|
|
3869
4745
|
}
|
|
@@ -3893,12 +4769,14 @@ async function runAnswerLoop(input) {
|
|
|
3893
4769
|
// A router-selected physical-closure denial happens before a capability
|
|
3894
4770
|
// exists. Its rejected bytes are diagnostics for the host, never a SQL
|
|
3895
4771
|
// preview that a user can review, copy, or turn into a draft.
|
|
3896
|
-
sql: exploratoryClosureDenied ? undefined : parsed.sql,
|
|
4772
|
+
sql: exploratoryClosureDenied || semanticFanoutProbeFailure ? undefined : parsed.sql,
|
|
3897
4773
|
suggestedViz: parsed.viz ?? 'table',
|
|
3898
4774
|
assumptions: [
|
|
3899
4775
|
...(exploratoryClosureDenied
|
|
3900
4776
|
? ['The router-selected physical closure rejected the generated SQL before capability minting.']
|
|
3901
|
-
:
|
|
4777
|
+
: semanticFanoutProbeFailure
|
|
4778
|
+
? ['The native semantic join could not be proven fanout-safe before plan freeze, so DQL did not execute or expose its SQL.']
|
|
4779
|
+
: ['The SQL preview is uncertified until an analyst reviews and promotes the DQL artifact.']),
|
|
3902
4780
|
...(certifiedExecutionFallback
|
|
3903
4781
|
? [`Certified block ${certifiedExecutionFallback.node.name} failed execution and was bypassed: ${certifiedExecutionFallback.error}`]
|
|
3904
4782
|
: []),
|
|
@@ -3911,17 +4789,34 @@ async function runAnswerLoop(input) {
|
|
|
3911
4789
|
// Global top-N asks must return exactly N rows even when the generated SQL
|
|
3912
4790
|
// returned more (missing/oversized LIMIT) — mirrors the certified path so a
|
|
3913
4791
|
// "top 10" question never shows 200 rows. per_group scope is left intact by
|
|
3914
|
-
//
|
|
4792
|
+
// trimCertifiedBlockResultToRequestedTopN. Domain-agnostic.
|
|
3915
4793
|
let topNTrimNote;
|
|
3916
4794
|
if (result) {
|
|
3917
4795
|
const beforeRows = Array.isArray(result.rows) ? result.rows.length : result.rowCount;
|
|
3918
|
-
result =
|
|
4796
|
+
result = trimCertifiedBlockResultToRequestedTopN(result, questionPlan);
|
|
3919
4797
|
const afterRows = Array.isArray(result.rows) ? result.rows.length : result.rowCount;
|
|
3920
4798
|
if (afterRows < beforeRows) {
|
|
3921
4799
|
topNTrimNote = `Showed the top ${questionPlan.requestedShape.topN?.n ?? afterRows} of ${beforeRows} rows the query returned.`;
|
|
3922
4800
|
}
|
|
3923
4801
|
}
|
|
3924
4802
|
const resultShape = result ? validateAnswerResultShape(questionPlan, result) : undefined;
|
|
4803
|
+
// A router-selected exploratory plan carries a host-bound output
|
|
4804
|
+
// projection. The ordinary generated-answer UX may show a partial result
|
|
4805
|
+
// with a warning, but a frozen exploratory plan may not: displaying it
|
|
4806
|
+
// would silently change the tuple DQL authorized. Keep the connector rows
|
|
4807
|
+
// out of the answer/artifact when a broken executor returns them anyway.
|
|
4808
|
+
const missingFrozenExploratoryOutputs = exploratoryExecutionSelected && result
|
|
4809
|
+
? missingFrozenRequiredOutputProjection(input.resolvedAnalyticalPlan, result)
|
|
4810
|
+
: [];
|
|
4811
|
+
const activeExploratoryOutputFreeze = exploratoryRepairExecution?.freeze ?? initialExploratoryExecution?.freeze;
|
|
4812
|
+
const missingFrozenExploratoryBindingProofs = exploratoryExecutionSelected && result
|
|
4813
|
+
? missingFrozenRequiredOutputBindingProof(input.resolvedAnalyticalPlan, activeExploratoryOutputFreeze)
|
|
4814
|
+
: [];
|
|
4815
|
+
const frozenExploratoryOutputFailures = [...new Set([
|
|
4816
|
+
...missingFrozenExploratoryOutputs,
|
|
4817
|
+
...missingFrozenExploratoryBindingProofs,
|
|
4818
|
+
])];
|
|
4819
|
+
const frozenExploratoryOutputContractFailure = frozenExploratoryOutputFailures.length > 0;
|
|
3925
4820
|
// ANY question whose SQL executed but dropped multiple requested columns used to
|
|
3926
4821
|
// REFUSE outright ("no governed answer"), throwing away a result that actually
|
|
3927
4822
|
// ran. Instead, SURFACE the partial result (review-required) with a warning that
|
|
@@ -3944,13 +4839,16 @@ async function runAnswerLoop(input) {
|
|
|
3944
4839
|
...repairNotes,
|
|
3945
4840
|
...(resultShape?.warnings ?? []),
|
|
3946
4841
|
...(partialShapeWarning ? [partialShapeWarning] : []),
|
|
4842
|
+
...(frozenExploratoryOutputContractFailure
|
|
4843
|
+
? [`The executed exploratory result violated the frozen output contract: ${frozenExploratoryOutputFailures.join(', ')}.`]
|
|
4844
|
+
: []),
|
|
3947
4845
|
...(topNTrimNote ? [topNTrimNote] : []),
|
|
3948
4846
|
...(executionError ? ['The preview execution error must be reviewed before reuse.'] : []),
|
|
3949
4847
|
];
|
|
3950
4848
|
const generatedOutputs = parsed.outputs?.length ? parsed.outputs : resultColumnNames(result);
|
|
3951
4849
|
const generatedRequestedFilters = mergeProposalStringLists(input.followUp?.filters, parsed.requestedFilters);
|
|
3952
4850
|
const generatedRequestedDimensions = mergeProposalStringLists(input.followUp?.dimensions, parsed.requestedDimensions);
|
|
3953
|
-
const baseDqlArtifact = exploratoryClosureDenied
|
|
4851
|
+
const baseDqlArtifact = exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure
|
|
3954
4852
|
? undefined
|
|
3955
4853
|
: result?.dqlArtifact ?? semanticBridgeAnswer?.dqlArtifact ?? buildGeneratedSqlDqlArtifact({
|
|
3956
4854
|
question,
|
|
@@ -3979,7 +4877,7 @@ async function runAnswerLoop(input) {
|
|
|
3979
4877
|
: undefined;
|
|
3980
4878
|
let draftBlock;
|
|
3981
4879
|
let draftCaptureError;
|
|
3982
|
-
if (!exploratoryClosureDenied && input.captureGeneratedDraft && parsed.sql) {
|
|
4880
|
+
if (!exploratoryClosureDenied && !semanticFanoutProbeFailure && !frozenExploratoryOutputContractFailure && input.captureGeneratedDraft && parsed.sql) {
|
|
3983
4881
|
try {
|
|
3984
4882
|
draftBlock = await input.captureGeneratedDraft({
|
|
3985
4883
|
question,
|
|
@@ -4026,10 +4924,21 @@ async function runAnswerLoop(input) {
|
|
|
4026
4924
|
: undefined;
|
|
4027
4925
|
const certifiedMetricAnswer = semanticMetricCertification === 'certified' || semanticMetricCertification === 'reviewed';
|
|
4028
4926
|
const governedMetricExecutionFailure = governedMetricAnswer && Boolean(executionError);
|
|
4029
|
-
const terminalExecutionFailure = governedMetricExecutionFailure || exploratoryClosureDenied;
|
|
4927
|
+
const terminalExecutionFailure = governedMetricExecutionFailure || exploratoryClosureDenied || frozenExploratoryOutputContractFailure;
|
|
4928
|
+
const semanticFanoutTraceError = semanticFanoutProbeFailure
|
|
4929
|
+
? Object.assign(new Error(semanticFanoutProbeFailure.message), {
|
|
4930
|
+
code: semanticFanoutProbeFailure.code,
|
|
4931
|
+
details: { phase: 'validation' },
|
|
4932
|
+
})
|
|
4933
|
+
: executionError;
|
|
4934
|
+
const terminalFailureMessage = frozenExploratoryOutputContractFailure
|
|
4935
|
+
? `The exploratory execution result did not retain proven frozen output binding ${frozenExploratoryOutputFailures.join(', ')}, so DQL did not display it.`
|
|
4936
|
+
: exploratoryClosureDenied
|
|
4937
|
+
? 'The generated SQL referenced a relation outside the router-selected physical closure, so DQL did not execute it.'
|
|
4938
|
+
: semanticFanoutProbeFailure?.message;
|
|
4030
4939
|
const finalSemanticExecutionTrace = semanticTraceAfterExecution(semanticExecutionTrace, {
|
|
4031
4940
|
executed: Boolean(result),
|
|
4032
|
-
...(
|
|
4941
|
+
...(semanticFanoutTraceError ? { error: semanticFanoutTraceError } : {}),
|
|
4033
4942
|
...(result ? { result } : {}),
|
|
4034
4943
|
});
|
|
4035
4944
|
return {
|
|
@@ -4039,21 +4948,25 @@ async function runAnswerLoop(input) {
|
|
|
4039
4948
|
reviewStatus: terminalExecutionFailure ? 'none' : governedMetricAnswer ? 'governed' : 'draft_ready',
|
|
4040
4949
|
semanticMetricCertification,
|
|
4041
4950
|
confidence: terminalExecutionFailure ? 0 : certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
|
|
4042
|
-
text:
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
? 'The generated SQL referenced a relation outside the router-selected physical closure, so DQL did not execute it.'
|
|
4047
|
-
: generatedText,
|
|
4048
|
-
...(exploratoryClosureDenied ? {} : { proposedSql: parsed.sql, sql: parsed.sql }),
|
|
4049
|
-
result,
|
|
4951
|
+
text: terminalFailureMessage ?? generatedText,
|
|
4952
|
+
answer: terminalFailureMessage ?? generatedText,
|
|
4953
|
+
...(exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure ? {} : { proposedSql: parsed.sql, sql: parsed.sql }),
|
|
4954
|
+
result: frozenExploratoryOutputContractFailure ? undefined : result,
|
|
4050
4955
|
executionError,
|
|
4051
4956
|
...(warehouseFailure ? { warehouseFailure } : {}),
|
|
4052
4957
|
...(finalSemanticExecutionTrace ? { semanticExecutionTrace: finalSemanticExecutionTrace } : {}),
|
|
4053
|
-
...(terminalExecutionFailure ? {
|
|
4958
|
+
...(terminalExecutionFailure ? {
|
|
4959
|
+
refusalCode: semanticFanoutProbeFailure ? 'policy_blocked' : 'grounding_gap',
|
|
4960
|
+
} : {}),
|
|
4961
|
+
...(semanticFanoutProbeFailure ? {
|
|
4962
|
+
refusalDetails: {
|
|
4963
|
+
code: semanticFanoutProbeFailure.code,
|
|
4964
|
+
message: semanticFanoutProbeFailure.message,
|
|
4965
|
+
},
|
|
4966
|
+
} : {}),
|
|
4054
4967
|
suggestedViz: parsed.viz ?? 'table',
|
|
4055
|
-
...(exploratoryClosureDenied ? {} : { dqlArtifact: answerDqlArtifact }),
|
|
4056
|
-
...(exploratoryClosureDenied ? {} : {
|
|
4968
|
+
...(exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure ? {} : { dqlArtifact: answerDqlArtifact }),
|
|
4969
|
+
...(exploratoryClosureDenied || semanticFanoutProbeFailure || frozenExploratoryOutputContractFailure ? {} : {
|
|
4057
4970
|
draftBlock,
|
|
4058
4971
|
draftBlockId: draftBlock?.path,
|
|
4059
4972
|
promoteCommand: draftBlock ? `dql certify --from-draft ${draftBlock.path}` : undefined,
|
|
@@ -4095,7 +5008,8 @@ async function runAnswerLoop(input) {
|
|
|
4095
5008
|
considered,
|
|
4096
5009
|
providerUsed: provider.name,
|
|
4097
5010
|
aggregationSafetyProof,
|
|
4098
|
-
...(
|
|
5011
|
+
...(initialExploratoryExecution ? { exploratoryExecutionFreeze: initialExploratoryExecution.freeze } : {}),
|
|
5012
|
+
...(exploratoryRepairExecution ? { exploratoryRepairExecutionFreeze: exploratoryRepairExecution.freeze } : {}),
|
|
4099
5013
|
// Carry the governed metric match so the exit point can name a
|
|
4100
5014
|
// `semantic_metric` route (spec 17, part C).
|
|
4101
5015
|
_semanticMetricMatch: governedMetricAnswer ? semanticMetricMatch ?? undefined : undefined,
|
|
@@ -4110,7 +5024,13 @@ function renderExecutionSchemaForRepair(schemaContext) {
|
|
|
4110
5024
|
? `Current execution-target columns (authoritative for this repair):\n${tables.join('\n')}`
|
|
4111
5025
|
: 'No execution-target columns were available; do not invent a replacement column.';
|
|
4112
5026
|
}
|
|
4113
|
-
|
|
5027
|
+
/**
|
|
5028
|
+
* Preserve the declared answer shape even when an authored certified block
|
|
5029
|
+
* returns a broader result set than its typed `top_n` input. The V2 exact
|
|
5030
|
+
* path uses this same normalization so a provider-free execution cannot
|
|
5031
|
+
* present a different result than the ordinary certified route.
|
|
5032
|
+
*/
|
|
5033
|
+
export function trimCertifiedBlockResultToRequestedTopN(result, plan) {
|
|
4114
5034
|
const topN = plan.requestedShape.topN;
|
|
4115
5035
|
if (!topN || topN.scope === 'per_group' || !Array.isArray(result.rows) || result.rows.length <= topN.n)
|
|
4116
5036
|
return result;
|
|
@@ -4135,6 +5055,70 @@ function resultColumnNames(result) {
|
|
|
4135
5055
|
.filter(Boolean);
|
|
4136
5056
|
return columns?.length ? columns : undefined;
|
|
4137
5057
|
}
|
|
5058
|
+
/**
|
|
5059
|
+
* Check only the host-bound aliases from a frozen plan. This is intentionally
|
|
5060
|
+
* stricter than the conversational shape helper: synonym matching is useful
|
|
5061
|
+
* for ordinary narration, but it cannot substitute a requested order/product
|
|
5062
|
+
* identifier after exploratory SQL has been authorized.
|
|
5063
|
+
*/
|
|
5064
|
+
function missingFrozenRequiredOutputProjection(plan, result) {
|
|
5065
|
+
const required = plan?.outputContract?.requiredOutputs ?? [];
|
|
5066
|
+
if (required.length === 0)
|
|
5067
|
+
return [];
|
|
5068
|
+
const columns = new Set((resultColumnNames(result) ?? []).map(canonicalFrozenOutputAlias));
|
|
5069
|
+
return required.flatMap((binding) => {
|
|
5070
|
+
const alias = binding.outputName
|
|
5071
|
+
?? binding.qualifiedId?.split(/[:.]/).at(-1)
|
|
5072
|
+
?? binding.requested;
|
|
5073
|
+
const normalized = canonicalFrozenOutputAlias(alias);
|
|
5074
|
+
return normalized && columns.has(normalized) ? [] : [alias];
|
|
5075
|
+
});
|
|
5076
|
+
}
|
|
5077
|
+
/**
|
|
5078
|
+
* A column name in a returned result does not prove where it came from. The
|
|
5079
|
+
* result is accepted only when the server-owned authorization receipt carries
|
|
5080
|
+
* the exact frozen output-source binding that was parser-validated for the SQL
|
|
5081
|
+
* handed to the connector. Old persisted receipts remain readable; they
|
|
5082
|
+
* simply cannot authorize a new result for a plan that has explicit outputs.
|
|
5083
|
+
*/
|
|
5084
|
+
function missingFrozenRequiredOutputBindingProof(plan, freeze) {
|
|
5085
|
+
const required = plan?.outputContract?.requiredOutputs ?? [];
|
|
5086
|
+
if (required.length === 0)
|
|
5087
|
+
return [];
|
|
5088
|
+
const expected = frozenRequiredOutputBindingProofsForPlan(plan);
|
|
5089
|
+
const missingSourceBindingAliases = required
|
|
5090
|
+
.filter((binding) => !expected.some((proof) => canonicalFrozenOutputAlias(proof.outputName)
|
|
5091
|
+
=== canonicalFrozenOutputAlias(binding.outputName
|
|
5092
|
+
?? binding.qualifiedId?.split(/[:.]/).at(-1)
|
|
5093
|
+
?? binding.requested)))
|
|
5094
|
+
.map((binding) => binding.outputName
|
|
5095
|
+
?? binding.qualifiedId?.split(/[:.]/).at(-1)
|
|
5096
|
+
?? binding.requested);
|
|
5097
|
+
const actual = freeze?.requiredOutputBindings ?? [];
|
|
5098
|
+
const missingProofAliases = expected
|
|
5099
|
+
.filter((proof) => !actual.some((candidate) => sameFrozenRequiredOutputBindingProof(candidate, proof)))
|
|
5100
|
+
.map((proof) => proof.outputName);
|
|
5101
|
+
return [...new Set([...missingSourceBindingAliases, ...missingProofAliases])];
|
|
5102
|
+
}
|
|
5103
|
+
function sameFrozenRequiredOutputBindingProof(actual, expected) {
|
|
5104
|
+
return actual.version === 1
|
|
5105
|
+
&& actual.qualifiedId === expected.qualifiedId
|
|
5106
|
+
&& canonicalFrozenOutputAlias(actual.outputName) === canonicalFrozenOutputAlias(expected.outputName)
|
|
5107
|
+
&& canonicalFrozenSourceIdentifier(actual.relation) === canonicalFrozenSourceIdentifier(expected.relation)
|
|
5108
|
+
&& canonicalFrozenSourceIdentifier(actual.column) === canonicalFrozenSourceIdentifier(expected.column);
|
|
5109
|
+
}
|
|
5110
|
+
function canonicalFrozenOutputAlias(value) {
|
|
5111
|
+
return value.toLowerCase()
|
|
5112
|
+
.replace(/["`]/g, '')
|
|
5113
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
5114
|
+
.replace(/^_+|_+$/g, '');
|
|
5115
|
+
}
|
|
5116
|
+
function canonicalFrozenSourceIdentifier(value) {
|
|
5117
|
+
return value.toLowerCase()
|
|
5118
|
+
.replace(/["`\[\]]/g, '')
|
|
5119
|
+
.replace(/\s*\.\s*/g, '.')
|
|
5120
|
+
.trim();
|
|
5121
|
+
}
|
|
4138
5122
|
function analyticalCapabilityForPlan(plan, contextPack, registry = []) {
|
|
4139
5123
|
const selected = normalizeMetricCapabilityContract(plan.selectedCapability);
|
|
4140
5124
|
if (selected) {
|
|
@@ -4501,8 +5485,11 @@ async function executeSemanticAnalyticalGraph(input) {
|
|
|
4501
5485
|
});
|
|
4502
5486
|
aggregationSafetyProof = proof;
|
|
4503
5487
|
if (proof.status !== 'safe') {
|
|
4504
|
-
return analyticalGraphFailureAnswer(input, '
|
|
4505
|
-
|
|
5488
|
+
return analyticalGraphFailureAnswer(input, 'COMPILATION_FAILED', semanticAggregationSafetyFailure(proof), {
|
|
5489
|
+
// This is compiler/plan validation before a warehouse statement is
|
|
5490
|
+
// dispatched. It must be represented as semantic.compile, never as
|
|
5491
|
+
// result normalization or a SQL execution failure.
|
|
5492
|
+
phase: 'compilation',
|
|
4506
5493
|
dqlArtifact: composed.dqlArtifact,
|
|
4507
5494
|
compiledSql: composed.sql,
|
|
4508
5495
|
semanticExecutionTrace,
|
|
@@ -4528,11 +5515,25 @@ async function executeSemanticAnalyticalGraph(input) {
|
|
|
4528
5515
|
}
|
|
4529
5516
|
catch (error) {
|
|
4530
5517
|
const message = error instanceof Error ? error.message : String(error);
|
|
5518
|
+
// A missing local execution target fails at the host's pre-execution
|
|
5519
|
+
// boundary. It must not be recast as a semantic compiler failure merely
|
|
5520
|
+
// because the semantic graph owns the callback. Preserve the narrow,
|
|
5521
|
+
// producer-tagged fact for the durable Ask receipt; it does not alter
|
|
5522
|
+
// the frozen route, trust, or user-facing analytical failure.
|
|
5523
|
+
const observabilityExecutionFailure = observabilityFailureForFrozenExecution(analyticalErrorDetail(error));
|
|
4531
5524
|
return analyticalGraphFailureAnswer(input, 'EXECUTION_FAILED', message, {
|
|
4532
5525
|
phase: 'execution',
|
|
4533
5526
|
dqlArtifact: composed.dqlArtifact,
|
|
4534
5527
|
compiledSql: composed.sql,
|
|
4535
5528
|
semanticExecutionTrace: semanticTraceAfterExecution(semanticExecutionTrace, { executed: false, error }),
|
|
5529
|
+
// The exact aggregation proof already passed before the connector
|
|
5530
|
+
// call. Preserve it so presentation never recasts a warehouse binder
|
|
5531
|
+
// error as a failed semantic aggregation proof.
|
|
5532
|
+
aggregationSafetyProof,
|
|
5533
|
+
// This producer-tagged fact is diagnostic only: it does not alter
|
|
5534
|
+
// routing, trust, or the immutable frozen plan.
|
|
5535
|
+
warehouseFailure: normalizeWarehouseSqlFailure(error, input.input.semanticDriver),
|
|
5536
|
+
...(observabilityExecutionFailure ? { observabilityExecutionFailure } : {}),
|
|
4536
5537
|
});
|
|
4537
5538
|
}
|
|
4538
5539
|
const normalized = normalizeAnalyticalSourceResult({
|
|
@@ -4962,6 +5963,10 @@ function analyticalGraphFailureAnswer(input, code, reason, artifacts = {}) {
|
|
|
4962
5963
|
...(artifacts.dqlArtifact ? { dqlArtifact: artifacts.dqlArtifact } : {}),
|
|
4963
5964
|
...(artifacts.semanticExecutionTrace ? { semanticExecutionTrace: artifacts.semanticExecutionTrace } : {}),
|
|
4964
5965
|
...(artifacts.aggregationSafetyProof ? { aggregationSafetyProof: artifacts.aggregationSafetyProof } : {}),
|
|
5966
|
+
...(artifacts.warehouseFailure ? { warehouseFailure: artifacts.warehouseFailure } : {}),
|
|
5967
|
+
...(artifacts.observabilityExecutionFailure
|
|
5968
|
+
? { observabilityExecutionFailure: artifacts.observabilityExecutionFailure }
|
|
5969
|
+
: {}),
|
|
4965
5970
|
citations: contextPackCitations(input.input.contextPack, 8),
|
|
4966
5971
|
considered: input.considered,
|
|
4967
5972
|
contextPack: input.input.contextPack,
|
|
@@ -5133,6 +6138,38 @@ Rules:
|
|
|
5133
6138
|
// combined dataset — show them separately" refusal into the join the user asked
|
|
5134
6139
|
// for, while still allowing an honest refusal if context is truly missing.
|
|
5135
6140
|
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.`;
|
|
6141
|
+
/**
|
|
6142
|
+
* A model-decline correction is only a repair when the router already froze a
|
|
6143
|
+
* bounded exploratory RAP and the host supplied the matching capability
|
|
6144
|
+
* mint/consume closures. Anything weaker is a new planning attempt and must
|
|
6145
|
+
* not borrow the repair transport allowance.
|
|
6146
|
+
*/
|
|
6147
|
+
function frozenExploratoryModelRepairAuthority(input) {
|
|
6148
|
+
const plan = input.resolvedAnalyticalPlan;
|
|
6149
|
+
return input.selectedCascadeTier === 'exploratory_sql'
|
|
6150
|
+
&& plan?.mode === 'authoritative'
|
|
6151
|
+
&& plan.capability === 'bounded_exploration'
|
|
6152
|
+
&& Boolean(plan.planId && plan.fingerprint && plan.snapshotId)
|
|
6153
|
+
&& Boolean(input.prepareExploratorySqlExecution && input.executeAgenticGeneratedSql);
|
|
6154
|
+
}
|
|
6155
|
+
/**
|
|
6156
|
+
* The corrective prompt repeats only host-owned tuple facts. It never asks
|
|
6157
|
+
* the model to choose a new route, relation closure, output contract, or
|
|
6158
|
+
* trust label; SQL authorization re-proves those facts after the response.
|
|
6159
|
+
*/
|
|
6160
|
+
function renderFrozenExploratoryRepairContract(plan, questionPlan) {
|
|
6161
|
+
const requiredOutputs = plan.outputContract?.requiredOutputs?.filter((output) => output.status === 'resolved' && output.outputName && output.qualifiedId)
|
|
6162
|
+
.map((output) => `${output.outputName} <- ${output.qualifiedId}`)
|
|
6163
|
+
?? [];
|
|
6164
|
+
return [
|
|
6165
|
+
'This is the one permitted correction for an already frozen, review-required exploratory plan.',
|
|
6166
|
+
renderRequestedShapeForRepair(questionPlan),
|
|
6167
|
+
requiredOutputs.length > 0
|
|
6168
|
+
? `Frozen required output bindings (preserve exactly): ${requiredOutputs.join('; ')}`
|
|
6169
|
+
: 'Preserve the frozen requested tuple exactly.',
|
|
6170
|
+
'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.',
|
|
6171
|
+
].join('\n');
|
|
6172
|
+
}
|
|
5136
6173
|
/**
|
|
5137
6174
|
* Produces the prompt-facing subset of a broad local context pack. Global
|
|
5138
6175
|
* records remain available, while a record explicitly owned by another domain
|
|
@@ -7842,6 +8879,11 @@ async function requestSqlRepair(input) {
|
|
|
7842
8879
|
// bumping would let an internal preview-repair exceed that cap. Escalation-level
|
|
7843
8880
|
// repairs bump-then-clamp in the host (resolveRunReasoningEffort).
|
|
7844
8881
|
reasoningEffort: input.reasoningEffort,
|
|
8882
|
+
...(input.dispatchPhase ? { dispatchPhase: input.dispatchPhase } : {}),
|
|
8883
|
+
...(input.egressPurpose ? { egressPurpose: input.egressPurpose } : {}),
|
|
8884
|
+
...(input.maxProviderDispatches !== undefined
|
|
8885
|
+
? { maxProviderDispatches: input.maxProviderDispatches }
|
|
8886
|
+
: {}),
|
|
7845
8887
|
});
|
|
7846
8888
|
}
|
|
7847
8889
|
async function generateProposalWithOptionalTools(input) {
|
|
@@ -7849,6 +8891,11 @@ async function generateProposalWithOptionalTools(input) {
|
|
|
7849
8891
|
const options = {
|
|
7850
8892
|
signal: input.signal,
|
|
7851
8893
|
reasoningEffort: input.reasoningEffort,
|
|
8894
|
+
...(input.dispatchPhase ? { dispatchPhase: input.dispatchPhase } : {}),
|
|
8895
|
+
...(input.egressPurpose ? { egressPurpose: input.egressPurpose } : {}),
|
|
8896
|
+
...(input.maxProviderDispatches !== undefined
|
|
8897
|
+
? { maxProviderDispatches: input.maxProviderDispatches }
|
|
8898
|
+
: {}),
|
|
7852
8899
|
};
|
|
7853
8900
|
// No tools → plain generation (nothing for the loop to drive).
|
|
7854
8901
|
if (tools.length === 0) {
|
|
@@ -7871,7 +8918,7 @@ async function generateProposalWithOptionalTools(input) {
|
|
|
7871
8918
|
// provider implements generateWithTools (Claude/OpenAI), and an equivalent text
|
|
7872
8919
|
// protocol otherwise (subscription-CLI passthrough, Ollama). This is what gives
|
|
7873
8920
|
// every provider — not just the two API ones — a real tool-driven Stage B.
|
|
7874
|
-
|
|
8921
|
+
const result = await runAgenticToolLoopDetailed(input.provider, [...input.messages], tools, {
|
|
7875
8922
|
...options,
|
|
7876
8923
|
toolPolicy,
|
|
7877
8924
|
maxToolCalls: toolBudget.maxToolCalls,
|
|
@@ -7882,7 +8929,26 @@ async function generateProposalWithOptionalTools(input) {
|
|
|
7882
8929
|
sink.push(evidenceToolCallFromEvent(event, sink.length + 1));
|
|
7883
8930
|
},
|
|
7884
8931
|
});
|
|
8932
|
+
// The tool loop converts a budget stop into a TERMINAL RESULT rather than
|
|
8933
|
+
// rethrowing, so the reason reached this caller as an empty string and the
|
|
8934
|
+
// run reported "the model declined to propose SQL" — blaming the model for
|
|
8935
|
+
// a ceiling DQL imposed on itself. The plain-generation branch above throws
|
|
8936
|
+
// these, so rethrow here too and let one handler label them honestly.
|
|
8937
|
+
const budgetStop = PROPOSAL_BUDGET_STOP_CODES[result.stop ?? ''];
|
|
8938
|
+
if (budgetStop) {
|
|
8939
|
+
throw Object.assign(new Error(`The Ask runtime stopped before a query was settled (${result.stop}).`), { code: budgetStop });
|
|
8940
|
+
}
|
|
8941
|
+
return result.text;
|
|
7885
8942
|
}
|
|
8943
|
+
/**
|
|
8944
|
+
* Tool-loop terminal reasons that mean DQL stopped itself, mapped back to the
|
|
8945
|
+
* dispatch codes the answer loop's provider-failure handler understands.
|
|
8946
|
+
*/
|
|
8947
|
+
const PROPOSAL_BUDGET_STOP_CODES = {
|
|
8948
|
+
provider_dispatch_budget_exhausted: 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED',
|
|
8949
|
+
run_soft_target_exceeded: 'RUN_SOFT_TARGET_EXCEEDED',
|
|
8950
|
+
run_deadline_insufficient: 'RUN_DEADLINE_INSUFFICIENT',
|
|
8951
|
+
};
|
|
7886
8952
|
async function selectDeepGeneratedProposalCandidate(input) {
|
|
7887
8953
|
const initial = await scoreDeepGeneratedProposalCandidate(input, {
|
|
7888
8954
|
raw: input.initial.raw,
|
|
@@ -8245,10 +9311,25 @@ function isRetryableGeneratedSqlError(failure) {
|
|
|
8245
9311
|
return failure.retryDisposition === 'model_repair';
|
|
8246
9312
|
}
|
|
8247
9313
|
/**
|
|
8248
|
-
* Only SQL-shape/binding failures may leave
|
|
8249
|
-
* authorization, cancellation, and timeout failures would affect
|
|
8250
|
-
* route and must remain terminal rather than triggering wasteful
|
|
9314
|
+
* Only SQL-shape/binding failures may leave a frozen governed lane.
|
|
9315
|
+
* Connectivity, authorization, cancellation, and timeout failures would affect
|
|
9316
|
+
* every query route and must remain terminal rather than triggering wasteful
|
|
9317
|
+
* provider work.
|
|
8251
9318
|
*/
|
|
9319
|
+
function observabilityFailureForFrozenExecution(detail) {
|
|
9320
|
+
// Do not infer this from user-facing error text. The local runtime tags the
|
|
9321
|
+
// physical pre-execution boundary; this projection only carries that exact
|
|
9322
|
+
// typed fact into the redacted trace after a governed plan has frozen.
|
|
9323
|
+
if (detail?.origin !== 'host' || detail.stage !== 'execute' || detail.code !== 'connection_not_configured') {
|
|
9324
|
+
return undefined;
|
|
9325
|
+
}
|
|
9326
|
+
return {
|
|
9327
|
+
version: 1,
|
|
9328
|
+
phase: 'execution',
|
|
9329
|
+
cause: 'connection_not_configured',
|
|
9330
|
+
safeAction: 'configure_connection',
|
|
9331
|
+
};
|
|
9332
|
+
}
|
|
8252
9333
|
function isRetryableCertifiedExecutionError(error) {
|
|
8253
9334
|
return /\b(?:binder|parser|catalog)\s+error\b/i.test(error)
|
|
8254
9335
|
// DuckDB says "ambiguous reference"; Snowflake says "ambiguous column name".
|
|
@@ -9213,29 +10294,45 @@ function uniqueAssets(assets) {
|
|
|
9213
10294
|
return Array.from(byId.values());
|
|
9214
10295
|
}
|
|
9215
10296
|
/**
|
|
9216
|
-
* Execute the semantic layer's fanout probe
|
|
9217
|
-
*
|
|
9218
|
-
*
|
|
9219
|
-
*
|
|
10297
|
+
* Execute the semantic layer's fanout probe before a governed native semantic
|
|
10298
|
+
* join freezes. A native join that cannot be checked is not a governed-safe
|
|
10299
|
+
* join: fail closed with a redacted typed failure instead of executing a query
|
|
10300
|
+
* that could multiply aggregates. MetricFlow/dbt Cloud paths have their own
|
|
10301
|
+
* compiler guarantees and do not use this native direct-join guard.
|
|
9220
10302
|
*/
|
|
9221
10303
|
export async function probeSemanticJoinFanout(probeSql, joinedTables, executeSql) {
|
|
9222
10304
|
try {
|
|
9223
10305
|
const payload = await executeSql(probeSql);
|
|
9224
10306
|
const counts = parseFanoutProbeCounts(payload);
|
|
9225
|
-
if (!counts || counts.base <= 0
|
|
9226
|
-
return
|
|
10307
|
+
if (!counts || counts.base <= 0) {
|
|
10308
|
+
return {
|
|
10309
|
+
status: 'blocked',
|
|
10310
|
+
code: 'SEMANTIC_FANOUT_PROBE_UNPARSEABLE',
|
|
10311
|
+
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.',
|
|
10312
|
+
};
|
|
10313
|
+
}
|
|
10314
|
+
if (counts.joined <= counts.base)
|
|
10315
|
+
return { status: 'safe' };
|
|
9227
10316
|
const factor = counts.joined / counts.base;
|
|
9228
10317
|
const tables = joinedTables.filter(Boolean).join(', ');
|
|
9229
|
-
return
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
10318
|
+
return {
|
|
10319
|
+
status: 'blocked',
|
|
10320
|
+
code: 'SEMANTIC_FANOUT_DUPLICATE_KEY',
|
|
10321
|
+
message: [
|
|
10322
|
+
`Blocked a governed semantic answer whose join inflates results: joining ${tables || 'the declared tables'}`,
|
|
10323
|
+
`turned ${counts.base.toLocaleString('en-US')} base rows into ${counts.joined.toLocaleString('en-US')} (×${factor >= 10 ? Math.round(factor) : factor.toFixed(1)}).`,
|
|
10324
|
+
'The declared join key is not unique on the joined side, so every aggregated value would be multiplied.',
|
|
10325
|
+
'Deduplicate the joined model (for example, filter a slowly-changing dimension to current records)',
|
|
10326
|
+
'or execute this metric through MetricFlow / dbt Cloud, then retry. No numbers were shown because they would be wrong.',
|
|
10327
|
+
].join(' '),
|
|
10328
|
+
};
|
|
9236
10329
|
}
|
|
9237
10330
|
catch {
|
|
9238
|
-
return
|
|
10331
|
+
return {
|
|
10332
|
+
status: 'blocked',
|
|
10333
|
+
code: 'SEMANTIC_FANOUT_PROBE_ERROR',
|
|
10334
|
+
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.',
|
|
10335
|
+
};
|
|
9239
10336
|
}
|
|
9240
10337
|
}
|
|
9241
10338
|
function parseFanoutProbeCounts(payload) {
|