@duckcodeailabs/dql-agent 1.14.0 → 1.14.2
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 +68 -2
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +520 -26
- 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 +25 -0
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +91 -0
- package/dist/agentic/analyst-loop.d.ts.map +1 -0
- package/dist/agentic/analyst-loop.js +331 -0
- package/dist/agentic/analyst-loop.js.map +1 -0
- package/dist/agentic/assumptions.d.ts +62 -0
- package/dist/agentic/assumptions.d.ts.map +1 -0
- package/dist/agentic/assumptions.js +73 -0
- package/dist/agentic/assumptions.js.map +1 -0
- package/dist/agentic/business-explanation.d.ts +56 -0
- package/dist/agentic/business-explanation.d.ts.map +1 -0
- package/dist/agentic/business-explanation.js +130 -0
- package/dist/agentic/business-explanation.js.map +1 -0
- package/dist/agentic/identifier-ledger.d.ts +74 -0
- package/dist/agentic/identifier-ledger.d.ts.map +1 -0
- package/dist/agentic/identifier-ledger.js +151 -0
- package/dist/agentic/identifier-ledger.js.map +1 -0
- package/dist/agentic/ledger-tools.d.ts +68 -0
- package/dist/agentic/ledger-tools.d.ts.map +1 -0
- package/dist/agentic/ledger-tools.js +198 -0
- package/dist/agentic/ledger-tools.js.map +1 -0
- package/dist/agentic/orchestrator-policy.d.ts +60 -0
- package/dist/agentic/orchestrator-policy.d.ts.map +1 -0
- package/dist/agentic/orchestrator-policy.js +73 -0
- package/dist/agentic/orchestrator-policy.js.map +1 -0
- package/dist/agentic/orchestrator.d.ts +54 -0
- package/dist/agentic/orchestrator.d.ts.map +1 -0
- package/dist/agentic/orchestrator.js +67 -0
- package/dist/agentic/orchestrator.js.map +1 -0
- package/dist/agentic/preview-tool.d.ts +36 -0
- package/dist/agentic/preview-tool.d.ts.map +1 -0
- package/dist/agentic/preview-tool.js +71 -0
- package/dist/agentic/preview-tool.js.map +1 -0
- package/dist/agentic/rerank.d.ts +50 -0
- package/dist/agentic/rerank.d.ts.map +1 -0
- package/dist/agentic/rerank.js +140 -0
- package/dist/agentic/rerank.js.map +1 -0
- package/dist/agentic/research/hypothesis.d.ts +110 -0
- package/dist/agentic/research/hypothesis.d.ts.map +1 -0
- package/dist/agentic/research/hypothesis.js +125 -0
- package/dist/agentic/research/hypothesis.js.map +1 -0
- package/dist/agentic/research/synthesis.d.ts +36 -0
- package/dist/agentic/research/synthesis.d.ts.map +1 -0
- package/dist/agentic/research/synthesis.js +109 -0
- package/dist/agentic/research/synthesis.js.map +1 -0
- package/dist/agentic/research-agent.d.ts +54 -0
- package/dist/agentic/research-agent.d.ts.map +1 -0
- package/dist/agentic/research-agent.js +177 -0
- package/dist/agentic/research-agent.js.map +1 -0
- package/dist/agentic/sql-authorization.d.ts +131 -0
- package/dist/agentic/sql-authorization.d.ts.map +1 -0
- package/dist/agentic/sql-authorization.js +460 -0
- package/dist/agentic/sql-authorization.js.map +1 -0
- package/dist/agentic/tool-loop.d.ts +29 -0
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +129 -52
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/agentic/toolset.d.ts.map +1 -1
- package/dist/agentic/toolset.js +155 -0
- package/dist/agentic/toolset.js.map +1 -1
- package/dist/agentic/turn-plan.d.ts +35 -0
- package/dist/agentic/turn-plan.d.ts.map +1 -0
- package/dist/agentic/turn-plan.js +108 -0
- package/dist/agentic/turn-plan.js.map +1 -0
- package/dist/agentic/value-probe.d.ts +89 -0
- package/dist/agentic/value-probe.d.ts.map +1 -0
- package/dist/agentic/value-probe.js +304 -0
- package/dist/agentic/value-probe.js.map +1 -0
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +10 -1
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +382 -0
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +782 -6
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/analytical-request-policy.d.ts +26 -0
- package/dist/analytical-request-policy.d.ts.map +1 -0
- package/dist/analytical-request-policy.js +82 -0
- package/dist/analytical-request-policy.js.map +1 -0
- package/dist/analytical-result-facts.d.ts +13 -0
- package/dist/analytical-result-facts.d.ts.map +1 -1
- package/dist/analytical-result-facts.js +37 -1
- package/dist/analytical-result-facts.js.map +1 -1
- package/dist/answer-loop.d.ts +105 -16
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +833 -108
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +32 -1
- package/dist/answer-shape.d.ts.map +1 -1
- package/dist/answer-shape.js +49 -3
- package/dist/answer-shape.js.map +1 -1
- package/dist/cascade/budgets.d.ts.map +1 -1
- package/dist/cascade/budgets.js +8 -1
- package/dist/cascade/budgets.js.map +1 -1
- package/dist/cascade/packer.d.ts +88 -0
- package/dist/cascade/packer.d.ts.map +1 -0
- package/dist/cascade/packer.js +106 -0
- package/dist/cascade/packer.js.map +1 -0
- package/dist/conversation/result-ops.d.ts +18 -3
- package/dist/conversation/result-ops.d.ts.map +1 -1
- package/dist/conversation/result-ops.js +77 -5
- package/dist/conversation/result-ops.js.map +1 -1
- package/dist/conversation/rolling-summary.d.ts.map +1 -1
- package/dist/conversation/rolling-summary.js +7 -2
- package/dist/conversation/rolling-summary.js.map +1 -1
- package/dist/conversation/session-store.d.ts +3 -0
- package/dist/conversation/session-store.d.ts.map +1 -1
- package/dist/conversation/session-store.js +8 -4
- package/dist/conversation/session-store.js.map +1 -1
- package/dist/conversation/snapshot.d.ts +20 -0
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +40 -2
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/fixtures/ask-ai-office-shaped.d.ts +153 -0
- package/dist/fixtures/ask-ai-office-shaped.d.ts.map +1 -0
- package/dist/fixtures/ask-ai-office-shaped.js +94 -0
- package/dist/fixtures/ask-ai-office-shaped.js.map +1 -0
- package/dist/hints/store.d.ts.map +1 -1
- package/dist/hints/store.js +15 -5
- package/dist/hints/store.js.map +1 -1
- package/dist/index.d.ts +29 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -6
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +36 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +95 -0
- package/dist/intent-controller.js.map +1 -1
- package/dist/meaning-resolution.d.ts +69 -2
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +128 -7
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +124 -7
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/block-fit.d.ts +36 -0
- package/dist/metadata/block-fit.d.ts.map +1 -1
- package/dist/metadata/block-fit.js +290 -38
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts +20 -23
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +193 -22
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +10 -1
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +275 -53
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/metadata/metric-match.d.ts +32 -0
- package/dist/metadata/metric-match.d.ts.map +1 -1
- package/dist/metadata/metric-match.js +120 -9
- package/dist/metadata/metric-match.js.map +1 -1
- package/dist/research-loop.d.ts +33 -2
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +66 -4
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +7 -0
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +11 -4
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +32 -19
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1977 -73
- package/dist/router.js.map +1 -1
- package/dist/semantic-bridge/member-select.d.ts.map +1 -1
- package/dist/semantic-bridge/member-select.js +39 -21
- package/dist/semantic-bridge/member-select.js.map +1 -1
- package/package.json +5 -5
package/dist/answer-loop.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { packContext } from './cascade/packer.js';
|
|
1
2
|
/**
|
|
2
3
|
* Dynamic-first governed answer loop.
|
|
3
4
|
*
|
|
@@ -26,10 +27,10 @@ import { domainContextSearchDomains } from './domain-context.js';
|
|
|
26
27
|
import { deriveGeneratedDraftSlug, renderGeneratedSqlDqlArtifact } from './metadata/drafts.js';
|
|
27
28
|
import { buildAnalysisQuestionPlan } from './metadata/analysis-planner.js';
|
|
28
29
|
import { certifiedFitAllowsTier1, certifiedTerminationVerdict, evaluateCertifiedBlockFit } from './metadata/block-fit.js';
|
|
29
|
-
import { buildGovernedMetricFirstSql, matchSemanticMetric, resolveGovernedMetricDefinition, resolveGovernedMetricSql } from './metadata/metric-match.js';
|
|
30
|
+
import { buildGovernedMetricFirstSql, matchSemanticMetric, resolveGovernedMetricDefinition, resolveGovernedMetricSql, semanticMetricEmbeddingOptions } from './metadata/metric-match.js';
|
|
30
31
|
import { decideAgentAction } from './intent-controller.js';
|
|
31
32
|
import { createContextLedger } from './grounding/context-ledger.js';
|
|
32
|
-
import { validateAnswerResultShape } from './answer-shape.js';
|
|
33
|
+
import { validateAnswerResultShape, } from './answer-shape.js';
|
|
33
34
|
import { analyticalErrorDetail, } from './analytical-error.js';
|
|
34
35
|
import { fanoutWarningsForSql } from './metadata/grain-ledger.js';
|
|
35
36
|
import { buildAggregationSafetyProof, buildSemanticCompilationAggregationSafetyProof, } from './aggregation-safety-proof.js';
|
|
@@ -383,6 +384,142 @@ function certifiedInvocationInputs(block, plan) {
|
|
|
383
384
|
}
|
|
384
385
|
return Object.keys(parameters).length > 0 ? { parameters, parameterSources } : {};
|
|
385
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Return only output aliases that the selected certified artifact itself
|
|
389
|
+
* declares. This is deliberately built after the router has frozen the plan:
|
|
390
|
+
* retrieval context, tags, examples, and sibling metrics never enter the
|
|
391
|
+
* result-shape proof.
|
|
392
|
+
*
|
|
393
|
+
* A block may express an entity role (`customer`) through a readable label
|
|
394
|
+
* column (`customer_name`), or a declared dimension role through a physical
|
|
395
|
+
* projection alias (`product_type`). Those are valid execution contracts only
|
|
396
|
+
* when the selected block owns both sides of the binding.
|
|
397
|
+
*/
|
|
398
|
+
function frozenCertifiedResultShapeBindings(plan, block, questionPlan, options = {}) {
|
|
399
|
+
if (!plan || block.kind !== 'block')
|
|
400
|
+
return [];
|
|
401
|
+
const canonical = (value) => value
|
|
402
|
+
.toLowerCase()
|
|
403
|
+
.replace(/[_\-./]+/g, ' ')
|
|
404
|
+
.replace(/[^a-z0-9 ]+/g, ' ')
|
|
405
|
+
.split(/\s+/)
|
|
406
|
+
.filter(Boolean)
|
|
407
|
+
.map((token) => token.endsWith('ies') && token.length > 4
|
|
408
|
+
? `${token.slice(0, -3)}y`
|
|
409
|
+
: token.endsWith('s') && token.length > 3
|
|
410
|
+
? token.slice(0, -1)
|
|
411
|
+
: token)
|
|
412
|
+
.join('_');
|
|
413
|
+
const declared = (block.outputContract?.length
|
|
414
|
+
? block.outputContract.map((output) => ({ name: output.name, role: output.role }))
|
|
415
|
+
: (block.declaredOutputs ?? []).map((name) => ({ name })))
|
|
416
|
+
.filter((output) => Boolean(output.name?.trim()));
|
|
417
|
+
const declaredByCanonical = new Map(declared.map((output) => [canonical(output.name), output]));
|
|
418
|
+
const bindings = [];
|
|
419
|
+
const add = (binding) => {
|
|
420
|
+
const key = `${binding.role}\0${canonical(binding.requested)}\0${canonical(binding.output)}`;
|
|
421
|
+
const existing = bindings.find((candidate) => `${candidate.role}\0${canonical(candidate.requested)}\0${canonical(candidate.output)}` === key);
|
|
422
|
+
if (existing) {
|
|
423
|
+
const aliases = [...(existing.aliases ?? []), ...(binding.aliases ?? [])]
|
|
424
|
+
.map((alias) => alias.trim())
|
|
425
|
+
.filter(Boolean);
|
|
426
|
+
if (aliases.length > 0)
|
|
427
|
+
existing.aliases = Array.from(new Set(aliases));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
bindings.push(binding);
|
|
431
|
+
};
|
|
432
|
+
// Measures are only admitted when the frozen plan retained the block's
|
|
433
|
+
// strict, artifact-local declared output identity. Do not infer an alias
|
|
434
|
+
// from output role here: that would reintroduce lifetime_spend -> revenue.
|
|
435
|
+
for (const measure of plan.query.measures) {
|
|
436
|
+
if (!measure.outputName)
|
|
437
|
+
continue;
|
|
438
|
+
const output = declaredByCanonical.get(canonical(measure.outputName));
|
|
439
|
+
if (output)
|
|
440
|
+
add({ requested: measure.requested, output: output.name, role: 'measure' });
|
|
441
|
+
}
|
|
442
|
+
const requestedDimensions = Array.from(new Set([
|
|
443
|
+
...plan.query.dimensions.map((binding) => binding.requested),
|
|
444
|
+
...questionPlan.requestedShape.dimensions,
|
|
445
|
+
].map((value) => canonical(value)).filter(Boolean)));
|
|
446
|
+
const declaredEntities = new Set((block.entities ?? []).map(canonical).filter(Boolean));
|
|
447
|
+
const declaredDimensions = new Set((block.dimensions ?? []).map(canonical).filter(Boolean));
|
|
448
|
+
const typedDimensionOutputs = declared.filter((output) => output.role === 'dimension');
|
|
449
|
+
for (const requested of requestedDimensions) {
|
|
450
|
+
// The declared entity role can be rendered as a standard display label,
|
|
451
|
+
// e.g. `customer` -> `customer_name`. Require both the authored entity
|
|
452
|
+
// declaration and the output's explicit entity-label shape.
|
|
453
|
+
if (declaredEntities.has(requested)) {
|
|
454
|
+
const entityLabel = declared.find((output) => {
|
|
455
|
+
const outputName = canonical(output.name);
|
|
456
|
+
return output.role === 'entity_label'
|
|
457
|
+
|| outputName === `${requested}_name`
|
|
458
|
+
|| outputName === `${requested}_label`;
|
|
459
|
+
});
|
|
460
|
+
if (entityLabel)
|
|
461
|
+
add({ requested, output: entityLabel.name, role: 'entity_label' });
|
|
462
|
+
}
|
|
463
|
+
// An exact declared output is an artifact-local business-dimension binding
|
|
464
|
+
// even when an older authored block predates explicit `role = dimension`.
|
|
465
|
+
// This is not a pooled or lexical alias: both the requested business
|
|
466
|
+
// dimension and the selected artifact's own projection must agree.
|
|
467
|
+
const exactDeclaredDimension = declaredByCanonical.get(requested);
|
|
468
|
+
if (declaredDimensions.has(requested) && exactDeclaredDimension) {
|
|
469
|
+
add({ requested, output: exactDeclaredDimension.name, role: 'dimension' });
|
|
470
|
+
// Otherwise a physical output can stand for a business dimension only when
|
|
471
|
+
// the block declares that dimension and has one unambiguous typed
|
|
472
|
+
// dimension projection. Multiple physical outputs require an authored
|
|
473
|
+
// alias rather than a guess.
|
|
474
|
+
}
|
|
475
|
+
else if (declaredDimensions.has(requested) && typedDimensionOutputs.length === 1) {
|
|
476
|
+
add({ requested, output: typedDimensionOutputs[0].name, role: 'dimension' });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// An exact *unique* authored example can prove that a parser token is a
|
|
480
|
+
// member value of the selected block's already-declared dimension. For
|
|
481
|
+
// example, the block-owned example "revenue by food and drink" returns the
|
|
482
|
+
// declared `category` output; `food` is not an independently requested
|
|
483
|
+
// physical column. Keep this binding intentionally local to the frozen
|
|
484
|
+
// block and its resolved dimension: no tags, descriptions, sibling evidence,
|
|
485
|
+
// or general lexical aliases are admitted here. Measures remain strict.
|
|
486
|
+
if (options.uniqueExactExampleContract === true) {
|
|
487
|
+
const plannedDimensions = new Set(plan.query.dimensions.map((binding) => canonical(binding.requested)));
|
|
488
|
+
const resolvedDimensionBindings = bindings.filter((binding) => binding.role === 'dimension'
|
|
489
|
+
&& plannedDimensions.has(canonical(binding.requested))
|
|
490
|
+
&& declaredByCanonical.has(canonical(binding.output)));
|
|
491
|
+
if (resolvedDimensionBindings.length === 1) {
|
|
492
|
+
const dimensionBinding = resolvedDimensionBindings[0];
|
|
493
|
+
const protectedTerms = new Set([
|
|
494
|
+
...questionPlan.requestedShape.measures,
|
|
495
|
+
...plan.query.measures.map((binding) => binding.requested),
|
|
496
|
+
...questionPlan.requestedShape.filters,
|
|
497
|
+
].map(canonical).filter(Boolean));
|
|
498
|
+
const structuralRoles = new Set([
|
|
499
|
+
'product', 'customer', 'account', 'user', 'member', 'category', 'segment',
|
|
500
|
+
'region', 'channel', 'order', 'day', 'week', 'month', 'quarter', 'year',
|
|
501
|
+
]);
|
|
502
|
+
const memberNoise = Array.from(new Set([
|
|
503
|
+
...questionPlan.requestedShape.dimensions,
|
|
504
|
+
...questionPlan.requestedShape.requiredOutputs,
|
|
505
|
+
].map(canonical).filter(Boolean))).filter((token) => token !== canonical(dimensionBinding.requested)
|
|
506
|
+
&& !declaredByCanonical.has(token)
|
|
507
|
+
&& !declaredEntities.has(token)
|
|
508
|
+
&& !declaredDimensions.has(token)
|
|
509
|
+
&& !protectedTerms.has(token)
|
|
510
|
+
&& !structuralRoles.has(token));
|
|
511
|
+
if (memberNoise.length > 0) {
|
|
512
|
+
add({
|
|
513
|
+
requested: dimensionBinding.requested,
|
|
514
|
+
output: dimensionBinding.output,
|
|
515
|
+
role: 'dimension',
|
|
516
|
+
aliases: memberNoise,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return bindings;
|
|
522
|
+
}
|
|
386
523
|
function damerauLevenshteinDistance(left, right) {
|
|
387
524
|
const rows = left.length + 1;
|
|
388
525
|
const columns = right.length + 1;
|
|
@@ -421,7 +558,35 @@ const MANIFEST_KINDS = ['dbt_model', 'dbt_source'];
|
|
|
421
558
|
* surface shows THIS; the validator's machine message (relation ids, repair
|
|
422
559
|
* tool guidance) belongs in refusalDetails/validationWarnings only.
|
|
423
560
|
*/
|
|
424
|
-
|
|
561
|
+
/**
|
|
562
|
+
* Name the ONE aggregation check that actually fired.
|
|
563
|
+
*
|
|
564
|
+
* The refusal used to list all four possible causes — "rounding too early,
|
|
565
|
+
* losing decimal precision, summing a non-additive value, or multiplying rows
|
|
566
|
+
* across a join" — because the specific one was never plumbed into the message.
|
|
567
|
+
* The system knows: `aggregationIntegrityIssuesForSql` classifies each issue and
|
|
568
|
+
* the proof carries the codes. A menu of four causes reads as "something is
|
|
569
|
+
* wrong somewhere", which is exactly the report a user cannot act on.
|
|
570
|
+
*/
|
|
571
|
+
function aggregationRefusalReason(issueCodes = []) {
|
|
572
|
+
const codes = new Set(issueCodes.map((code) => code.toUpperCase()));
|
|
573
|
+
// Ordered by how badly each distorts the number, so a query tripping several
|
|
574
|
+
// leads with the one that matters most.
|
|
575
|
+
if (codes.has('FANOUT')) {
|
|
576
|
+
return 'joining those tables multiplies rows, so the total would be inflated. Aggregate at the row-level grain first, then join';
|
|
577
|
+
}
|
|
578
|
+
if (codes.has('NON_ADDITIVE_MEASURE')) {
|
|
579
|
+
return 'it sums a measure that is not additive (an average or a ratio cannot be added up). Ask for the governed metric, which carries the right rule';
|
|
580
|
+
}
|
|
581
|
+
if (codes.has('PREMATURE_ROUNDING')) {
|
|
582
|
+
return 'it rounds each value before adding them, which loses money at scale. Rounding belongs on the final total';
|
|
583
|
+
}
|
|
584
|
+
if (codes.has('LOSSY_NUMERIC_CAST')) {
|
|
585
|
+
return 'it converts amounts to floating point before adding them, which drifts on decimal currency';
|
|
586
|
+
}
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
589
|
+
export function renderContextValidationRefusalForUser(code, machineError, memberBindings, aggregationIssueCodes) {
|
|
425
590
|
switch (code) {
|
|
426
591
|
case 'unknown_relation':
|
|
427
592
|
return 'I drafted a query, but it uses a table that was not part of the metadata retrieved for this question, so I did not run it. Ask again (retrieval usually finds it on a follow-up), name the table or metric explicitly, or re-sync the dbt metadata.';
|
|
@@ -438,8 +603,15 @@ function renderContextValidationRefusalForUser(code, machineError, memberBinding
|
|
|
438
603
|
return 'This comparison needs a baseline period or value that the drafted query did not include. Say what to compare against (for example, the prior month) and I will run it.';
|
|
439
604
|
case 'unsafe_sql':
|
|
440
605
|
return 'The drafted query used a statement type that is not allowed in this governed preview, so I did not run it.';
|
|
441
|
-
case 'unsafe_aggregation':
|
|
442
|
-
|
|
606
|
+
case 'unsafe_aggregation': {
|
|
607
|
+
const reason = aggregationRefusalReason(aggregationIssueCodes);
|
|
608
|
+
if (reason) {
|
|
609
|
+
return `I drafted a query but did not run it, because ${reason}. Ask for the governed metric and I will use the safe version.`;
|
|
610
|
+
}
|
|
611
|
+
// No classified code reached us — say that plainly rather than listing
|
|
612
|
+
// every cause as if one of them were known to apply.
|
|
613
|
+
return 'I drafted a query, but it would change how the metric is calculated, so I did not run it. Ask for the governed semantic metric and I will use the safe version.';
|
|
614
|
+
}
|
|
443
615
|
case 'insufficient_context':
|
|
444
616
|
if (/could not be parsed|parse error|syntax/i.test(machineError)) {
|
|
445
617
|
return 'I drafted a query, but its SQL syntax did not match the connected warehouse, so I did not run it. The failed draft is available in Inspect; retrying will generate a warehouse-specific query.';
|
|
@@ -601,7 +773,10 @@ function tryCrossResultAnswer(input) {
|
|
|
601
773
|
const prior = input.followUp?.priorResult;
|
|
602
774
|
if (!prior || !Array.isArray(prior.rows) || prior.rows.length === 0)
|
|
603
775
|
return null;
|
|
604
|
-
|
|
776
|
+
// Pass the prior result's shape so a follow-up that names one of its columns
|
|
777
|
+
// ("what's the average lifetime_spend?") is recognised without requiring a
|
|
778
|
+
// demonstrative pronoun.
|
|
779
|
+
const op = detectResultSetOperation(input.question, prior);
|
|
605
780
|
if (!op)
|
|
606
781
|
return null;
|
|
607
782
|
const computed = computeResultSetOperation(op, prior);
|
|
@@ -696,6 +871,10 @@ async function freezeLegacySemanticSelection(input) {
|
|
|
696
871
|
const match = await matchSemanticMetric(input.question, metricNodes, {
|
|
697
872
|
measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
|
|
698
873
|
...(semanticLayer ? { canExecute: (name) => semanticLayer.canComposeMetric(name) } : {}),
|
|
874
|
+
...(snapshotVectorMetricShortlist(input.contextPack).length
|
|
875
|
+
? { vectorMetricShortlist: snapshotVectorMetricShortlist(input.contextPack) }
|
|
876
|
+
: {}),
|
|
877
|
+
...semanticMetricEmbeddingOptions(input.embeddingProvider),
|
|
699
878
|
}).catch(() => null);
|
|
700
879
|
if (!match)
|
|
701
880
|
return undefined;
|
|
@@ -813,6 +992,26 @@ async function freezeLegacySemanticSelection(input) {
|
|
|
813
992
|
mode: 'authoritative',
|
|
814
993
|
});
|
|
815
994
|
}
|
|
995
|
+
/**
|
|
996
|
+
* Reuse the immutable context pack's independent vector lane. Metric matching
|
|
997
|
+
* never opens the catalog or runs a second metadata retrieval pass; it only
|
|
998
|
+
* narrows its vector-only candidate admission to IDs already retrieved for this
|
|
999
|
+
* request snapshot.
|
|
1000
|
+
*/
|
|
1001
|
+
function snapshotVectorMetricShortlist(contextPack) {
|
|
1002
|
+
const lane = contextPack?.retrievalDiagnostics.lanes?.find((item) => item.lane === 'vector');
|
|
1003
|
+
if (!lane?.candidates.length || !contextPack)
|
|
1004
|
+
return [];
|
|
1005
|
+
const objects = new Map(contextPack.objects.map((object) => [object.objectKey, object]));
|
|
1006
|
+
const result = [];
|
|
1007
|
+
for (const candidate of lane.candidates) {
|
|
1008
|
+
const object = objects.get(candidate.objectKey);
|
|
1009
|
+
if (!object || (object.objectType !== 'semantic_metric' && object.objectType !== 'semantic_measure'))
|
|
1010
|
+
continue;
|
|
1011
|
+
result.push(candidate.objectKey, object.name);
|
|
1012
|
+
}
|
|
1013
|
+
return [...new Set(result)];
|
|
1014
|
+
}
|
|
816
1015
|
export async function answer(input) {
|
|
817
1016
|
const inheritedPlan = !input.resolvedAnalyticalPlan
|
|
818
1017
|
&& input.followUp?.priorResolvedAnalyticalPlan
|
|
@@ -1138,6 +1337,20 @@ function cascadeExecutionStatus(result) {
|
|
|
1138
1337
|
*/
|
|
1139
1338
|
function deriveAiRoute(result, metricMatch) {
|
|
1140
1339
|
if (result.kind === 'no_answer') {
|
|
1340
|
+
// Preserve the frozen certified tier as provenance when that exact block
|
|
1341
|
+
// executed but failed its own result contract. The route is not a trust
|
|
1342
|
+
// badge: `certification` remains review-required and no generated fallback
|
|
1343
|
+
// may be selected after freeze.
|
|
1344
|
+
if (result.certifiedResultShapeFailure && result.sourceTier === 'certified_artifact') {
|
|
1345
|
+
const ref = result.sourceCertifiedBlock ?? result.block?.name ?? result.citations[0]?.name;
|
|
1346
|
+
return {
|
|
1347
|
+
tier: 'certified_block',
|
|
1348
|
+
label: ref
|
|
1349
|
+
? `Certified block ${ref} did not return the complete requested output shape.`
|
|
1350
|
+
: 'The selected certified block did not return the complete requested output shape.',
|
|
1351
|
+
ref,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1141
1354
|
if (result.exploratoryCandidate) {
|
|
1142
1355
|
return {
|
|
1143
1356
|
tier: 'no_answer',
|
|
@@ -1201,6 +1414,17 @@ async function runAnswerLoop(input) {
|
|
|
1201
1414
|
const scopedContextPack = questionDomainScope.length > 0
|
|
1202
1415
|
? scopeContextPackToQuestionDomains(input.contextPack, questionDomainScope, input.manifest)
|
|
1203
1416
|
: input.contextPack;
|
|
1417
|
+
// The router has already chosen the physical exploratory closure. Treat it
|
|
1418
|
+
// as an execution authority boundary here, before any prompt is constructed
|
|
1419
|
+
// or provider is dispatched. Full retrieval stays on `input.contextPack`
|
|
1420
|
+
// for receipts and diagnostics only.
|
|
1421
|
+
const forcedExploratoryTier = input.selectedCascadeTier === 'exploratory_sql';
|
|
1422
|
+
const exploratoryClosureContextPack = forcedExploratoryTier
|
|
1423
|
+
? scopeContextPackToExploratoryCandidateClosure(scopedContextPack, input.exploratoryCandidateIds)
|
|
1424
|
+
: undefined;
|
|
1425
|
+
const executionContextPack = forcedExploratoryTier
|
|
1426
|
+
? exploratoryClosureContextPack
|
|
1427
|
+
: scopedContextPack;
|
|
1204
1428
|
// Select the RELEVANT skills (not all) for this question; keep pinned project
|
|
1205
1429
|
// skills (SQL conventions). Block hints still come from the full set so a
|
|
1206
1430
|
// preferred-block mapping is never lost.
|
|
@@ -1242,7 +1466,7 @@ async function runAnswerLoop(input) {
|
|
|
1242
1466
|
const semanticHits = kg.search({ query: question, ...searchScope, kinds: SEMANTIC_KINDS, limit: 12 });
|
|
1243
1467
|
const manifestHits = kg.search({ query: question, ...searchScope, kinds: MANIFEST_KINDS, limit: 12 });
|
|
1244
1468
|
const considered = mergeHits(artifactHits, semanticHits, manifestHits, kg.search({ query: question, domain, limit: 10 })).slice(0, 30);
|
|
1245
|
-
const schemaContext = schemaContextWithAllowedSqlContext(schemaContextWithinQuestionScope(input.schemaContext ?? [], input.contextPack,
|
|
1469
|
+
const schemaContext = schemaContextWithAllowedSqlContext(schemaContextWithinQuestionScope(input.schemaContext ?? [], input.contextPack, executionContextPack), executionContextPack);
|
|
1246
1470
|
const catalogRoute = input.contextPack?.routeDecision;
|
|
1247
1471
|
const baseQuestionPlan = input.contextPack?.questionPlan?.requestedShape
|
|
1248
1472
|
? input.contextPack.questionPlan
|
|
@@ -1258,12 +1482,35 @@ async function runAnswerLoop(input) {
|
|
|
1258
1482
|
// "exact certified" route into the generation prompt. The model should decide
|
|
1259
1483
|
// from compatible certified evidence, semantic members, and dbt/runtime
|
|
1260
1484
|
// columns—not copy a customer-grain worked example into a product-grain ask.
|
|
1261
|
-
const promptContextPack = contextPackForRequestedShape(
|
|
1485
|
+
const promptContextPack = contextPackForRequestedShape(executionContextPack, question, questionPlan, kg);
|
|
1262
1486
|
const repairBudgetState = createCascadeBudgetState(input.cascadeBudgetModel);
|
|
1263
|
-
|
|
1487
|
+
// A pre-freeze exploratory selection is already the router's result of
|
|
1488
|
+
// evaluating certified → semantic → relational eligibility. Do not let the
|
|
1489
|
+
// legacy answer loop independently rediscover a broad block or metric and
|
|
1490
|
+
// replace that decision before it has attempted the selected bounded SQL
|
|
1491
|
+
// path. Frozen tiers retain their existing plan binding behavior.
|
|
1492
|
+
if (forcedExploratoryTier && input.contextPack && !exploratoryClosureContextPack) {
|
|
1493
|
+
const text = 'The router-selected exploratory path no longer has a complete same-snapshot physical relation closure, so DQL did not send SQL generation or execute a query.';
|
|
1494
|
+
return {
|
|
1495
|
+
kind: 'no_answer',
|
|
1496
|
+
sourceTier: 'no_answer',
|
|
1497
|
+
certification: 'analyst_review_required',
|
|
1498
|
+
reviewStatus: 'none',
|
|
1499
|
+
confidence: 0,
|
|
1500
|
+
text,
|
|
1501
|
+
answer: text,
|
|
1502
|
+
refusalCode: 'grounding_gap',
|
|
1503
|
+
refusalDetails: { code: 'grounding_gap', message: text },
|
|
1504
|
+
citations: contextPackCitations(input.contextPack, 8),
|
|
1505
|
+
contextPack: input.contextPack,
|
|
1506
|
+
considered,
|
|
1507
|
+
providerUsed: provider.name,
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
const authoritativePlanBinding = !forcedExploratoryTier && input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1264
1511
|
? input.resolvedPlanExecutionBinding
|
|
1265
1512
|
: undefined;
|
|
1266
|
-
if (input.analyticalPeriodResolutionFailure) {
|
|
1513
|
+
if (!forcedExploratoryTier && input.analyticalPeriodResolutionFailure) {
|
|
1267
1514
|
const failure = input.analyticalPeriodResolutionFailure;
|
|
1268
1515
|
const structuredFailureCode = failure.error && typeof failure.error === 'object'
|
|
1269
1516
|
&& typeof failure.error.code === 'string'
|
|
@@ -1299,7 +1546,7 @@ async function runAnswerLoop(input) {
|
|
|
1299
1546
|
providerUsed: provider.name,
|
|
1300
1547
|
};
|
|
1301
1548
|
}
|
|
1302
|
-
if (input.analyticalExecutionGraphFailure) {
|
|
1549
|
+
if (!forcedExploratoryTier && input.analyticalExecutionGraphFailure) {
|
|
1303
1550
|
const failure = input.analyticalExecutionGraphFailure;
|
|
1304
1551
|
const analyticalFailure = analyticalFailureForInput(input, {
|
|
1305
1552
|
error: { code: failure.code, message: failure.reason },
|
|
@@ -1324,7 +1571,7 @@ async function runAnswerLoop(input) {
|
|
|
1324
1571
|
providerUsed: provider.name,
|
|
1325
1572
|
};
|
|
1326
1573
|
}
|
|
1327
|
-
if (input.semanticGraphExecutionBinding?.status === 'blocked') {
|
|
1574
|
+
if (!forcedExploratoryTier && input.semanticGraphExecutionBinding?.status === 'blocked') {
|
|
1328
1575
|
const failure = input.semanticGraphExecutionBinding;
|
|
1329
1576
|
const analyticalFailure = analyticalFailureForInput(input, {
|
|
1330
1577
|
error: { code: failure.code, message: failure.reason },
|
|
@@ -1349,7 +1596,7 @@ async function runAnswerLoop(input) {
|
|
|
1349
1596
|
providerUsed: provider.name,
|
|
1350
1597
|
};
|
|
1351
1598
|
}
|
|
1352
|
-
if (input.semanticGraphExecutionBinding?.status === 'ready' && input.analyticalExecutionGraph) {
|
|
1599
|
+
if (!forcedExploratoryTier && input.semanticGraphExecutionBinding?.status === 'ready' && input.analyticalExecutionGraph) {
|
|
1353
1600
|
return executeSemanticAnalyticalGraph({
|
|
1354
1601
|
input,
|
|
1355
1602
|
binding: input.semanticGraphExecutionBinding,
|
|
@@ -1359,7 +1606,7 @@ async function runAnswerLoop(input) {
|
|
|
1359
1606
|
providerName: provider.name,
|
|
1360
1607
|
});
|
|
1361
1608
|
}
|
|
1362
|
-
if (input.governedAnalyticalGraphCompilation?.status === 'blocked') {
|
|
1609
|
+
if (!forcedExploratoryTier && input.governedAnalyticalGraphCompilation?.status === 'blocked') {
|
|
1363
1610
|
const failure = input.governedAnalyticalGraphCompilation;
|
|
1364
1611
|
const analyticalFailure = analyticalFailureForInput(input, {
|
|
1365
1612
|
error: { code: failure.code, message: failure.reason },
|
|
@@ -1384,7 +1631,7 @@ async function runAnswerLoop(input) {
|
|
|
1384
1631
|
providerUsed: provider.name,
|
|
1385
1632
|
};
|
|
1386
1633
|
}
|
|
1387
|
-
if (input.governedAnalyticalGraphCompilation?.status === 'compiled' && input.analyticalExecutionGraph) {
|
|
1634
|
+
if (!forcedExploratoryTier && input.governedAnalyticalGraphCompilation?.status === 'compiled' && input.analyticalExecutionGraph) {
|
|
1388
1635
|
return executeGovernedRelationalAnalyticalGraph({
|
|
1389
1636
|
input,
|
|
1390
1637
|
compilation: input.governedAnalyticalGraphCompilation,
|
|
@@ -1395,7 +1642,7 @@ async function runAnswerLoop(input) {
|
|
|
1395
1642
|
});
|
|
1396
1643
|
}
|
|
1397
1644
|
const governedRelationalCompilation = input.governedRelationalCompilation;
|
|
1398
|
-
if (governedRelationalCompilation?.status === 'blocked') {
|
|
1645
|
+
if (!forcedExploratoryTier && governedRelationalCompilation?.status === 'blocked') {
|
|
1399
1646
|
const analyticalFailure = analyticalFailureForInput(input, {
|
|
1400
1647
|
error: { code: governedRelationalCompilation.code, message: governedRelationalCompilation.reason },
|
|
1401
1648
|
phase: 'compilation',
|
|
@@ -1422,7 +1669,7 @@ async function runAnswerLoop(input) {
|
|
|
1422
1669
|
providerUsed: provider.name,
|
|
1423
1670
|
};
|
|
1424
1671
|
}
|
|
1425
|
-
if (governedRelationalCompilation?.status === 'compiled') {
|
|
1672
|
+
if (!forcedExploratoryTier && governedRelationalCompilation?.status === 'compiled') {
|
|
1426
1673
|
const dqlArtifact = renderGovernedRelationalDqlArtifact(governedRelationalCompilation);
|
|
1427
1674
|
let result;
|
|
1428
1675
|
let executionError;
|
|
@@ -1486,9 +1733,15 @@ async function runAnswerLoop(input) {
|
|
|
1486
1733
|
const text = clarificationOptions?.length
|
|
1487
1734
|
? `The previous answer listed ${choices.values.length} ${humanizeDeicticDimension(choices.dimension)}. Which one did you mean?`
|
|
1488
1735
|
: `The resolved analytical plan cannot execute safely: ${authoritativePlanBinding.reason}`;
|
|
1736
|
+
const frozenCertifiedPlan = input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1737
|
+
&& input.resolvedAnalyticalPlan.capability === 'certified_execution';
|
|
1738
|
+
const frozenCertifiedRef = input.resolvedAnalyticalPlan?.executionId;
|
|
1489
1739
|
return {
|
|
1490
1740
|
kind: 'no_answer',
|
|
1491
|
-
|
|
1741
|
+
// A binding/adapter failure after the router selected a certified
|
|
1742
|
+
// execution authority is still a certified-tier terminal. It must not
|
|
1743
|
+
// be relabelled as generated merely because no rows were produced.
|
|
1744
|
+
sourceTier: frozenCertifiedPlan ? 'certified_artifact' : 'no_answer',
|
|
1492
1745
|
certification: 'analyst_review_required',
|
|
1493
1746
|
reviewStatus: 'none',
|
|
1494
1747
|
confidence: 0,
|
|
@@ -1500,6 +1753,15 @@ async function runAnswerLoop(input) {
|
|
|
1500
1753
|
: 'grounding_gap',
|
|
1501
1754
|
refusalDetails: { code: authoritativePlanBinding.code, message: authoritativePlanBinding.reason },
|
|
1502
1755
|
...(clarificationOptions?.length ? { clarificationOptions } : {}),
|
|
1756
|
+
...(frozenCertifiedPlan ? {
|
|
1757
|
+
route: {
|
|
1758
|
+
tier: 'certified_block',
|
|
1759
|
+
label: frozenCertifiedRef
|
|
1760
|
+
? `The selected certified block ${frozenCertifiedRef} could not execute its frozen plan.`
|
|
1761
|
+
: 'The selected certified block could not execute its frozen plan.',
|
|
1762
|
+
...(frozenCertifiedRef ? { ref: frozenCertifiedRef } : {}),
|
|
1763
|
+
},
|
|
1764
|
+
} : {}),
|
|
1503
1765
|
citations: contextPackCitations(input.contextPack, 8),
|
|
1504
1766
|
considered,
|
|
1505
1767
|
contextPack: input.contextPack,
|
|
@@ -1536,14 +1798,20 @@ async function runAnswerLoop(input) {
|
|
|
1536
1798
|
const semanticLayerForExec = input.semanticLayer;
|
|
1537
1799
|
const canExecuteSemanticMetricForMatch = input.canExecuteSemanticMetric
|
|
1538
1800
|
?? (semanticLayerForExec ? (name) => semanticLayerForExec.canComposeMetric(name) : undefined);
|
|
1539
|
-
let semanticMetricMatch =
|
|
1540
|
-
?
|
|
1541
|
-
:
|
|
1542
|
-
?
|
|
1543
|
-
:
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1801
|
+
let semanticMetricMatch = forcedExploratoryTier
|
|
1802
|
+
? null
|
|
1803
|
+
: preferredSemanticMetric
|
|
1804
|
+
? { metric: preferredSemanticMetric, score: 1, basis: 'name' }
|
|
1805
|
+
: input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1806
|
+
? null
|
|
1807
|
+
: await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
|
|
1808
|
+
measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
|
|
1809
|
+
...(canExecuteSemanticMetricForMatch ? { canExecute: canExecuteSemanticMetricForMatch } : {}),
|
|
1810
|
+
...(snapshotVectorMetricShortlist(input.contextPack).length
|
|
1811
|
+
? { vectorMetricShortlist: snapshotVectorMetricShortlist(input.contextPack) }
|
|
1812
|
+
: {}),
|
|
1813
|
+
...semanticMetricEmbeddingOptions(input.embeddingProvider),
|
|
1814
|
+
}).catch(() => null);
|
|
1547
1815
|
// Stage 1: certified artifact match. Blocks can be executed; dashboards,
|
|
1548
1816
|
// Apps, and notebooks are returned as governed citations/navigation targets.
|
|
1549
1817
|
const drilldownCertifiedHit = input.followUp?.kind === 'drilldown'
|
|
@@ -1563,8 +1831,10 @@ async function runAnswerLoop(input) {
|
|
|
1563
1831
|
&& objectNameInQuestion(question, node)
|
|
1564
1832
|
&& /\b(run|use|open|show|execute|certified|saved|block)\b/i.test(question));
|
|
1565
1833
|
});
|
|
1566
|
-
const shouldTryCertifiedRoute =
|
|
1567
|
-
|
|
1834
|
+
const shouldTryCertifiedRoute = !forcedExploratoryTier
|
|
1835
|
+
&& (shouldUseCertifiedRoute(catalogRoute, intent)
|
|
1836
|
+
|| questionPlan.mode === 'definition'
|
|
1837
|
+
|| explicitlyRequestedCertifiedBlock);
|
|
1568
1838
|
const catalogCertifiedHit = shouldTryCertifiedRoute
|
|
1569
1839
|
? certifiedHitFromContextPack(input.contextPack, kg)
|
|
1570
1840
|
: null;
|
|
@@ -1574,7 +1844,9 @@ async function runAnswerLoop(input) {
|
|
|
1574
1844
|
// metric, so a high-scoring `top_beverage_customers` catalog hit could answer a
|
|
1575
1845
|
// product-type → product-name flow request with customer rows.
|
|
1576
1846
|
const unsafeCatalogCertifiedHit = catalogCertifiedHit?.node.kind === 'block'
|
|
1577
|
-
&& !hasCertifiedNodeFit(question, questionPlan, catalogCertifiedHit.node
|
|
1847
|
+
&& !hasCertifiedNodeFit(question, questionPlan, catalogCertifiedHit.node, {
|
|
1848
|
+
uniqueExactExampleContract: hasUniqueExactCatalogExample(question, input.contextPack, catalogCertifiedHit.node),
|
|
1849
|
+
})
|
|
1578
1850
|
? null
|
|
1579
1851
|
: catalogCertifiedHit;
|
|
1580
1852
|
const fallbackCertifiedHit = shouldTryCertifiedRoute ? pickCertifiedArtifact({
|
|
@@ -1591,13 +1863,85 @@ async function runAnswerLoop(input) {
|
|
|
1591
1863
|
&& authoritativePlanBinding.kind === 'certified'
|
|
1592
1864
|
? authoritativePlanBinding
|
|
1593
1865
|
: undefined;
|
|
1866
|
+
const namedDefinitionArtifact = input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1867
|
+
? null
|
|
1868
|
+
: uniqueNamedCertifiedDefinitionArtifact(question, kg, authorizedDomains);
|
|
1594
1869
|
let artifactHit = authoritativeCertifiedBinding
|
|
1595
1870
|
? { node: authoritativeCertifiedBinding.node, score: 1 }
|
|
1596
1871
|
: input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1597
1872
|
? null
|
|
1598
|
-
: drilldownCertifiedHit ?? unsafeCatalogCertifiedHit
|
|
1873
|
+
: namedDefinitionArtifact ?? drilldownCertifiedHit ?? unsafeCatalogCertifiedHit
|
|
1599
1874
|
?? (catalogCertifiedHit ? null : fallbackCertifiedHit);
|
|
1600
1875
|
let certifiedExecutionFallback;
|
|
1876
|
+
// A named certified artifact-definition request is documentation about that
|
|
1877
|
+
// artifact, not a request to execute its SQL. The planner strips the
|
|
1878
|
+
// artifact identifier from its output contract, and this deterministic path
|
|
1879
|
+
// returns only authored metadata. It is deliberately narrower than a bare
|
|
1880
|
+
// "what is revenue" question: the user must name the certified block and use
|
|
1881
|
+
// an explicit definition form before execution is bypassed.
|
|
1882
|
+
if (artifactHit?.node.kind === 'block'
|
|
1883
|
+
&& artifactHit.node.status === 'certified'
|
|
1884
|
+
&& isUniqueNamedCertifiedDefinitionQuestion(question, artifactHit.node, kg, authorizedDomains)) {
|
|
1885
|
+
const artifact = artifactHit.node;
|
|
1886
|
+
const outputs = [...new Set([
|
|
1887
|
+
...(artifact.outputContract ?? []).map((output) => output.name),
|
|
1888
|
+
...(artifact.declaredOutputs ?? []),
|
|
1889
|
+
...(artifact.outputs ?? []).map((output) => output.name),
|
|
1890
|
+
].map((output) => output.trim()).filter(Boolean))].slice(0, 16);
|
|
1891
|
+
const metadataLines = [
|
|
1892
|
+
artifact.description ?? artifact.llmContext ?? 'No authored description is available for this certified artifact.',
|
|
1893
|
+
outputs.length > 0 ? `Declared outputs: ${outputs.join(', ')}.` : undefined,
|
|
1894
|
+
artifact.grain ? `Grain: ${artifact.grain}.` : undefined,
|
|
1895
|
+
artifact.owner ? `Owner: ${artifact.owner}.` : undefined,
|
|
1896
|
+
'This is certified artifact metadata, not a query result.',
|
|
1897
|
+
].filter((line) => Boolean(line));
|
|
1898
|
+
const citations = [{
|
|
1899
|
+
nodeId: artifact.nodeId,
|
|
1900
|
+
kind: artifact.kind,
|
|
1901
|
+
name: artifact.name,
|
|
1902
|
+
gitSha: artifact.gitSha,
|
|
1903
|
+
sourceTier: 'certified_artifact',
|
|
1904
|
+
provenance: artifact.provenance,
|
|
1905
|
+
}];
|
|
1906
|
+
const analysisPlan = buildAnalysisPlan({
|
|
1907
|
+
question,
|
|
1908
|
+
intent: 'definition_lookup',
|
|
1909
|
+
routeReason: 'The question explicitly requested the meaning of one named certified artifact; no SQL execution was requested.',
|
|
1910
|
+
selectedNodes: [artifact],
|
|
1911
|
+
schemaContext,
|
|
1912
|
+
});
|
|
1913
|
+
const text = `Certified artifact **${artifact.name}**\n\n${metadataLines.join('\n\n')}`;
|
|
1914
|
+
return {
|
|
1915
|
+
kind: 'certified',
|
|
1916
|
+
sourceTier: 'certified_artifact',
|
|
1917
|
+
certification: 'certified',
|
|
1918
|
+
reviewStatus: 'certified',
|
|
1919
|
+
confidence: 0.99,
|
|
1920
|
+
text,
|
|
1921
|
+
answer: text,
|
|
1922
|
+
block: artifact,
|
|
1923
|
+
sourceCertifiedBlock: artifact.name,
|
|
1924
|
+
trustLabel: 'certified',
|
|
1925
|
+
citations,
|
|
1926
|
+
memoryContext: input.memoryContext,
|
|
1927
|
+
analysisPlan,
|
|
1928
|
+
evidence: buildCertifiedEvidence({
|
|
1929
|
+
question,
|
|
1930
|
+
artifact,
|
|
1931
|
+
businessHits,
|
|
1932
|
+
semanticHits,
|
|
1933
|
+
manifestHits,
|
|
1934
|
+
considered,
|
|
1935
|
+
executorWasAvailable: false,
|
|
1936
|
+
citations,
|
|
1937
|
+
memoryContext: input.memoryContext ?? [],
|
|
1938
|
+
analysisPlan,
|
|
1939
|
+
}),
|
|
1940
|
+
contextPack: input.contextPack,
|
|
1941
|
+
considered,
|
|
1942
|
+
providerUsed: provider.name,
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1601
1945
|
// Certified remains first when it actually covers the question. If the
|
|
1602
1946
|
// retrieved block does not fit but a governed semantic metric does, never
|
|
1603
1947
|
// let the broad catalog match pre-empt Lane 2.
|
|
@@ -1706,7 +2050,16 @@ async function runAnswerLoop(input) {
|
|
|
1706
2050
|
providerUsed: provider.name,
|
|
1707
2051
|
};
|
|
1708
2052
|
}
|
|
1709
|
-
const resultShapeWarnings = result
|
|
2053
|
+
const resultShapeWarnings = result
|
|
2054
|
+
? validateAnswerResultShape(questionPlan, result, {
|
|
2055
|
+
outputBindings: authoritativeCertifiedBinding
|
|
2056
|
+
? frozenCertifiedResultShapeBindings(input.resolvedAnalyticalPlan, artifactHit.node, questionPlan, {
|
|
2057
|
+
uniqueExactExampleContract: hasUniqueExactCatalogExample(question, input.contextPack, artifactHit.node),
|
|
2058
|
+
})
|
|
2059
|
+
: [],
|
|
2060
|
+
requireBoundMeasures: Boolean(authoritativeCertifiedBinding),
|
|
2061
|
+
}).warnings
|
|
2062
|
+
: [];
|
|
1710
2063
|
// When a certified block's execution was ATTEMPTED and FAILED, the answer
|
|
1711
2064
|
// cannot wear the certified badge — a failed run has no data to stand behind.
|
|
1712
2065
|
// Downgrade to analyst_review_required (the error is surfaced in the text).
|
|
@@ -1745,6 +2098,10 @@ async function runAnswerLoop(input) {
|
|
|
1745
2098
|
? undefined
|
|
1746
2099
|
: questionPlan.requestedShape.topN?.n);
|
|
1747
2100
|
const authoritativeCertifiedFailure = Boolean(authoritativeCertifiedBinding && executionError);
|
|
2101
|
+
// A selected certified block which returns an incomplete tuple is a
|
|
2102
|
+
// same-tier terminal failure. It is not a reason to reinterpret meaning,
|
|
2103
|
+
// label the partial rows generated, or retry another route after freeze.
|
|
2104
|
+
const authoritativeCertifiedShapeFailure = Boolean(authoritativeCertifiedBinding && !executionError && resultShapeWarnings.length > 0);
|
|
1748
2105
|
const recoverableCertifiedFailure = artifactHit.node.kind === 'block'
|
|
1749
2106
|
&& executionError !== undefined
|
|
1750
2107
|
&& !authoritativeCertifiedFailure
|
|
@@ -1759,18 +2116,38 @@ async function runAnswerLoop(input) {
|
|
|
1759
2116
|
artifactHit = null;
|
|
1760
2117
|
}
|
|
1761
2118
|
else {
|
|
2119
|
+
const frozenCertifiedRoute = authoritativeCertifiedBinding
|
|
2120
|
+
? {
|
|
2121
|
+
tier: 'certified_block',
|
|
2122
|
+
label: certifiedShapePassed
|
|
2123
|
+
? `Answered from certified block ${artifactHit.node.name}`
|
|
2124
|
+
: `The selected certified block ${artifactHit.node.name} could not complete its frozen output contract.`,
|
|
2125
|
+
ref: artifactHit.node.name,
|
|
2126
|
+
}
|
|
2127
|
+
: undefined;
|
|
1762
2128
|
return {
|
|
1763
|
-
kind: authoritativeCertifiedFailure
|
|
1764
|
-
|
|
2129
|
+
kind: authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
|
|
2130
|
+
? 'no_answer'
|
|
2131
|
+
: certifiedShapePassed ? 'certified' : 'uncertified',
|
|
2132
|
+
// Preserve the selected certified authority even when its own
|
|
2133
|
+
// execution/result contract fails. The failure is terminal and has
|
|
2134
|
+
// review-required trust, but it is not generated SQL.
|
|
2135
|
+
sourceTier,
|
|
1765
2136
|
certification: certifiedShapePassed ? 'certified' : 'analyst_review_required',
|
|
1766
|
-
reviewStatus: authoritativeCertifiedFailure
|
|
2137
|
+
reviewStatus: authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
|
|
2138
|
+
? 'none'
|
|
2139
|
+
: certifiedShapePassed ? 'certified' : 'analyst_review_required',
|
|
1767
2140
|
confidence: certifiedShapePassed ? 0.95 : 0.45,
|
|
1768
2141
|
text,
|
|
1769
2142
|
answer: text,
|
|
1770
2143
|
block: artifactHit.node.kind === 'block' ? artifactHit.node : undefined,
|
|
1771
2144
|
result,
|
|
1772
2145
|
executionError,
|
|
1773
|
-
...(authoritativeCertifiedFailure
|
|
2146
|
+
...(authoritativeCertifiedFailure || authoritativeCertifiedShapeFailure
|
|
2147
|
+
? { refusalCode: 'grounding_gap' }
|
|
2148
|
+
: {}),
|
|
2149
|
+
...(authoritativeCertifiedShapeFailure ? { certifiedResultShapeFailure: true } : {}),
|
|
2150
|
+
...(frozenCertifiedRoute ? { route: frozenCertifiedRoute } : {}),
|
|
1774
2151
|
sql: result?.sql,
|
|
1775
2152
|
dqlArtifact,
|
|
1776
2153
|
trustLabel: certifiedShapePassed ? input.contextPack?.trustLabel ?? 'certified' : 'mixed',
|
|
@@ -1808,7 +2185,7 @@ async function runAnswerLoop(input) {
|
|
|
1808
2185
|
// a governed metric can answer. Match by name + synonyms + measure family + hybrid
|
|
1809
2186
|
// rank over the FTS semantic hits, then ALL metric KG nodes (revenue ⇄
|
|
1810
2187
|
// cumulative_revenue). Certified-first is still preserved (checked above).
|
|
1811
|
-
const clarifyBeforeGeneration = shouldClarifyBeforeGeneration({
|
|
2188
|
+
const clarifyBeforeGeneration = !forcedExploratoryTier && shouldClarifyBeforeGeneration({
|
|
1812
2189
|
intent,
|
|
1813
2190
|
routeDecision: catalogRoute,
|
|
1814
2191
|
hasSemanticMetricMatch: Boolean(semanticMetricMatch),
|
|
@@ -1866,12 +2243,14 @@ async function runAnswerLoop(input) {
|
|
|
1866
2243
|
// wins over raw dbt manifest context; memory is appended last as advisory.
|
|
1867
2244
|
// A confident metric match forces the semantic tier even when FTS returned no
|
|
1868
2245
|
// semantic hits, so the governed metric (not refusal) answers the question.
|
|
1869
|
-
const activeTier =
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
? '
|
|
1874
|
-
:
|
|
2246
|
+
const activeTier = forcedExploratoryTier
|
|
2247
|
+
? 'dbt_manifest'
|
|
2248
|
+
: sourceTierFromContextPack(input.contextPack)
|
|
2249
|
+
?? (semanticHits.length > 0 || semanticMetricMatch
|
|
2250
|
+
? 'semantic_layer'
|
|
2251
|
+
: manifestHits.length > 0
|
|
2252
|
+
? 'dbt_manifest'
|
|
2253
|
+
: 'dbt_manifest');
|
|
1875
2254
|
const reviewRequiredArtifactHits = artifactHits
|
|
1876
2255
|
.filter((hit) => hit.score >= CERTIFIED_HIT_THRESHOLD && !isCertifiedHit(hit, kg))
|
|
1877
2256
|
.slice(0, 4);
|
|
@@ -1904,7 +2283,13 @@ async function runAnswerLoop(input) {
|
|
|
1904
2283
|
{ hits: businessHits.slice(0, 4), reserve: 3 },
|
|
1905
2284
|
{ hits: manifestHits, reserve: 3 },
|
|
1906
2285
|
], 14);
|
|
1907
|
-
|
|
2286
|
+
// A frozen exploratory selection gets its relation/column/proof context only
|
|
2287
|
+
// from `executionContextPack`. KG hit prose is useful diagnostics, but it
|
|
2288
|
+
// is not execution authority and must not tell a provider about another
|
|
2289
|
+
// same-snapshot relation it could then attempt to query.
|
|
2290
|
+
const contextNodes = forcedExploratoryTier
|
|
2291
|
+
? []
|
|
2292
|
+
: mergeNodes(followUpSourceBlock && input.followUp?.kind === 'drilldown' ? [followUpSourceBlock] : [], (contextHits.length > 0 ? contextHits : considered.slice(0, 6)).map((h) => h.node)).filter((node) => questionDomainScope.length === 0 || !node.domain || questionDomainScope.includes(node.domain));
|
|
1908
2293
|
const kgJoinPathHints = buildKgJoinPathHints(kg, contextNodes, questionPlan);
|
|
1909
2294
|
const contextBlocks = contextNodes.filter((node) => {
|
|
1910
2295
|
if (node.kind !== 'block')
|
|
@@ -1924,10 +2309,10 @@ async function runAnswerLoop(input) {
|
|
|
1924
2309
|
const messages = [
|
|
1925
2310
|
{ role: 'system', content: SYSTEM_PROMPT },
|
|
1926
2311
|
];
|
|
1927
|
-
const skillsPrompt = buildSkillsPrompt(selectedSkills, userId ?? null);
|
|
2312
|
+
const skillsPrompt = forcedExploratoryTier ? '' : buildSkillsPrompt(selectedSkills, userId ?? null);
|
|
1928
2313
|
if (skillsPrompt)
|
|
1929
2314
|
messages.push({ role: 'system', content: skillsPrompt });
|
|
1930
|
-
const analyticalPlan = input.manifest
|
|
2315
|
+
const analyticalPlan = !forcedExploratoryTier && input.manifest
|
|
1931
2316
|
? planAnalyticalPath(input.manifest, {
|
|
1932
2317
|
entityIds: inferAnalyticalEntityIds(question, contextNodes, input.manifest),
|
|
1933
2318
|
ownerDomain: input.domainContext?.activeDomain ?? input.domain,
|
|
@@ -2279,7 +2664,7 @@ async function runAnswerLoop(input) {
|
|
|
2279
2664
|
// (pre-validated, since the wrapper only restricts an already-certified result)
|
|
2280
2665
|
// and is labeled BELOW certified. Falls through to generation on any miss.
|
|
2281
2666
|
let certifiedAdaptation;
|
|
2282
|
-
if (!semanticBridgeAnswer && !metricFirst && input.executeGeneratedSql) {
|
|
2667
|
+
if (!forcedExploratoryTier && !semanticBridgeAnswer && !metricFirst && input.executeGeneratedSql) {
|
|
2283
2668
|
const fit = input.contextPack?.routeDecision?.blockFit;
|
|
2284
2669
|
const sourceBlock = input.contextPack?.allowedSqlContext?.sourceBlockSql?.[0];
|
|
2285
2670
|
if (fit && sourceBlock?.sql) {
|
|
@@ -2317,7 +2702,18 @@ async function runAnswerLoop(input) {
|
|
|
2317
2702
|
onCompiled: (record) => compiledSemanticRecords.push(record),
|
|
2318
2703
|
}),
|
|
2319
2704
|
];
|
|
2320
|
-
if (
|
|
2705
|
+
if (input.forcedGeneratedProposal) {
|
|
2706
|
+
// The analyst loop already did its provider/tool work. Re-entering ordinary
|
|
2707
|
+
// generation here would create a second proposal whose SQL is not covered
|
|
2708
|
+
// by the request-scoped execution capability.
|
|
2709
|
+
parsed = {
|
|
2710
|
+
sql: input.forcedGeneratedProposal.sql,
|
|
2711
|
+
text: input.forcedGeneratedProposal.summary
|
|
2712
|
+
?? 'Prepared from identifiers observed during this bounded analyst run. Review-required until an analyst promotes it.',
|
|
2713
|
+
...(input.forcedGeneratedProposal.suggestedViz ? { viz: input.forcedGeneratedProposal.suggestedViz } : {}),
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
else if (semanticBridgeAnswer) {
|
|
2321
2717
|
governedMetricAnswer = true;
|
|
2322
2718
|
parsed = {
|
|
2323
2719
|
sql: semanticBridgeAnswer.sql,
|
|
@@ -2503,7 +2899,7 @@ async function runAnswerLoop(input) {
|
|
|
2503
2899
|
let deepCandidateResult;
|
|
2504
2900
|
let deepCandidateExecutionError;
|
|
2505
2901
|
let deepCandidateNotes = [];
|
|
2506
|
-
if (!governedMetricAnswer && input.analysisDepth === 'deep' && parsed.sql) {
|
|
2902
|
+
if (!input.forcedGeneratedProposal && !governedMetricAnswer && input.analysisDepth === 'deep' && parsed.sql) {
|
|
2507
2903
|
const selection = await selectDeepGeneratedProposalCandidate({
|
|
2508
2904
|
provider,
|
|
2509
2905
|
messages,
|
|
@@ -2705,7 +3101,12 @@ async function runAnswerLoop(input) {
|
|
|
2705
3101
|
};
|
|
2706
3102
|
}
|
|
2707
3103
|
}
|
|
2708
|
-
|
|
3104
|
+
// The analyst capability binds the exact composed bytes. Any projection
|
|
3105
|
+
// tightening is a new SQL proposal and therefore must wait for a fresh
|
|
3106
|
+
// analyst pass/capability rather than silently rewriting this one.
|
|
3107
|
+
const tightenedFlow = input.forcedGeneratedProposal
|
|
3108
|
+
? undefined
|
|
3109
|
+
: tightenSourceTargetFlowProjection(parsed.sql, question, questionPlan);
|
|
2709
3110
|
if (tightenedFlow && tightenedFlow.sql !== parsed.sql) {
|
|
2710
3111
|
parsed.sql = tightenedFlow.sql;
|
|
2711
3112
|
parsed.outputs = tightenedFlow.outputs;
|
|
@@ -2723,7 +3124,7 @@ async function runAnswerLoop(input) {
|
|
|
2723
3124
|
// model emitted to its real warehouse relation from the runtime schema BEFORE
|
|
2724
3125
|
// governance validation. Same resolver the build path uses — one grounding,
|
|
2725
3126
|
// no weak path. `allowedSqlContext` relations are already qualified.
|
|
2726
|
-
if (parsed.sql) {
|
|
3127
|
+
if (parsed.sql && !input.forcedGeneratedProposal) {
|
|
2727
3128
|
parsed.sql = contextLedger.qualifySql(parsed.sql).sql;
|
|
2728
3129
|
}
|
|
2729
3130
|
// Validation gate. Governed metric SQL synthesized from the semantic layer is
|
|
@@ -2840,7 +3241,8 @@ async function runAnswerLoop(input) {
|
|
|
2840
3241
|
// that predicate entirely, inject it deterministically before spending the
|
|
2841
3242
|
// bounded AI validation repair. Misbound or ambiguous predicates are never
|
|
2842
3243
|
// rewritten here; they continue through the guarded correction/refusal path.
|
|
2843
|
-
if (!
|
|
3244
|
+
if (!input.forcedGeneratedProposal
|
|
3245
|
+
&& !contextValidation.ok
|
|
2844
3246
|
&& contextValidation.code === 'misbound_filter'
|
|
2845
3247
|
&& !contextValidation.offending?.relation
|
|
2846
3248
|
&& !contextValidation.offending?.column
|
|
@@ -2892,7 +3294,8 @@ async function runAnswerLoop(input) {
|
|
|
2892
3294
|
// stands instead of guessing a measure.
|
|
2893
3295
|
// 2) Otherwise fall back to a CLEAN governed-metric definition (direct or exact
|
|
2894
3296
|
// leaf-measure; no fuzzy family guess that could answer the wrong measure).
|
|
2895
|
-
if (!
|
|
3297
|
+
if (!input.forcedGeneratedProposal
|
|
3298
|
+
&& !contextValidation.ok && contextValidation.code !== 'unsafe_aggregation' && semanticMetricRoute && semanticMetricMatch && !rankedGrainGap) {
|
|
2896
3299
|
const grounded = contextLedger.validateRuntimeGrounding(parsed.sql);
|
|
2897
3300
|
// Runtime grounding proves that relations and columns exist; it cannot prove
|
|
2898
3301
|
// that a resolved business member was bound to the right dimension. Never
|
|
@@ -2922,7 +3325,8 @@ async function runAnswerLoop(input) {
|
|
|
2922
3325
|
// nothing, or found the table and something else rejected the SQL — which are
|
|
2923
3326
|
// three different fixes.
|
|
2924
3327
|
let regroundDiagnostic;
|
|
2925
|
-
if (!
|
|
3328
|
+
if (!input.forcedGeneratedProposal
|
|
3329
|
+
&& !contextValidation.ok && contextValidation.code !== 'unsafe_aggregation' && !governedMetricAnswer && input.expandGroundingContext && canUseLaneRepair(repairBudgetState, 'reground')) {
|
|
2926
3330
|
recordLaneRepair(repairBudgetState, 'reground');
|
|
2927
3331
|
try {
|
|
2928
3332
|
const expansion = await input.expandGroundingContext({
|
|
@@ -3019,6 +3423,7 @@ async function runAnswerLoop(input) {
|
|
|
3019
3423
|
&& input.resolvedAnalyticalPlan?.mode !== 'authoritative'
|
|
3020
3424
|
&& !semanticAggregationAuthorityBlocked
|
|
3021
3425
|
&& !governedMetricAnswer
|
|
3426
|
+
&& !input.forcedGeneratedProposal
|
|
3022
3427
|
&& canUseLaneRepair(repairBudgetState, 'validation')) {
|
|
3023
3428
|
recordLaneRepair(repairBudgetState, 'validation');
|
|
3024
3429
|
try {
|
|
@@ -3094,7 +3499,7 @@ async function runAnswerLoop(input) {
|
|
|
3094
3499
|
// Business-language chat text; the validator's machine message (with
|
|
3095
3500
|
// relation ids and tool guidance for the repair prompt) stays in
|
|
3096
3501
|
// refusalDetails + validationWarnings for the Inspect surface.
|
|
3097
|
-
const text = renderContextValidationRefusalForUser(contextValidation.code, contextValidation.error, input.followUp?.memberBindings);
|
|
3502
|
+
const text = renderContextValidationRefusalForUser(contextValidation.code, contextValidation.error, input.followUp?.memberBindings, contextValidation.aggregationSafetyProof?.issueCodes);
|
|
3098
3503
|
const analysisPlan = buildAnalysisPlan({
|
|
3099
3504
|
question,
|
|
3100
3505
|
intent,
|
|
@@ -3258,7 +3663,13 @@ async function runAnswerLoop(input) {
|
|
|
3258
3663
|
let result;
|
|
3259
3664
|
let executionError;
|
|
3260
3665
|
let warehouseFailure;
|
|
3666
|
+
let exploratoryClosureDenied = false;
|
|
3261
3667
|
let repairAttempts = 0;
|
|
3668
|
+
// A forced proposal may originate from the bounded analyst loop, but it
|
|
3669
|
+
// cannot bypass a router-owned exploratory decision. In that case the host
|
|
3670
|
+
// still prepares and freezes the exact validated SQL before execution.
|
|
3671
|
+
const exploratoryExecutionSelected = input.selectedCascadeTier === 'exploratory_sql';
|
|
3672
|
+
let preparedExploratoryExecution;
|
|
3262
3673
|
// Repair candidates that were generated but rejected before execution. These
|
|
3263
3674
|
// are diagnostics ABOUT the recovery attempt, not the reason the run failed,
|
|
3264
3675
|
// so they are reported alongside the original error rather than replacing it.
|
|
@@ -3273,7 +3684,7 @@ async function runAnswerLoop(input) {
|
|
|
3273
3684
|
executionError = deepCandidateExecutionError;
|
|
3274
3685
|
warehouseFailure = normalizeWarehouseSqlFailure(deepCandidateExecutionError, input.semanticDriver);
|
|
3275
3686
|
}
|
|
3276
|
-
const executeCurrentSql = () => {
|
|
3687
|
+
const executeCurrentSql = async () => {
|
|
3277
3688
|
const requestedLimit = questionPlan.requestedShape.topN?.scope === 'per_group'
|
|
3278
3689
|
? 200
|
|
3279
3690
|
: questionPlan.requestedShape.topN?.n ?? 200;
|
|
@@ -3283,8 +3694,6 @@ async function runAnswerLoop(input) {
|
|
|
3283
3694
|
&& semanticBridgeAnswer?.sql.trim() === parsed.sql?.trim()) {
|
|
3284
3695
|
return input.executeDqlArtifact({ ...semanticArtifact, limit: requestedLimit });
|
|
3285
3696
|
}
|
|
3286
|
-
if (!input.executeGeneratedSql)
|
|
3287
|
-
throw new Error('No generated SQL executor is configured.');
|
|
3288
3697
|
const artifact = buildGeneratedSqlDqlArtifact({
|
|
3289
3698
|
question,
|
|
3290
3699
|
sql: parsed.sql,
|
|
@@ -3300,9 +3709,35 @@ async function runAnswerLoop(input) {
|
|
|
3300
3709
|
validationWarnings: [...contextValidation.warnings, ...deepCandidateNotes],
|
|
3301
3710
|
outputs: parsed.outputs,
|
|
3302
3711
|
});
|
|
3303
|
-
|
|
3712
|
+
const boundedArtifact = artifact ? { ...artifact, limit: requestedLimit } : undefined;
|
|
3713
|
+
if (input.forcedGeneratedProposal && !exploratoryExecutionSelected) {
|
|
3714
|
+
if (!input.agenticSqlExecutionCapability || !input.executeAgenticGeneratedSql) {
|
|
3715
|
+
throw Object.assign(new Error('The analyst-approved SQL has no live execution capability, so DQL did not execute it.'), {
|
|
3716
|
+
code: 'AGENTIC_EXECUTION_CAPABILITY_REQUIRED',
|
|
3717
|
+
});
|
|
3718
|
+
}
|
|
3719
|
+
return input.executeAgenticGeneratedSql(input.agenticSqlExecutionCapability, parsed.sql, boundedArtifact);
|
|
3720
|
+
}
|
|
3721
|
+
if (exploratoryExecutionSelected) {
|
|
3722
|
+
if (!input.prepareExploratorySqlExecution || !input.executeAgenticGeneratedSql) {
|
|
3723
|
+
throw Object.assign(new Error('The selected exploratory SQL has no host-authorized execution capability, so DQL did not execute it.'), {
|
|
3724
|
+
code: 'EXPLORATORY_EXECUTION_CAPABILITY_REQUIRED',
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
// This is the only point at which a router-selected exploratory proposal
|
|
3728
|
+
// crosses from validated text into executable authority. The callback
|
|
3729
|
+
// rechecks the snapshot, physical target, and qualified runtime columns,
|
|
3730
|
+
// then returns a one-shot capability bound to these exact SQL bytes.
|
|
3731
|
+
preparedExploratoryExecution ??= await input.prepareExploratorySqlExecution(parsed.sql, boundedArtifact);
|
|
3732
|
+
return input.executeAgenticGeneratedSql(preparedExploratoryExecution.capability, parsed.sql, boundedArtifact);
|
|
3733
|
+
}
|
|
3734
|
+
if (!input.executeGeneratedSql)
|
|
3735
|
+
throw new Error('No generated SQL executor is configured.');
|
|
3736
|
+
return input.executeGeneratedSql(parsed.sql, boundedArtifact);
|
|
3304
3737
|
};
|
|
3305
|
-
if (input.executeGeneratedSql
|
|
3738
|
+
if ((input.executeGeneratedSql
|
|
3739
|
+
|| (input.forcedGeneratedProposal && input.executeAgenticGeneratedSql)
|
|
3740
|
+
|| exploratoryExecutionSelected) && !result) {
|
|
3306
3741
|
// Fanout gate for native semantic direct-joins: a duplicate join-key row on
|
|
3307
3742
|
// the joined side multiplies fact rows BEFORE aggregation, so every summed
|
|
3308
3743
|
// value inflates silently (seen in the field as trillions-scale "governed"
|
|
@@ -3311,6 +3746,9 @@ async function runAnswerLoop(input) {
|
|
|
3311
3746
|
// repair, which would regenerate the same multiplying join.
|
|
3312
3747
|
let fanoutContradiction = false;
|
|
3313
3748
|
if (!executionError
|
|
3749
|
+
&& !input.forcedGeneratedProposal
|
|
3750
|
+
&& !exploratoryExecutionSelected
|
|
3751
|
+
&& input.executeGeneratedSql
|
|
3314
3752
|
&& semanticBridgeAnswer?.composeResult?.fanoutProbeSql
|
|
3315
3753
|
&& semanticBridgeAnswer.sql.trim() === parsed.sql?.trim()) {
|
|
3316
3754
|
const inflationError = await probeSemanticJoinFanout(semanticBridgeAnswer.composeResult.fanoutProbeSql, semanticBridgeAnswer.composeResult.tables, input.executeGeneratedSql);
|
|
@@ -3324,10 +3762,24 @@ async function runAnswerLoop(input) {
|
|
|
3324
3762
|
result = await executeCurrentSql();
|
|
3325
3763
|
}
|
|
3326
3764
|
catch (err) {
|
|
3765
|
+
const executionCode = err && typeof err === 'object'
|
|
3766
|
+
&& typeof err.code === 'string'
|
|
3767
|
+
? err.code
|
|
3768
|
+
: undefined;
|
|
3769
|
+
// A router-selected exploratory proposal that leaves its candidate
|
|
3770
|
+
// closure is not an executable draft. It is a pre-capability safety
|
|
3771
|
+
// denial: do not present its SQL/artifact as review-required work after
|
|
3772
|
+
// the host rejected it.
|
|
3773
|
+
exploratoryClosureDenied = exploratoryExecutionSelected
|
|
3774
|
+
&& executionCode === 'UNAUTHORIZED_SQL';
|
|
3327
3775
|
warehouseFailure = normalizeWarehouseSqlFailure(err, input.semanticDriver);
|
|
3328
3776
|
executionError = warehouseFailure.redactedMessage;
|
|
3329
3777
|
}
|
|
3330
|
-
if (executionError
|
|
3778
|
+
if (executionError
|
|
3779
|
+
&& !input.forcedGeneratedProposal
|
|
3780
|
+
&& !exploratoryExecutionSelected
|
|
3781
|
+
&& !fanoutContradiction
|
|
3782
|
+
&& !authoritativeSemanticBinding) {
|
|
3331
3783
|
warehouseFailure ??= normalizeWarehouseSqlFailure(executionError, input.semanticDriver);
|
|
3332
3784
|
if (isRetryableGeneratedSqlError(warehouseFailure)) {
|
|
3333
3785
|
const localRepairSql = repairGeneratedSqlLocally(parsed.sql, executionError, schemaContext);
|
|
@@ -3438,10 +3890,15 @@ async function runAnswerLoop(input) {
|
|
|
3438
3890
|
: 'The question asks for a custom analysis, ranking, breakdown, comparison, or grain that should not be answered by a loose certified block match.'),
|
|
3439
3891
|
selectedNodes: contextNodes,
|
|
3440
3892
|
schemaContext,
|
|
3441
|
-
|
|
3893
|
+
// A router-selected physical-closure denial happens before a capability
|
|
3894
|
+
// exists. Its rejected bytes are diagnostics for the host, never a SQL
|
|
3895
|
+
// preview that a user can review, copy, or turn into a draft.
|
|
3896
|
+
sql: exploratoryClosureDenied ? undefined : parsed.sql,
|
|
3442
3897
|
suggestedViz: parsed.viz ?? 'table',
|
|
3443
3898
|
assumptions: [
|
|
3444
|
-
|
|
3899
|
+
...(exploratoryClosureDenied
|
|
3900
|
+
? ['The router-selected physical closure rejected the generated SQL before capability minting.']
|
|
3901
|
+
: ['The SQL preview is uncertified until an analyst reviews and promotes the DQL artifact.']),
|
|
3445
3902
|
...(certifiedExecutionFallback
|
|
3446
3903
|
? [`Certified block ${certifiedExecutionFallback.node.name} failed execution and was bypassed: ${certifiedExecutionFallback.error}`]
|
|
3447
3904
|
: []),
|
|
@@ -3493,21 +3950,23 @@ async function runAnswerLoop(input) {
|
|
|
3493
3950
|
const generatedOutputs = parsed.outputs?.length ? parsed.outputs : resultColumnNames(result);
|
|
3494
3951
|
const generatedRequestedFilters = mergeProposalStringLists(input.followUp?.filters, parsed.requestedFilters);
|
|
3495
3952
|
const generatedRequestedDimensions = mergeProposalStringLists(input.followUp?.dimensions, parsed.requestedDimensions);
|
|
3496
|
-
const baseDqlArtifact =
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3953
|
+
const baseDqlArtifact = exploratoryClosureDenied
|
|
3954
|
+
? undefined
|
|
3955
|
+
: result?.dqlArtifact ?? semanticBridgeAnswer?.dqlArtifact ?? buildGeneratedSqlDqlArtifact({
|
|
3956
|
+
question,
|
|
3957
|
+
sql: parsed.sql,
|
|
3958
|
+
intent,
|
|
3959
|
+
domain,
|
|
3960
|
+
followUp: input.followUp,
|
|
3961
|
+
contextPack: input.contextPack,
|
|
3962
|
+
sourceBlock: followUpSourceBlock ?? undefined,
|
|
3963
|
+
sourceDqlArtifact: input.followUp?.priorDqlArtifact,
|
|
3964
|
+
proposedEntity: parsed.proposedEntity,
|
|
3965
|
+
requestedFilters: generatedRequestedFilters,
|
|
3966
|
+
requestedDimensions: generatedRequestedDimensions,
|
|
3967
|
+
validationWarnings,
|
|
3968
|
+
outputs: generatedOutputs,
|
|
3969
|
+
});
|
|
3511
3970
|
const requestedTopN = questionPlan.requestedShape.topN?.scope === 'per_group'
|
|
3512
3971
|
? undefined
|
|
3513
3972
|
: questionPlan.requestedShape.topN?.n;
|
|
@@ -3520,7 +3979,7 @@ async function runAnswerLoop(input) {
|
|
|
3520
3979
|
: undefined;
|
|
3521
3980
|
let draftBlock;
|
|
3522
3981
|
let draftCaptureError;
|
|
3523
|
-
if (input.captureGeneratedDraft && parsed.sql) {
|
|
3982
|
+
if (!exploratoryClosureDenied && input.captureGeneratedDraft && parsed.sql) {
|
|
3524
3983
|
try {
|
|
3525
3984
|
draftBlock = await input.captureGeneratedDraft({
|
|
3526
3985
|
question,
|
|
@@ -3567,32 +4026,38 @@ async function runAnswerLoop(input) {
|
|
|
3567
4026
|
: undefined;
|
|
3568
4027
|
const certifiedMetricAnswer = semanticMetricCertification === 'certified' || semanticMetricCertification === 'reviewed';
|
|
3569
4028
|
const governedMetricExecutionFailure = governedMetricAnswer && Boolean(executionError);
|
|
4029
|
+
const terminalExecutionFailure = governedMetricExecutionFailure || exploratoryClosureDenied;
|
|
3570
4030
|
const finalSemanticExecutionTrace = semanticTraceAfterExecution(semanticExecutionTrace, {
|
|
3571
4031
|
executed: Boolean(result),
|
|
3572
4032
|
...(executionError ? { error: executionError } : {}),
|
|
3573
4033
|
...(result ? { result } : {}),
|
|
3574
4034
|
});
|
|
3575
4035
|
return {
|
|
3576
|
-
kind:
|
|
3577
|
-
sourceTier:
|
|
3578
|
-
certification:
|
|
3579
|
-
reviewStatus:
|
|
4036
|
+
kind: terminalExecutionFailure ? 'no_answer' : 'uncertified',
|
|
4037
|
+
sourceTier: terminalExecutionFailure ? 'no_answer' : governedMetricAnswer ? 'semantic_layer' : activeTier,
|
|
4038
|
+
certification: terminalExecutionFailure ? 'analyst_review_required' : governedMetricAnswer ? 'governed' : 'ai_generated',
|
|
4039
|
+
reviewStatus: terminalExecutionFailure ? 'none' : governedMetricAnswer ? 'governed' : 'draft_ready',
|
|
3580
4040
|
semanticMetricCertification,
|
|
3581
|
-
confidence:
|
|
3582
|
-
text:
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
4041
|
+
confidence: terminalExecutionFailure ? 0 : certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
|
|
4042
|
+
text: exploratoryClosureDenied
|
|
4043
|
+
? 'The generated SQL referenced a relation outside the router-selected physical closure, so DQL did not execute it.'
|
|
4044
|
+
: generatedText,
|
|
4045
|
+
answer: exploratoryClosureDenied
|
|
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 }),
|
|
3586
4049
|
result,
|
|
3587
4050
|
executionError,
|
|
3588
4051
|
...(warehouseFailure ? { warehouseFailure } : {}),
|
|
3589
4052
|
...(finalSemanticExecutionTrace ? { semanticExecutionTrace: finalSemanticExecutionTrace } : {}),
|
|
3590
|
-
...(
|
|
4053
|
+
...(terminalExecutionFailure ? { refusalCode: 'grounding_gap' } : {}),
|
|
3591
4054
|
suggestedViz: parsed.viz ?? 'table',
|
|
3592
|
-
dqlArtifact: answerDqlArtifact,
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
4055
|
+
...(exploratoryClosureDenied ? {} : { dqlArtifact: answerDqlArtifact }),
|
|
4056
|
+
...(exploratoryClosureDenied ? {} : {
|
|
4057
|
+
draftBlock,
|
|
4058
|
+
draftBlockId: draftBlock?.path,
|
|
4059
|
+
promoteCommand: draftBlock ? `dql certify --from-draft ${draftBlock.path}` : undefined,
|
|
4060
|
+
}),
|
|
3596
4061
|
trustLabel: input.contextPack?.trustLabel,
|
|
3597
4062
|
sourceCertifiedBlock,
|
|
3598
4063
|
contextPackId: input.contextPack?.id,
|
|
@@ -3630,6 +4095,7 @@ async function runAnswerLoop(input) {
|
|
|
3630
4095
|
considered,
|
|
3631
4096
|
providerUsed: provider.name,
|
|
3632
4097
|
aggregationSafetyProof,
|
|
4098
|
+
...(preparedExploratoryExecution ? { exploratoryExecutionFreeze: preparedExploratoryExecution.freeze } : {}),
|
|
3633
4099
|
// Carry the governed metric match so the exit point can name a
|
|
3634
4100
|
// `semantic_metric` route (spec 17, part C).
|
|
3635
4101
|
_semanticMetricMatch: governedMetricAnswer ? semanticMetricMatch ?? undefined : undefined,
|
|
@@ -4718,6 +5184,147 @@ function scopeContextPackToQuestionDomains(contextPack, domains, manifest) {
|
|
|
4718
5184
|
},
|
|
4719
5185
|
};
|
|
4720
5186
|
}
|
|
5187
|
+
/**
|
|
5188
|
+
* Make the router-selected exploratory physical closure the only SQL authority
|
|
5189
|
+
* visible to generation and execution. The broad context pack remains on the
|
|
5190
|
+
* run as diagnostics, but must not make a same-snapshot neighbouring relation
|
|
5191
|
+
* executable merely because it ranked well for the surrounding question.
|
|
5192
|
+
*
|
|
5193
|
+
* Matching is canonical-ID only. In particular this deliberately does not
|
|
5194
|
+
* fall back to relation/model leaf names: duplicate `orders` relations across
|
|
5195
|
+
* schemas/domains must not inherit each other's proof.
|
|
5196
|
+
*/
|
|
5197
|
+
export function scopeContextPackToExploratoryCandidateClosure(contextPack, candidateIds) {
|
|
5198
|
+
if (!contextPack || !candidateIds?.length)
|
|
5199
|
+
return undefined;
|
|
5200
|
+
const requestedIds = new Set(candidateIds.map(normalizeExploratoryCandidateIdentity).filter(Boolean));
|
|
5201
|
+
if (requestedIds.size === 0)
|
|
5202
|
+
return undefined;
|
|
5203
|
+
const intersects = (values) => {
|
|
5204
|
+
for (const value of values) {
|
|
5205
|
+
const normalized = normalizeExploratoryCandidateIdentity(value ?? '');
|
|
5206
|
+
if (normalized && requestedIds.has(normalized))
|
|
5207
|
+
return true;
|
|
5208
|
+
}
|
|
5209
|
+
return false;
|
|
5210
|
+
};
|
|
5211
|
+
const objectIdentities = (object) => {
|
|
5212
|
+
const payload = object.payload ?? {};
|
|
5213
|
+
return [
|
|
5214
|
+
object.objectKey,
|
|
5215
|
+
object.fullName,
|
|
5216
|
+
payload.qualifiedId,
|
|
5217
|
+
payload.uniqueId,
|
|
5218
|
+
payload.id,
|
|
5219
|
+
payload.localId,
|
|
5220
|
+
payload.relation,
|
|
5221
|
+
payload.table,
|
|
5222
|
+
payload.model,
|
|
5223
|
+
...metadataStringValues(payload.sourceObjects),
|
|
5224
|
+
...metadataStringValues(payload.sourceRelations),
|
|
5225
|
+
...metadataStringValues(payload.tableDependencies),
|
|
5226
|
+
].filter((value) => typeof value === 'string');
|
|
5227
|
+
};
|
|
5228
|
+
const objectRelationReferences = (object) => {
|
|
5229
|
+
const payload = object.payload ?? {};
|
|
5230
|
+
return [
|
|
5231
|
+
payload.relation,
|
|
5232
|
+
payload.table,
|
|
5233
|
+
payload.model,
|
|
5234
|
+
...metadataStringValues(payload.sourceObjects),
|
|
5235
|
+
...metadataStringValues(payload.sourceRelations),
|
|
5236
|
+
...metadataStringValues(payload.tableDependencies),
|
|
5237
|
+
].filter((value) => typeof value === 'string');
|
|
5238
|
+
};
|
|
5239
|
+
const directObjects = contextPack.objects.filter((object) => intersects(objectIdentities(object)));
|
|
5240
|
+
const requestedRelations = new Set();
|
|
5241
|
+
for (const object of directObjects) {
|
|
5242
|
+
for (const relation of objectRelationReferences(object)) {
|
|
5243
|
+
const normalized = normalizeExploratoryCandidateIdentity(relation);
|
|
5244
|
+
if (normalized)
|
|
5245
|
+
requestedRelations.add(normalized);
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
for (const relation of contextPack.allowedSqlContext.relations) {
|
|
5249
|
+
if (intersects([relation.objectKey, relation.relation])) {
|
|
5250
|
+
requestedRelations.add(normalizeExploratoryCandidateIdentity(relation.relation));
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
const relations = contextPack.allowedSqlContext.relations.filter((relation) => {
|
|
5254
|
+
const normalized = normalizeExploratoryCandidateIdentity(relation.relation);
|
|
5255
|
+
return requestedIds.has(normalized)
|
|
5256
|
+
|| requestedRelations.has(normalized)
|
|
5257
|
+
|| Boolean(relation.objectKey && directObjects.some((object) => object.objectKey === relation.objectKey));
|
|
5258
|
+
});
|
|
5259
|
+
// A router-selected exploratory attempt without a physical relation is not
|
|
5260
|
+
// executable. Returning undefined is intentionally fail-closed; callers may
|
|
5261
|
+
// preserve the full pack for receipts but may not use it as SQL authority.
|
|
5262
|
+
if (relations.length === 0)
|
|
5263
|
+
return undefined;
|
|
5264
|
+
const relationIds = new Set(relations.map((relation) => normalizeExploratoryCandidateIdentity(relation.relation)));
|
|
5265
|
+
const relationshipEndpointIds = new Set();
|
|
5266
|
+
for (const object of directObjects.filter((object) => object.objectType === 'relationship')) {
|
|
5267
|
+
const payload = object.payload ?? {};
|
|
5268
|
+
for (const endpoint of [payload.from, payload.to]) {
|
|
5269
|
+
if (typeof endpoint === 'string') {
|
|
5270
|
+
const normalized = normalizeExploratoryCandidateIdentity(endpoint);
|
|
5271
|
+
if (normalized)
|
|
5272
|
+
relationshipEndpointIds.add(normalized);
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
const objects = contextPack.objects.filter((object) => {
|
|
5277
|
+
if (directObjects.includes(object))
|
|
5278
|
+
return true;
|
|
5279
|
+
const identities = objectIdentities(object).map(normalizeExploratoryCandidateIdentity);
|
|
5280
|
+
if (identities.some((identity) => relationIds.has(identity) || relationshipEndpointIds.has(identity)))
|
|
5281
|
+
return true;
|
|
5282
|
+
return objectRelationReferences(object)
|
|
5283
|
+
.map(normalizeExploratoryCandidateIdentity)
|
|
5284
|
+
.some((identity) => relationIds.has(identity));
|
|
5285
|
+
});
|
|
5286
|
+
const objectKeys = new Set(objects.map((object) => object.objectKey));
|
|
5287
|
+
const relationKeys = new Set(relations.map((relation) => normalizeRelationKey(relation.relation)));
|
|
5288
|
+
const selectedRelations = contextPack.retrievalDiagnostics.selectedRelations?.filter((relation) => relationKeys.has(normalizeRelationKey(relation.relation)));
|
|
5289
|
+
const selectedJoinPaths = contextPack.retrievalDiagnostics.selectedJoinPaths?.filter((path) => relationKeys.has(normalizeRelationKey(path.leftRelation))
|
|
5290
|
+
&& relationKeys.has(normalizeRelationKey(path.rightRelation)));
|
|
5291
|
+
return {
|
|
5292
|
+
...contextPack,
|
|
5293
|
+
focusObjectKey: contextPack.focusObjectKey && objectKeys.has(contextPack.focusObjectKey)
|
|
5294
|
+
? contextPack.focusObjectKey
|
|
5295
|
+
: objects[0]?.objectKey ?? null,
|
|
5296
|
+
objects,
|
|
5297
|
+
skills: [],
|
|
5298
|
+
edges: contextPack.edges.filter((edge) => objectKeys.has(edge.fromKey) && objectKeys.has(edge.toKey)),
|
|
5299
|
+
citations: contextPack.citations.filter((citation) => objectKeys.has(citation.objectKey)),
|
|
5300
|
+
evidenceSummaries: contextPack.evidenceSummaries.filter((summary) => !summary.objectKey || objectKeys.has(summary.objectKey)),
|
|
5301
|
+
evidenceRoles: contextPack.evidenceRoles.filter((role) => objectKeys.has(role.objectKey)),
|
|
5302
|
+
allowedSqlContext: {
|
|
5303
|
+
relations,
|
|
5304
|
+
// Authored block SQL is not a physical exploratory authority. The
|
|
5305
|
+
// selected dbt/runtime relation closure above is the whole prompt/SQL
|
|
5306
|
+
// boundary for this tier.
|
|
5307
|
+
sourceBlockSql: [],
|
|
5308
|
+
},
|
|
5309
|
+
retrievalDiagnostics: {
|
|
5310
|
+
...contextPack.retrievalDiagnostics,
|
|
5311
|
+
selectedObjects: objects.length,
|
|
5312
|
+
selectedEvidence: contextPack.retrievalDiagnostics.selectedEvidence.filter((evidence) => objectKeys.has(evidence.objectKey)),
|
|
5313
|
+
selectedRelations,
|
|
5314
|
+
selectedJoinPaths,
|
|
5315
|
+
schemaShapeCandidates: contextPack.retrievalDiagnostics.schemaShapeCandidates?.filter((candidate) => objectKeys.has(candidate.objectKey)),
|
|
5316
|
+
certifiedCandidateFits: contextPack.retrievalDiagnostics.certifiedCandidateFits.filter((candidate) => objectKeys.has(candidate.objectKey)),
|
|
5317
|
+
},
|
|
5318
|
+
};
|
|
5319
|
+
}
|
|
5320
|
+
function metadataStringValues(value) {
|
|
5321
|
+
return Array.isArray(value)
|
|
5322
|
+
? value.filter((item) => typeof item === 'string')
|
|
5323
|
+
: [];
|
|
5324
|
+
}
|
|
5325
|
+
function normalizeExploratoryCandidateIdentity(value) {
|
|
5326
|
+
return value.trim().replace(/["`\[\]]/g, '').replace(/\s*\.\s*/g, '.').toLowerCase();
|
|
5327
|
+
}
|
|
4721
5328
|
/**
|
|
4722
5329
|
* Build the prompt-facing evidence pack after enforcing the certified output
|
|
4723
5330
|
* contract. Incompatible blocks remain in the returned answer's `considered`
|
|
@@ -4733,7 +5340,13 @@ function contextPackForRequestedShape(contextPack, question, questionPlan, kg) {
|
|
|
4733
5340
|
const blockName = object.name || object.objectKey.replace(/^dql:block:/, '');
|
|
4734
5341
|
const node = kg.getNode(`block:${blockName}`);
|
|
4735
5342
|
if (node?.kind === 'block') {
|
|
4736
|
-
const fit = evaluateCertifiedBlockFit({
|
|
5343
|
+
const fit = evaluateCertifiedBlockFit({
|
|
5344
|
+
question,
|
|
5345
|
+
plan: questionPlan,
|
|
5346
|
+
block: node,
|
|
5347
|
+
exactExampleMatch: hasUniqueExactCatalogExample(question, contextPack, node),
|
|
5348
|
+
uniqueExactExampleContract: hasUniqueExactCatalogExample(question, contextPack, node),
|
|
5349
|
+
});
|
|
4737
5350
|
if (fit.kind === 'context_only' || fit.kind === 'not_applicable') {
|
|
4738
5351
|
incompatibleBlockKeys.add(object.objectKey);
|
|
4739
5352
|
}
|
|
@@ -5251,15 +5864,26 @@ function renderContextPackForPrompt(contextPack, budget) {
|
|
|
5251
5864
|
const warnings = contextPack.warnings.length
|
|
5252
5865
|
? `Warnings:\n${contextPack.warnings.slice(0, budget.warningLimit).map((warning) => `- ${warning}`).join('\n')}\n`
|
|
5253
5866
|
: '';
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5867
|
+
// Pack by value per token instead of truncating at a fixed position. The
|
|
5868
|
+
// objects arrive already ranked, so position IS the value — but cost is not
|
|
5869
|
+
// uniform: one object with a long description can crowd out several cheap
|
|
5870
|
+
// ones that rank just below it, and the old `slice` cut by rank alone. This
|
|
5871
|
+
// keeps the same ordering pressure while letting an inexpensive object that
|
|
5872
|
+
// ranked 19th still fit. The budget is derived from the existing item limit
|
|
5873
|
+
// so the window stays the size the caller already tuned for.
|
|
5874
|
+
const objects = packContext(contextPack.objects.map((object, index) => ({
|
|
5875
|
+
id: object.objectKey,
|
|
5876
|
+
score: contextPack.objects.length - index,
|
|
5877
|
+
render: () => {
|
|
5878
|
+
const detail = [
|
|
5879
|
+
object.objectType,
|
|
5880
|
+
object.domain ? `domain: ${object.domain}` : '',
|
|
5881
|
+
object.status ? `status: ${object.status}` : '',
|
|
5882
|
+
object.description ? `description: ${object.description}` : '',
|
|
5883
|
+
].filter(Boolean).join('; ');
|
|
5884
|
+
return `- ${object.objectKey} (${detail})`;
|
|
5885
|
+
},
|
|
5886
|
+
})), Math.max(1, budget.contextObjectLimit) * 45).text;
|
|
5263
5887
|
const conflicts = contextPack.retrievalDiagnostics.candidateConflicts.length
|
|
5264
5888
|
? `\nCandidate conflicts:\n${contextPack.retrievalDiagnostics.candidateConflicts.slice(0, 4).map((conflict) => `- ${conflict.reason} ${conflict.prompt}`).join('\n')}`
|
|
5265
5889
|
: '';
|
|
@@ -5275,7 +5899,6 @@ function renderContextPackForPrompt(contextPack, budget) {
|
|
|
5275
5899
|
: '';
|
|
5276
5900
|
const sourceSql = renderSourceBlockSqlContext(contextPack, budget);
|
|
5277
5901
|
return [
|
|
5278
|
-
`context_pack_id: ${contextPack.id}`,
|
|
5279
5902
|
`trust_label: ${contextPack.trustLabel}`,
|
|
5280
5903
|
contextPack.trustLabelInfo ? `trust_label_canonical: ${contextPack.trustLabelInfo.display}` : '',
|
|
5281
5904
|
renderDomainBriefingForPrompt(contextPack).trim(),
|
|
@@ -5569,6 +6192,72 @@ function certifiedHitFromContextPack(contextPack, kg) {
|
|
|
5569
6192
|
const node = nodeId ? kg.getNode(nodeId) : null;
|
|
5570
6193
|
return node ? { node, score: 1, snippet: object.snippet } : null;
|
|
5571
6194
|
}
|
|
6195
|
+
/**
|
|
6196
|
+
* Preserve the catalog's unique exact-example proof when the answer boundary
|
|
6197
|
+
* rechecks the frozen block. The context pack is the same retrieval snapshot
|
|
6198
|
+
* that produced `exactObjectKey`; broad KG search is intentionally not used to
|
|
6199
|
+
* manufacture example uniqueness.
|
|
6200
|
+
*/
|
|
6201
|
+
function hasUniqueExactCatalogExample(question, contextPack, node) {
|
|
6202
|
+
const normalizedQuestion = normalizeQuestion(question);
|
|
6203
|
+
if (!normalizedQuestion || !contextPack)
|
|
6204
|
+
return false;
|
|
6205
|
+
const matches = contextPack.objects.filter((object) => {
|
|
6206
|
+
if (object.objectType !== 'dql_block')
|
|
6207
|
+
return false;
|
|
6208
|
+
if (object.status !== 'certified' && object.status !== 'approved' && object.payload?.certification !== 'certified')
|
|
6209
|
+
return false;
|
|
6210
|
+
const examples = Array.isArray(object.payload?.examples) ? object.payload.examples : [];
|
|
6211
|
+
return examples.some((example) => example
|
|
6212
|
+
&& typeof example === 'object'
|
|
6213
|
+
&& normalizeQuestion(String(example.question ?? '')) === normalizedQuestion);
|
|
6214
|
+
});
|
|
6215
|
+
return matches.length === 1 && normalizeQuestion(matches[0].name) === normalizeQuestion(node.name);
|
|
6216
|
+
}
|
|
6217
|
+
/**
|
|
6218
|
+
* A metadata-only definition answer is safe only for one certified artifact
|
|
6219
|
+
* named exactly by the user. Do not let FTS ranking decide this: a generic
|
|
6220
|
+
* definition phrase must never select a neighbouring block. Payload aliases
|
|
6221
|
+
* are authored block aliases; descriptions and tags are deliberately excluded.
|
|
6222
|
+
*/
|
|
6223
|
+
function uniqueNamedCertifiedDefinitionArtifact(question, kg, authorizedDomains) {
|
|
6224
|
+
const subject = namedArtifactDefinitionSubject(question);
|
|
6225
|
+
if (!subject)
|
|
6226
|
+
return null;
|
|
6227
|
+
const matches = kg.getNodesByKind('block', 1000).filter((node) => {
|
|
6228
|
+
if (node.status !== 'certified')
|
|
6229
|
+
return false;
|
|
6230
|
+
if (authorizedDomains.length > 0 && node.domain && !authorizedDomains.includes(node.domain))
|
|
6231
|
+
return false;
|
|
6232
|
+
return certifiedArtifactNames(node).some((name) => normalizeQuestion(name) === subject);
|
|
6233
|
+
});
|
|
6234
|
+
return matches.length === 1 ? { node: matches[0], score: 1, snippet: undefined } : null;
|
|
6235
|
+
}
|
|
6236
|
+
function isUniqueNamedCertifiedDefinitionQuestion(question, node, kg, authorizedDomains) {
|
|
6237
|
+
const selected = uniqueNamedCertifiedDefinitionArtifact(question, kg, authorizedDomains);
|
|
6238
|
+
return selected?.node.nodeId === node.nodeId;
|
|
6239
|
+
}
|
|
6240
|
+
function namedArtifactDefinitionSubject(question) {
|
|
6241
|
+
const match = /^\s*what\s+does\s+(?:the\s+)?(.+?)\s+(?:measure|mean|define|represent)\s*[?.!]*\s*$/i.exec(question);
|
|
6242
|
+
if (!match?.[1])
|
|
6243
|
+
return undefined;
|
|
6244
|
+
// Remove an optional explicit artifact noun while preserving the exact
|
|
6245
|
+
// authored identifier. `top_customers block` and `top_customers` therefore
|
|
6246
|
+
// resolve identically, while arbitrary definition questions have no subject
|
|
6247
|
+
// match and remain on the normal route.
|
|
6248
|
+
const subject = match[1]
|
|
6249
|
+
.replace(/\b(?:certified\s+)?(?:block|artifact|model|metric)\b/gi, ' ')
|
|
6250
|
+
.replace(/\s+/g, ' ')
|
|
6251
|
+
.trim();
|
|
6252
|
+
const normalized = normalizeQuestion(subject);
|
|
6253
|
+
return normalized || undefined;
|
|
6254
|
+
}
|
|
6255
|
+
function certifiedArtifactNames(node) {
|
|
6256
|
+
const aliases = Array.isArray(node.payload?.aliases)
|
|
6257
|
+
? node.payload.aliases.filter((alias) => typeof alias === 'string')
|
|
6258
|
+
: [];
|
|
6259
|
+
return [node.name, ...aliases];
|
|
6260
|
+
}
|
|
5572
6261
|
/**
|
|
5573
6262
|
* Reduce a possibly-composed prompt to the user's actual question for display.
|
|
5574
6263
|
* Callers (e.g. the Build-SQL-draft modal) sometimes prepend a multi-line instruction
|
|
@@ -5785,6 +6474,28 @@ function flowFieldCovers(field, term) {
|
|
|
5785
6474
|
function normalizeFlowField(value) {
|
|
5786
6475
|
return value.toLowerCase().replace(/[_-]+/g, ' ').replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
5787
6476
|
}
|
|
6477
|
+
/**
|
|
6478
|
+
* Remove the model's QUERY PLAN scaffolding from user-facing prose.
|
|
6479
|
+
*
|
|
6480
|
+
* Rule 4 of the SQL prompt tells the model to state its grain, measures, join
|
|
6481
|
+
* path, and join keys BEFORE writing SQL. That instruction is load-bearing —
|
|
6482
|
+
* it is what prevents wrong-grain answers and fan-out joins — but it is
|
|
6483
|
+
* reasoning, not an answer, and it was reaching the reader verbatim:
|
|
6484
|
+
*
|
|
6485
|
+
* "QUERY PLAN: grain = one row per customer, filtered to the named customer
|
|
6486
|
+
* (Matthew Meyer, honorific stripped) ... FROM dev.customers c, filtered on
|
|
6487
|
+
* c.customer_name ILIKE '%Matthew Meyer%'."
|
|
6488
|
+
*
|
|
6489
|
+
* Someone who asked why a customer tops a list should not be handed a join
|
|
6490
|
+
* plan. The plan stays in the artifact and the trace; it leaves the prose.
|
|
6491
|
+
*/
|
|
6492
|
+
export function stripQueryPlanScaffolding(text) {
|
|
6493
|
+
return text
|
|
6494
|
+
// A QUERY PLAN section runs to the next blank line or the end of the prose.
|
|
6495
|
+
.replace(/(?:^|\n)\s*(?:\*\*)?QUERY PLAN(?:\*\*)?\s*[::][\s\S]*?(?=\n\s*\n|$)/gi, '')
|
|
6496
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
6497
|
+
.trim();
|
|
6498
|
+
}
|
|
5788
6499
|
/**
|
|
5789
6500
|
* Public for tests. Prefer the structured W2.7 JSON proposal contract, then
|
|
5790
6501
|
* fall back to the legacy prose + ```sql block + Viz line format.
|
|
@@ -5798,11 +6509,11 @@ export function parseProposal(raw) {
|
|
|
5798
6509
|
const vizMatch = raw.match(/^Viz:\s*([a-z_]+)/im);
|
|
5799
6510
|
const viz = vizMatch ? vizMatch[1].trim().toLowerCase() : undefined;
|
|
5800
6511
|
// Strip the SQL block + Viz line from the prose to keep the summary clean.
|
|
5801
|
-
const text = raw
|
|
6512
|
+
const text = stripQueryPlanScaffolding(raw
|
|
5802
6513
|
.replace(/```json[\s\S]*?```/gi, '')
|
|
5803
6514
|
.replace(/```sql[\s\S]*?```/gi, '')
|
|
5804
6515
|
.replace(/^Viz:.*$/gim, '')
|
|
5805
|
-
.trim();
|
|
6516
|
+
.trim());
|
|
5806
6517
|
return { text, sql, viz };
|
|
5807
6518
|
}
|
|
5808
6519
|
function parseStructuredProposal(raw) {
|
|
@@ -5830,7 +6541,7 @@ function parsedProposalFromJson(value) {
|
|
|
5830
6541
|
return undefined;
|
|
5831
6542
|
const record = value;
|
|
5832
6543
|
const sql = firstJsonString(record.sql, record.query)?.trim();
|
|
5833
|
-
const text = firstJsonString(record.summary, record.text, record.answer, record.description)?.trim() ?? '';
|
|
6544
|
+
const text = stripQueryPlanScaffolding(firstJsonString(record.summary, record.text, record.answer, record.description)?.trim() ?? '');
|
|
5834
6545
|
const viz = firstJsonString(record.viz, record.visualization, record.chartType)
|
|
5835
6546
|
?.trim()
|
|
5836
6547
|
.toLowerCase()
|
|
@@ -6634,7 +7345,12 @@ function isBusinessDefinitionQuestion(question) {
|
|
|
6634
7345
|
}
|
|
6635
7346
|
/** Definition that may terminate with documentation instead of executing data. */
|
|
6636
7347
|
function isPureBusinessDefinitionQuestion(question) {
|
|
6637
|
-
return questionTypeFromText(question) === 'definition'
|
|
7348
|
+
return questionTypeFromText(question) === 'definition'
|
|
7349
|
+
// `questionTypeFromText` intentionally recognizes ranking before generic
|
|
7350
|
+
// definitions, so a named artifact such as `top_customers` would otherwise
|
|
7351
|
+
// look like a ranking request even in the explicit "what does … measure?"
|
|
7352
|
+
// form. Object-name matching remains mandatory at the terminal site.
|
|
7353
|
+
|| /^\s*what\s+does\s+(?:the\s+)?[^?.!]{1,160}\s+(?:measure|mean|define|represent)\s*[?.!]*\s*$/i.test(question);
|
|
6638
7354
|
}
|
|
6639
7355
|
function isBreakdownOrDrilldownQuestion(question) {
|
|
6640
7356
|
return /\b(break\s*down|breakdown|drill\s*(?:down|into)|slice|segment|split|by\s+[a-z][\w\s-]{1,40})\b/i.test(question);
|
|
@@ -6697,12 +7413,21 @@ function hasCertifiedNodeFit(question, plan, node, options = {}) {
|
|
|
6697
7413
|
plan,
|
|
6698
7414
|
block: node,
|
|
6699
7415
|
exactExampleMatch: exactExampleMatch || exactObjectRequest,
|
|
7416
|
+
uniqueExactExampleContract: options.uniqueExactExampleContract === true,
|
|
6700
7417
|
definitionLookup,
|
|
6701
7418
|
});
|
|
6702
7419
|
// Delegate the termination decision to THE single authority instead of a
|
|
6703
7420
|
// loop-local re-derivation (the seams between such copies produced certified
|
|
6704
7421
|
// answers that ignored member-scoped follow-ups).
|
|
6705
|
-
return certifiedTerminationVerdict({
|
|
7422
|
+
return certifiedTerminationVerdict({
|
|
7423
|
+
fit,
|
|
7424
|
+
// A named `what does <artifact> measure?` request is an artifact metadata
|
|
7425
|
+
// lookup, not a query-result claim. The termination authority still
|
|
7426
|
+
// rejects missing declared measures and unsupported filters before this
|
|
7427
|
+
// narrow bypass is considered.
|
|
7428
|
+
...(definitionLookup ? { bypass: 'definition_lookup' } : {}),
|
|
7429
|
+
allowInferredContract: options.allowInferredContract,
|
|
7430
|
+
}).allow;
|
|
6706
7431
|
}
|
|
6707
7432
|
function objectNameInQuestion(question, node) {
|
|
6708
7433
|
const questionText = normalizeQuestion(question);
|