@duckcodeailabs/dql-agent 1.13.4 → 1.14.0
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 +87 -7
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +559 -40
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +42 -25
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +100 -32
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/aggregation-safety-proof.d.ts +41 -0
- package/dist/aggregation-safety-proof.d.ts.map +1 -0
- package/dist/aggregation-safety-proof.js +511 -0
- package/dist/aggregation-safety-proof.js.map +1 -0
- package/dist/analytical-compatibility.d.ts.map +1 -1
- package/dist/analytical-compatibility.js +42 -25
- package/dist/analytical-compatibility.js.map +1 -1
- package/dist/analytical-execution-graph.d.ts +4 -21
- package/dist/analytical-execution-graph.d.ts.map +1 -1
- package/dist/analytical-execution-graph.js +27 -0
- package/dist/analytical-execution-graph.js.map +1 -1
- package/dist/analytical-frame.d.ts +12 -1
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +102 -2
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +256 -0
- package/dist/analytical-orchestration.d.ts.map +1 -0
- package/dist/analytical-orchestration.js +419 -0
- package/dist/analytical-orchestration.js.map +1 -0
- package/dist/analytical-result-facts.d.ts +54 -1
- package/dist/analytical-result-facts.d.ts.map +1 -1
- package/dist/analytical-result-facts.js +183 -0
- package/dist/analytical-result-facts.js.map +1 -1
- package/dist/answer-loop.d.ts +44 -3
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +771 -38
- package/dist/answer-loop.js.map +1 -1
- package/dist/cascade/budgets.js +1 -1
- package/dist/cascade/route-policy.d.ts +1 -1
- package/dist/cascade/route-policy.d.ts.map +1 -1
- package/dist/cascade/route-policy.js +2 -0
- package/dist/cascade/route-policy.js.map +1 -1
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +3 -0
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/conversation/turn-trust.d.ts.map +1 -1
- package/dist/conversation/turn-trust.js +6 -0
- package/dist/conversation/turn-trust.js.map +1 -1
- package/dist/generated-analytical-proposal.d.ts +37 -0
- package/dist/generated-analytical-proposal.d.ts.map +1 -0
- package/dist/generated-analytical-proposal.js +219 -0
- package/dist/generated-analytical-proposal.js.map +1 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +8 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.js +102 -6
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +30 -0
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +61 -8
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +46 -2
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/block-fit.d.ts.map +1 -1
- package/dist/metadata/block-fit.js +55 -2
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts +24 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +227 -42
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +408 -36
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/metadata/sql-context-validation.d.ts +3 -0
- package/dist/metadata/sql-context-validation.d.ts.map +1 -1
- package/dist/metadata/sql-context-validation.js +16 -5
- package/dist/metadata/sql-context-validation.js.map +1 -1
- package/dist/plan-execution-adapter.d.ts +9 -1
- package/dist/plan-execution-adapter.d.ts.map +1 -1
- package/dist/plan-execution-adapter.js +81 -32
- package/dist/plan-execution-adapter.js.map +1 -1
- package/dist/provider-egress.d.ts +122 -0
- package/dist/provider-egress.d.ts.map +1 -0
- package/dist/provider-egress.js +504 -0
- package/dist/provider-egress.js.map +1 -0
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +76 -33
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/dispatch.d.ts +15 -0
- package/dist/providers/dispatch.d.ts.map +1 -0
- package/dist/providers/dispatch.js +25 -0
- package/dist/providers/dispatch.js.map +1 -0
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +16 -8
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +2 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.d.ts.map +1 -1
- package/dist/providers/ollama.js +29 -10
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +92 -36
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +43 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/relationship-proof.d.ts +19 -0
- package/dist/relationship-proof.d.ts.map +1 -0
- package/dist/relationship-proof.js +75 -0
- package/dist/relationship-proof.js.map +1 -0
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +17 -11
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +25 -3
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +460 -31
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +38 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1060 -53
- package/dist/router.js.map +1 -1
- package/dist/semantic-identities.d.ts +18 -0
- package/dist/semantic-identities.d.ts.map +1 -0
- package/dist/semantic-identities.js +60 -0
- package/dist/semantic-identities.js.map +1 -0
- package/dist/semantic-proof-authority.d.ts +29 -0
- package/dist/semantic-proof-authority.d.ts.map +1 -0
- package/dist/semantic-proof-authority.js +34 -0
- package/dist/semantic-proof-authority.js.map +1 -0
- package/package.json +4 -4
package/dist/answer-loop.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* full pipeline.
|
|
16
16
|
*/
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
|
-
import { describeDialectForPrompt, normalizeMetricCapabilityContract, } from '@duckcodeailabs/dql-core';
|
|
18
|
+
import { describeDialectForPrompt, buildSqlOutputExpressionSignature, normalizeMetricCapabilityContract, semanticExecutionFingerprint, } from '@duckcodeailabs/dql-core';
|
|
19
19
|
import { buildSkillBlockHints, buildSkillMetricHints, buildSkillsPrompt, expandQuestionWithSkillVocabulary, selectRelevantSkills } from './skills/loader.js';
|
|
20
20
|
import { conversationTurnContextState, } from './conversation/snapshot.js';
|
|
21
21
|
import { isTrustedConversationTurn } from './conversation/turn-trust.js';
|
|
@@ -32,6 +32,9 @@ import { createContextLedger } from './grounding/context-ledger.js';
|
|
|
32
32
|
import { validateAnswerResultShape } from './answer-shape.js';
|
|
33
33
|
import { analyticalErrorDetail, } from './analytical-error.js';
|
|
34
34
|
import { fanoutWarningsForSql } from './metadata/grain-ledger.js';
|
|
35
|
+
import { buildAggregationSafetyProof, buildSemanticCompilationAggregationSafetyProof, } from './aggregation-safety-proof.js';
|
|
36
|
+
import { buildSemanticProofAuthorityV1 } from './semantic-proof-authority.js';
|
|
37
|
+
import { GENERATED_ANALYTICAL_TUPLE_DRIFT_MESSAGE, validateGeneratedAnalyticalProposal, } from './generated-analytical-proposal.js';
|
|
35
38
|
import { evaluateDbtFirstGeneratedSql } from './metadata/dbt-first-safety.js';
|
|
36
39
|
import { planAnalyticalPath, humanizeAnalyticalEntityId, analyticalPolicyUserFacingReason, } from './metadata/analytical-policy.js';
|
|
37
40
|
import { planCertifiedAdaptation } from './metadata/block-adapt.js';
|
|
@@ -47,14 +50,14 @@ import { questionTypeFromText } from './meaning-resolution.js';
|
|
|
47
50
|
import { cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, } from './cascade/cascade.js';
|
|
48
51
|
import { shouldClarifyBeforeGeneration } from './cascade/triage.js';
|
|
49
52
|
import { stampTrustLabel } from './trust/stamp.js';
|
|
50
|
-
import { deriveResolvedAnalyticalPlan } from './resolved-analytical-plan.js';
|
|
53
|
+
import { buildResolvedAnalyticalPlan, deriveResolvedAnalyticalPlan } from './resolved-analytical-plan.js';
|
|
51
54
|
import { adaptAnalyticalFreshnessRequest, adaptAnalyticalSemanticGraph, adaptResolvedAnalyticalPlan, buildPlanExecutionRegistry, } from './plan-execution-adapter.js';
|
|
52
55
|
import { buildGovernedRelationalRegistry, compileGovernedRelationalExecutionGraph, compileGovernedRelationalPlan, finalizeGovernedCompilationReceipt, renderGovernedRelationalDqlArtifact, } from './governed-relational-compiler.js';
|
|
53
56
|
import { buildAnalyticalExecutionGraph, executeAnalyticalExecutionGraph, } from './analytical-execution-graph.js';
|
|
54
57
|
import { buildAnalyticalResultFacts, renderDeterministicAnalyticalNarrative, validateAnalyticalNarrativeClaims, } from './analytical-result-facts.js';
|
|
55
58
|
import { resolveAnalyticalPeriods, } from './analytical-period-resolution.js';
|
|
56
59
|
import { classifyAnalyticalFailure, createAnalyticalFailure, } from './analytical-failure-repair.js';
|
|
57
|
-
import { QUICK_PROMPT_CONTEXT_BUDGET, canUseLaneRepair, cascadeBudgetTrace, createCascadeBudgetState,
|
|
60
|
+
import { QUICK_PROMPT_CONTEXT_BUDGET, canUseLaneRepair, cascadeBudgetTrace, createCascadeBudgetState, promptContextBudgetForQuestion, proposalToolBudgetForQuestion, recordLaneRepair, } from './cascade/budgets.js';
|
|
58
61
|
function semanticCompilerFailure(error) {
|
|
59
62
|
const record = error && typeof error === 'object' ? error : undefined;
|
|
60
63
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -675,6 +678,141 @@ export function materializeKnowledgeLensSkills(contextPack, available) {
|
|
|
675
678
|
};
|
|
676
679
|
});
|
|
677
680
|
}
|
|
681
|
+
/**
|
|
682
|
+
* Older direct AnswerLoop callers supplied the semantic catalog but no frozen
|
|
683
|
+
* route contract. Migrate that one bounded retrieval result exactly once at
|
|
684
|
+
* ingress: the chosen KG identity and its normalized capability are copied into
|
|
685
|
+
* an authoritative RAP before compiler/proof code runs. No downstream stage is
|
|
686
|
+
* permitted to repeat this name/family selection.
|
|
687
|
+
*/
|
|
688
|
+
async function freezeLegacySemanticSelection(input) {
|
|
689
|
+
const metricNodes = input.kg.getNodesByKind('metric', 100_000);
|
|
690
|
+
if (metricNodes.length === 0)
|
|
691
|
+
return undefined;
|
|
692
|
+
const questionPlan = input.contextPack?.questionPlan?.requestedShape
|
|
693
|
+
? input.contextPack.questionPlan
|
|
694
|
+
: buildAnalysisQuestionPlan(input.question, input.followUp);
|
|
695
|
+
const semanticLayer = input.semanticLayer;
|
|
696
|
+
const match = await matchSemanticMetric(input.question, metricNodes, {
|
|
697
|
+
measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
|
|
698
|
+
...(semanticLayer ? { canExecute: (name) => semanticLayer.canComposeMetric(name) } : {}),
|
|
699
|
+
}).catch(() => null);
|
|
700
|
+
if (!match)
|
|
701
|
+
return undefined;
|
|
702
|
+
const normalizedCapability = normalizeMetricCapabilityContract(match.metric.payload?.analyticalCapability);
|
|
703
|
+
if (!normalizedCapability)
|
|
704
|
+
return undefined;
|
|
705
|
+
const metricDefinition = semanticLayer?.listMetrics().find((metric) => metric.name === match.metric.name
|
|
706
|
+
|| metric.name.endsWith(`.${match.metric.name}`)
|
|
707
|
+
|| match.metric.name.endsWith(`.${metric.name}`));
|
|
708
|
+
const composed = semanticLayer ? composeSemanticQueryForQuestion({
|
|
709
|
+
semanticLayer,
|
|
710
|
+
question: input.question,
|
|
711
|
+
questionPlan,
|
|
712
|
+
matchedMetric: match.metric,
|
|
713
|
+
filterValueColumns: (value) => resolveFilterValueColumns(value, input.schemaContext ?? []),
|
|
714
|
+
filterValueBindings: (value) => resolveAgentFilterValueBindings(value, input.schemaContext ?? []),
|
|
715
|
+
...(input.semanticDriver ? { driver: input.semanticDriver } : {}),
|
|
716
|
+
...(input.semanticTableMapping ? { tableMapping: input.semanticTableMapping } : {}),
|
|
717
|
+
}) : undefined;
|
|
718
|
+
// A semantic runtime may still resolve the shape later (Research/member-tool
|
|
719
|
+
// lane). Do not freeze a partial tuple here.
|
|
720
|
+
if (semanticLayer && !composed)
|
|
721
|
+
return undefined;
|
|
722
|
+
const capability = {
|
|
723
|
+
...normalizedCapability,
|
|
724
|
+
dimensions: normalizedCapability.dimensions.map((dimension) => {
|
|
725
|
+
const definition = semanticLayer?.resolveGroupBy(dimension.dimensionId);
|
|
726
|
+
const compilerSelected = composed?.dimensions.includes(dimension.dimensionId)
|
|
727
|
+
|| composed?.timeDimension?.name === dimension.dimensionId;
|
|
728
|
+
return definition
|
|
729
|
+
? {
|
|
730
|
+
...dimension,
|
|
731
|
+
nativeGroupingReference: definition.qualifiedName ?? definition.name,
|
|
732
|
+
...(compilerSelected && questionPlan.requestedShape.rankingDirection
|
|
733
|
+
? { supportedRoles: [...new Set([...dimension.supportedRoles, 'rank_entity'])] }
|
|
734
|
+
: {}),
|
|
735
|
+
}
|
|
736
|
+
: dimension;
|
|
737
|
+
}),
|
|
738
|
+
};
|
|
739
|
+
const identity = match.metric.nodeId;
|
|
740
|
+
const selectedMetricNames = composed?.metrics ?? [match.metric.name];
|
|
741
|
+
const selectedNodes = selectedMetricNames.flatMap((name) => {
|
|
742
|
+
const matches = metricNodes.filter((node) => node.name === name || node.name.endsWith(`.${name}`) || name.endsWith(`.${node.name}`));
|
|
743
|
+
return matches.length === 1 ? matches : [];
|
|
744
|
+
});
|
|
745
|
+
if (selectedNodes.length !== selectedMetricNames.length)
|
|
746
|
+
return undefined;
|
|
747
|
+
const candidates = selectedNodes.flatMap((node) => {
|
|
748
|
+
const nodeCapability = normalizeMetricCapabilityContract(node.payload?.analyticalCapability);
|
|
749
|
+
if (!nodeCapability)
|
|
750
|
+
return [];
|
|
751
|
+
const isPrimary = node.nodeId === match.metric.nodeId;
|
|
752
|
+
return [{
|
|
753
|
+
id: node.nodeId,
|
|
754
|
+
kind: 'semantic_metric',
|
|
755
|
+
semanticObjectType: 'metric',
|
|
756
|
+
trustTier: 'semantic',
|
|
757
|
+
name: node.name,
|
|
758
|
+
aliases: node.tags,
|
|
759
|
+
definition: node.description,
|
|
760
|
+
domain: node.domain,
|
|
761
|
+
primaryEntity: nodeCapability.primaryEntityId,
|
|
762
|
+
dimensions: nodeCapability.dimensions.map((dimension) => dimension.dimensionId),
|
|
763
|
+
sourceObjects: nodeCapability.semanticModelId ? [nodeCapability.semanticModelId] : [],
|
|
764
|
+
relevanceScore: isPrimary ? match.score : 1,
|
|
765
|
+
matchReasons: [`bounded_${isPrimary ? match.basis ?? 'semantic' : 'compiler'}_retrieval`],
|
|
766
|
+
compatibility: 'compatible',
|
|
767
|
+
compatibilityFacts: ['The selected KG identity carries the exact normalized semantic capability embedded in this plan.'],
|
|
768
|
+
analyticalCapability: isPrimary ? capability : nodeCapability,
|
|
769
|
+
eligible: true,
|
|
770
|
+
exactMatch: true,
|
|
771
|
+
}];
|
|
772
|
+
});
|
|
773
|
+
if (candidates.length !== selectedNodes.length)
|
|
774
|
+
return undefined;
|
|
775
|
+
return buildResolvedAnalyticalPlan({
|
|
776
|
+
question: input.question,
|
|
777
|
+
resolution: {
|
|
778
|
+
interpretedQuestion: input.question,
|
|
779
|
+
questionType: questionTypeFromText(input.question),
|
|
780
|
+
selectedConceptIds: candidates.map((item) => item.id),
|
|
781
|
+
recommendedExecutionId: identity,
|
|
782
|
+
queryIntent: {
|
|
783
|
+
measures: composed?.metrics ?? [match.metric.name],
|
|
784
|
+
dimensions: composed ? [
|
|
785
|
+
...composed.dimensions,
|
|
786
|
+
...(composed.timeDimension ? [composed.timeDimension.name] : []),
|
|
787
|
+
] : [
|
|
788
|
+
...questionPlan.requestedShape.dimensions,
|
|
789
|
+
...(questionPlan.timeTerms.length > 0 && (metricDefinition?.aggTimeDimension ?? capability.timeDimensions[0]?.dimensionId)
|
|
790
|
+
? [metricDefinition?.aggTimeDimension ?? capability.timeDimensions[0].dimensionId]
|
|
791
|
+
: []),
|
|
792
|
+
],
|
|
793
|
+
filters: composed?.filters.map((filter) => ({ field: filter.dimension, value: String(filter.values[0] ?? '') })) ?? [],
|
|
794
|
+
...(composed?.timeDimension?.granularity
|
|
795
|
+
? { timeGrain: composed.timeDimension.granularity }
|
|
796
|
+
: questionPlan.timeTerms.some((term) => /month/i.test(term)) ? { timeGrain: 'month' } : {}),
|
|
797
|
+
...(questionPlan.requestedShape.rankingDirection
|
|
798
|
+
? { order: questionPlan.requestedShape.rankingDirection === 'top' ? 'desc' : 'asc' }
|
|
799
|
+
: {}),
|
|
800
|
+
...(questionPlan.requestedShape.topN ? { limit: questionPlan.requestedShape.topN.n } : {}),
|
|
801
|
+
},
|
|
802
|
+
rejectedCandidates: [],
|
|
803
|
+
confidence: match.score >= 0.7 ? 'high' : 'medium',
|
|
804
|
+
missingInformation: [],
|
|
805
|
+
recommendedRoute: 'semantic',
|
|
806
|
+
},
|
|
807
|
+
evidence: {
|
|
808
|
+
snapshotId: input.contextPack?.knowledgeLens?.snapshotId ?? input.contextPack?.id ?? 'legacy-answer-loop-snapshot',
|
|
809
|
+
sourceFingerprint: capability.sourceFingerprint,
|
|
810
|
+
candidates,
|
|
811
|
+
},
|
|
812
|
+
candidates,
|
|
813
|
+
mode: 'authoritative',
|
|
814
|
+
});
|
|
815
|
+
}
|
|
678
816
|
export async function answer(input) {
|
|
679
817
|
const inheritedPlan = !input.resolvedAnalyticalPlan
|
|
680
818
|
&& input.followUp?.priorResolvedAnalyticalPlan
|
|
@@ -694,6 +832,11 @@ export async function answer(input) {
|
|
|
694
832
|
resolvedAnalyticalPlan: normalizedInput.resolvedAnalyticalPlan,
|
|
695
833
|
};
|
|
696
834
|
}
|
|
835
|
+
if (!normalizedInput.resolvedAnalyticalPlan) {
|
|
836
|
+
const migratedPlan = await freezeLegacySemanticSelection(normalizedInput);
|
|
837
|
+
if (migratedPlan)
|
|
838
|
+
normalizedInput = { ...normalizedInput, resolvedAnalyticalPlan: migratedPlan };
|
|
839
|
+
}
|
|
697
840
|
const executionRegistry = buildPlanExecutionRegistry({
|
|
698
841
|
nodes: [
|
|
699
842
|
...normalizedInput.kg.getNodesByKind('block', 100_000),
|
|
@@ -731,7 +874,7 @@ export async function answer(input) {
|
|
|
731
874
|
request: freshnessRequest,
|
|
732
875
|
registry: executionRegistry,
|
|
733
876
|
semanticLayer: normalizedInput.semanticLayer,
|
|
734
|
-
expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens
|
|
877
|
+
expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens?.snapshotId,
|
|
735
878
|
})
|
|
736
879
|
: undefined;
|
|
737
880
|
if (adapterBinding?.status === 'blocked') {
|
|
@@ -788,7 +931,7 @@ export async function answer(input) {
|
|
|
788
931
|
plan: normalizedInput.resolvedAnalyticalPlan,
|
|
789
932
|
registry: executionRegistry,
|
|
790
933
|
semanticLayer: normalizedInput.semanticLayer,
|
|
791
|
-
expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens
|
|
934
|
+
expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens?.snapshotId,
|
|
792
935
|
})
|
|
793
936
|
: undefined;
|
|
794
937
|
const analyticalCapability = normalizedInput.resolvedAnalyticalPlan
|
|
@@ -801,9 +944,11 @@ export async function answer(input) {
|
|
|
801
944
|
? analyticalCapability.executionCapabilities.find((candidate) => candidate.route === analyticalRoute)?.adapterId
|
|
802
945
|
: undefined;
|
|
803
946
|
const multiMetricPlan = (normalizedInput.resolvedAnalyticalPlan?.analyticalFrame?.metricConceptIds.length ?? 0) > 1;
|
|
947
|
+
const generatedProposalPlan = normalizedInput.resolvedAnalyticalPlan?.capability === 'bounded_exploration';
|
|
804
948
|
const analyticalGraphBuild = normalizedInput.resolvedAnalyticalPlan?.schemaVersion === 2
|
|
805
949
|
&& normalizedInput.resolvedAnalyticalPlan.analyticalFrame
|
|
806
950
|
&& !multiMetricPlan
|
|
951
|
+
&& !generatedProposalPlan
|
|
807
952
|
&& !normalizedInput.analyticalPeriodResolutionFailure
|
|
808
953
|
&& analyticalCapability
|
|
809
954
|
&& analyticalRoute
|
|
@@ -819,6 +964,7 @@ export async function answer(input) {
|
|
|
819
964
|
: undefined;
|
|
820
965
|
const analyticalExecutionGraphFailure = normalizedInput.resolvedAnalyticalPlan?.schemaVersion === 2
|
|
821
966
|
&& !multiMetricPlan
|
|
967
|
+
&& !generatedProposalPlan
|
|
822
968
|
? analyticalGraphBuild?.status === 'blocked'
|
|
823
969
|
? {
|
|
824
970
|
code: analyticalGraphBuild.code,
|
|
@@ -839,7 +985,7 @@ export async function answer(input) {
|
|
|
839
985
|
plan: normalizedInput.resolvedAnalyticalPlan,
|
|
840
986
|
registry: executionRegistry,
|
|
841
987
|
semanticLayer: normalizedInput.semanticLayer,
|
|
842
|
-
expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens
|
|
988
|
+
expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens?.snapshotId,
|
|
843
989
|
})
|
|
844
990
|
: undefined;
|
|
845
991
|
const planBoundInput = resolvedPlanExecutionBinding || analyticalExecutionGraph || analyticalExecutionGraphFailure
|
|
@@ -1017,17 +1163,27 @@ function deriveAiRoute(result, metricMatch) {
|
|
|
1017
1163
|
};
|
|
1018
1164
|
}
|
|
1019
1165
|
// Uncertified: a governed metric matched → semantic_metric; else generated preview.
|
|
1020
|
-
if (result.sourceTier === 'semantic_layer' && metricMatch) {
|
|
1166
|
+
if (result.sourceTier === 'semantic_layer' && (metricMatch || result.analyticalExecutionGraph?.route === 'semantic')) {
|
|
1167
|
+
const metricName = metricMatch?.metric.name
|
|
1168
|
+
?? result.dqlArtifact?.metrics?.join(', ')
|
|
1169
|
+
?? result.analyticalExecutionGraph?.metricId;
|
|
1021
1170
|
return {
|
|
1022
1171
|
tier: 'semantic_metric',
|
|
1023
|
-
label: `Answered from metric ${
|
|
1024
|
-
ref:
|
|
1172
|
+
label: metricName ? `Answered from metric ${metricName}` : 'Answered from the snapshot-bound semantic plan',
|
|
1173
|
+
...(metricName ? { ref: metricName } : {}),
|
|
1025
1174
|
};
|
|
1026
1175
|
}
|
|
1027
1176
|
return result.dqlArtifact
|
|
1028
1177
|
? { tier: 'generated_sql', label: 'Prepared review-required DQL artifact with SQL preview.' }
|
|
1029
1178
|
: { tier: 'generated_sql', label: 'Prepared review-required SQL preview.' };
|
|
1030
1179
|
}
|
|
1180
|
+
/** "customers_customer_name" → "customers" for a question the user has to read. */
|
|
1181
|
+
function humanizeDeicticDimension(dimension) {
|
|
1182
|
+
const leaf = dimension.split(/[.:]/).pop() ?? dimension;
|
|
1183
|
+
const words = leaf.replace(/[_-]+/g, ' ').trim().toLowerCase();
|
|
1184
|
+
const last = words.split(' ').filter(Boolean).pop() ?? words;
|
|
1185
|
+
return last.endsWith('s') ? last : `${last}s`;
|
|
1186
|
+
}
|
|
1031
1187
|
async function runAnswerLoop(input) {
|
|
1032
1188
|
const { question, userId, domain, provider, kg, skills = [], blockHints = [] } = input;
|
|
1033
1189
|
// AGT-004: with no explicit domain selection, let direct question evidence
|
|
@@ -1198,6 +1354,7 @@ async function runAnswerLoop(input) {
|
|
|
1198
1354
|
input,
|
|
1199
1355
|
binding: input.semanticGraphExecutionBinding,
|
|
1200
1356
|
graph: input.analyticalExecutionGraph,
|
|
1357
|
+
capability: normalizeMetricCapabilityContract(input.semanticGraphExecutionBinding.metricNode.payload?.analyticalCapability),
|
|
1201
1358
|
considered,
|
|
1202
1359
|
providerName: provider.name,
|
|
1203
1360
|
});
|
|
@@ -1313,7 +1470,22 @@ async function runAnswerLoop(input) {
|
|
|
1313
1470
|
};
|
|
1314
1471
|
}
|
|
1315
1472
|
if (authoritativePlanBinding?.status === 'blocked') {
|
|
1316
|
-
|
|
1473
|
+
// An ambiguous singular reference is answerable the moment the user says
|
|
1474
|
+
// which one they meant, so offer the candidates rather than dead-ending.
|
|
1475
|
+
// The old refusal shipped the binder's internal sentence as the answer
|
|
1476
|
+
// text, which reached the user as a generic "could not be completed" card.
|
|
1477
|
+
const choices = input.followUp?.deicticChoices;
|
|
1478
|
+
const clarificationOptions = choices
|
|
1479
|
+
? choices.values.slice(0, 8).map((value) => ({
|
|
1480
|
+
id: `member:${choices.dimension}:${value}`,
|
|
1481
|
+
label: value,
|
|
1482
|
+
kind: 'member',
|
|
1483
|
+
question: `${question} (${value})`,
|
|
1484
|
+
}))
|
|
1485
|
+
: undefined;
|
|
1486
|
+
const text = clarificationOptions?.length
|
|
1487
|
+
? `The previous answer listed ${choices.values.length} ${humanizeDeicticDimension(choices.dimension)}. Which one did you mean?`
|
|
1488
|
+
: `The resolved analytical plan cannot execute safely: ${authoritativePlanBinding.reason}`;
|
|
1317
1489
|
return {
|
|
1318
1490
|
kind: 'no_answer',
|
|
1319
1491
|
sourceTier: 'no_answer',
|
|
@@ -1322,8 +1494,12 @@ async function runAnswerLoop(input) {
|
|
|
1322
1494
|
confidence: 0,
|
|
1323
1495
|
text,
|
|
1324
1496
|
answer: text,
|
|
1325
|
-
|
|
1497
|
+
// Offering real choices makes this a clarify turn, not a terminal block.
|
|
1498
|
+
refusalCode: clarificationOptions?.length || authoritativePlanBinding.code === 'PLAN_BLOCKED'
|
|
1499
|
+
? 'ambiguous'
|
|
1500
|
+
: 'grounding_gap',
|
|
1326
1501
|
refusalDetails: { code: authoritativePlanBinding.code, message: authoritativePlanBinding.reason },
|
|
1502
|
+
...(clarificationOptions?.length ? { clarificationOptions } : {}),
|
|
1327
1503
|
citations: contextPackCitations(input.contextPack, 8),
|
|
1328
1504
|
considered,
|
|
1329
1505
|
contextPack: input.contextPack,
|
|
@@ -1766,6 +1942,13 @@ async function runAnswerLoop(input) {
|
|
|
1766
1942
|
?? renderSingleEntityModelingPrompt(analyticalPlan, input.manifest);
|
|
1767
1943
|
if (analyticalPlanPrompt)
|
|
1768
1944
|
messages.push({ role: 'system', content: analyticalPlanPrompt });
|
|
1945
|
+
const attributeLookup = isMetricIndependentAttributeLookup(question, questionPlan, input.followUp);
|
|
1946
|
+
if (attributeLookup) {
|
|
1947
|
+
messages.push({
|
|
1948
|
+
role: 'system',
|
|
1949
|
+
content: renderAttributeLookupInstruction(questionPlan, input.followUp),
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1769
1952
|
if (questionDomainScope.length > 0) {
|
|
1770
1953
|
messages.push({
|
|
1771
1954
|
role: 'system',
|
|
@@ -1795,6 +1978,7 @@ async function runAnswerLoop(input) {
|
|
|
1795
1978
|
let semanticBridgeAnswer;
|
|
1796
1979
|
let semanticRuntimeFailure;
|
|
1797
1980
|
let semanticExecutionTrace;
|
|
1981
|
+
let aggregationCompilerReceipt;
|
|
1798
1982
|
let semanticAttemptedArtifact;
|
|
1799
1983
|
let semanticRuntimeCompiledAnswer = false;
|
|
1800
1984
|
if (authoritativeSemanticBinding && input.semanticLayer) {
|
|
@@ -1902,7 +2086,7 @@ async function runAnswerLoop(input) {
|
|
|
1902
2086
|
// AGT-005: a governed runtime failure is terminal for this selected metric. Do not
|
|
1903
2087
|
// spend another model call re-selecting the same member or silently fall
|
|
1904
2088
|
// into exploratory SQL; surface the compiler's actionable error instead.
|
|
1905
|
-
if (!semanticBridgeAnswer && !semanticRuntimeFailure) {
|
|
2089
|
+
if (!semanticBridgeAnswer && !semanticRuntimeFailure && input.allowProviderSemanticMemberSelection === true) {
|
|
1906
2090
|
const requestedGroupingDims = requestedGroupingDimensions(questionPlan);
|
|
1907
2091
|
const wantedBreakdown = questionPlan.requestedShape.dimensions.length > 0
|
|
1908
2092
|
|| questionPlan.dimensionTerms.length > 0;
|
|
@@ -2219,6 +2403,7 @@ async function runAnswerLoop(input) {
|
|
|
2219
2403
|
reasoningEffort: input.reasoningEffort,
|
|
2220
2404
|
analysisDepth: input.analysisDepth,
|
|
2221
2405
|
toolCalls: proposalToolCalls,
|
|
2406
|
+
providerPayloadGuard: input.providerPayloadGuard,
|
|
2222
2407
|
});
|
|
2223
2408
|
}
|
|
2224
2409
|
catch (err) {
|
|
@@ -2230,7 +2415,28 @@ async function runAnswerLoop(input) {
|
|
|
2230
2415
|
throw input.signal.reason ?? err;
|
|
2231
2416
|
if (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError'))
|
|
2232
2417
|
throw err;
|
|
2233
|
-
|
|
2418
|
+
// DQL's own orchestration budget refusing a dispatch is NOT an upstream
|
|
2419
|
+
// outage. Reporting it as one ("Claude subscription failed: the 25-second
|
|
2420
|
+
// soft target elapsed") blames the user's AI provider for DQL's timer and
|
|
2421
|
+
// sends them to debug the wrong system.
|
|
2422
|
+
const budgetCode = err && typeof err === 'object' && 'code' in err
|
|
2423
|
+
? String(err.code)
|
|
2424
|
+
: '';
|
|
2425
|
+
// Admission control is NOT an exhausted clock. `RUN_DEADLINE_INSUFFICIENT`
|
|
2426
|
+
// means DQL declined to START a call it predicted would not fit in the
|
|
2427
|
+
// remaining budget — it can fire seconds into a run, and a compound turn's
|
|
2428
|
+
// second task hit it after 6.4s of a 45s budget. Sharing the "stopped at
|
|
2429
|
+
// its own time budget" wording with the genuine soft-target codes told the
|
|
2430
|
+
// reader the run ran out of time when most of it remained, sending them to
|
|
2431
|
+
// debug latency instead of the estimate.
|
|
2432
|
+
const admissionDeclined = budgetCode === 'RUN_DEADLINE_INSUFFICIENT';
|
|
2433
|
+
const orchestrationBudget = budgetCode === 'RUN_SOFT_TARGET_EXCEEDED'
|
|
2434
|
+
|| budgetCode === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED';
|
|
2435
|
+
const text = admissionDeclined
|
|
2436
|
+
? `DQL did not start another AI call because it estimated the call would not finish inside this run's remaining budget, so the run stopped early rather than mid-flight. The AI provider did not fail and the time limit was not necessarily reached. ${err.message} Retry, or use Research for a longer budget.`
|
|
2437
|
+
: orchestrationBudget
|
|
2438
|
+
? 'DQL stopped this run at its own time budget before the query could be settled. The AI provider did not fail. Retry, or use Research for a longer budget.'
|
|
2439
|
+
: `Provider error: ${err.message}`;
|
|
2234
2440
|
return {
|
|
2235
2441
|
kind: 'no_answer',
|
|
2236
2442
|
sourceTier: 'no_answer',
|
|
@@ -2240,7 +2446,10 @@ async function runAnswerLoop(input) {
|
|
|
2240
2446
|
text,
|
|
2241
2447
|
answer: text,
|
|
2242
2448
|
refusalCode: 'provider_error',
|
|
2243
|
-
refusalDetails: {
|
|
2449
|
+
refusalDetails: {
|
|
2450
|
+
code: orchestrationBudget ? 'orchestration_budget_exhausted' : 'provider_error',
|
|
2451
|
+
message: orchestrationBudget ? `${text} (${err.message})` : text,
|
|
2452
|
+
},
|
|
2244
2453
|
citations: [],
|
|
2245
2454
|
memoryContext: input.memoryContext,
|
|
2246
2455
|
evidence: buildNoAnswerEvidence({
|
|
@@ -2307,7 +2516,10 @@ async function runAnswerLoop(input) {
|
|
|
2307
2516
|
executeGeneratedSql: input.executeGeneratedSql,
|
|
2308
2517
|
signal: input.signal,
|
|
2309
2518
|
reasoningEffort: input.reasoningEffort,
|
|
2310
|
-
|
|
2519
|
+
// One initial generation plus one diverse candidate stays within the
|
|
2520
|
+
// ordinary two-round ceiling. Candidates are validated without preview
|
|
2521
|
+
// execution below; only the selected SQL reaches the gateway once.
|
|
2522
|
+
maxAlternatives: 1,
|
|
2311
2523
|
});
|
|
2312
2524
|
if (selection.selected) {
|
|
2313
2525
|
proposed = selection.selected.raw;
|
|
@@ -2368,13 +2580,16 @@ async function runAnswerLoop(input) {
|
|
|
2368
2580
|
proposed = await generateProposalWithOptionalTools({
|
|
2369
2581
|
provider,
|
|
2370
2582
|
messages: [...messages, { role: 'system', content: FORCE_JOIN_INSTRUCTION }],
|
|
2371
|
-
|
|
2583
|
+
// One tool round per ordinary Ask. The second generation is a bounded
|
|
2584
|
+
// composition correction over already-retrieved evidence.
|
|
2585
|
+
tools: [],
|
|
2372
2586
|
questionPlan,
|
|
2373
2587
|
intent,
|
|
2374
2588
|
signal: input.signal,
|
|
2375
2589
|
reasoningEffort: input.reasoningEffort,
|
|
2376
2590
|
analysisDepth: input.analysisDepth,
|
|
2377
2591
|
toolCalls: proposalToolCalls,
|
|
2592
|
+
providerPayloadGuard: input.providerPayloadGuard,
|
|
2378
2593
|
});
|
|
2379
2594
|
parsed = parseProposal(proposed);
|
|
2380
2595
|
}
|
|
@@ -2406,7 +2621,9 @@ async function runAnswerLoop(input) {
|
|
|
2406
2621
|
? analyticalPlan.userFacingReason
|
|
2407
2622
|
?? analyticalPolicyUserFacingReason(analyticalPlan.code ?? 'unsafe_relationship', analyticalPlan.entities)
|
|
2408
2623
|
: declinedDespiteContext
|
|
2409
|
-
?
|
|
2624
|
+
? attributeLookup
|
|
2625
|
+
? 'I could not resolve the requested attribute from the available metadata. I preserved the conversation member, but the inspected models do not prove a relationship to that attribute. Tell me the exact field or table, or re-sync the dbt metadata, and I can prepare a review-required lookup.'
|
|
2626
|
+
: 'I could not compose a governed query for this from the available tables and metrics. This usually needs a clearer join path or an explicit metric and grouping — name the specific measure and how to break it down, and I can generate a review-required draft.'
|
|
2410
2627
|
: parsed.text || 'No answer (the model declined to propose SQL).';
|
|
2411
2628
|
return {
|
|
2412
2629
|
kind: 'no_answer',
|
|
@@ -2446,6 +2663,48 @@ async function runAnswerLoop(input) {
|
|
|
2446
2663
|
};
|
|
2447
2664
|
}
|
|
2448
2665
|
if (parsed.sql) {
|
|
2666
|
+
const frozenPlan = input.resolvedAnalyticalPlan;
|
|
2667
|
+
if (!governedMetricAnswer && frozenPlan?.mode === 'authoritative' && frozenPlan.schemaVersion === 2) {
|
|
2668
|
+
const targetFingerprint = input.generatedProposalTargetFingerprint ?? '';
|
|
2669
|
+
const validation = validateGeneratedAnalyticalProposal({
|
|
2670
|
+
plan: frozenPlan,
|
|
2671
|
+
expectedTargetFingerprint: targetFingerprint,
|
|
2672
|
+
contextPack: input.contextPack,
|
|
2673
|
+
dialect: input.semanticDriver,
|
|
2674
|
+
proposal: {
|
|
2675
|
+
version: 1,
|
|
2676
|
+
planId: frozenPlan.planId,
|
|
2677
|
+
planFingerprint: frozenPlan.fingerprint,
|
|
2678
|
+
snapshotId: frozenPlan.snapshotId,
|
|
2679
|
+
executionId: frozenPlan.executionId ?? '',
|
|
2680
|
+
capabilityFingerprint: frozenPlan.selectedCapabilityFingerprint ?? '',
|
|
2681
|
+
targetFingerprint,
|
|
2682
|
+
sql: parsed.sql,
|
|
2683
|
+
},
|
|
2684
|
+
});
|
|
2685
|
+
if (!validation.ok) {
|
|
2686
|
+
return {
|
|
2687
|
+
kind: 'no_answer',
|
|
2688
|
+
sourceTier: 'no_answer',
|
|
2689
|
+
certification: 'analyst_review_required',
|
|
2690
|
+
reviewStatus: 'none',
|
|
2691
|
+
confidence: 0,
|
|
2692
|
+
text: GENERATED_ANALYTICAL_TUPLE_DRIFT_MESSAGE,
|
|
2693
|
+
answer: GENERATED_ANALYTICAL_TUPLE_DRIFT_MESSAGE,
|
|
2694
|
+
refusalCode: 'policy_blocked',
|
|
2695
|
+
refusalDetails: {
|
|
2696
|
+
code: validation.code,
|
|
2697
|
+
message: `${validation.message}: ${validation.driftCodes.join(', ')}`,
|
|
2698
|
+
},
|
|
2699
|
+
validationWarnings: validation.driftCodes,
|
|
2700
|
+
citations: contextPackCitations(input.contextPack, 8),
|
|
2701
|
+
memoryContext: input.memoryContext,
|
|
2702
|
+
contextPack: input.contextPack,
|
|
2703
|
+
considered,
|
|
2704
|
+
providerUsed: provider.name,
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2449
2708
|
const tightenedFlow = tightenSourceTargetFlowProjection(parsed.sql, question, questionPlan);
|
|
2450
2709
|
if (tightenedFlow && tightenedFlow.sql !== parsed.sql) {
|
|
2451
2710
|
parsed.sql = tightenedFlow.sql;
|
|
@@ -2479,8 +2738,52 @@ async function runAnswerLoop(input) {
|
|
|
2479
2738
|
// semantic environment. A thin local retrieval pack cannot safely reject it
|
|
2480
2739
|
// and replace it with a guessed leaf-measure SQL. Warehouse execution remains
|
|
2481
2740
|
// the final binder/runtime check.
|
|
2741
|
+
const toolOwnedMetricNames = semanticBridgeAnswer?.metrics
|
|
2742
|
+
?? compiledSemanticRecords.flatMap((record) => record.metrics);
|
|
2743
|
+
const semanticProofAuthority = input.resolvedAnalyticalPlan
|
|
2744
|
+
? exactSemanticProofAuthority(input.resolvedAnalyticalPlan, semanticMetricNodes)
|
|
2745
|
+
: (input.allowProviderSemanticMemberSelection === true || compiledSemanticRecords.length > 0)
|
|
2746
|
+
? exactToolSemanticProofAuthority(toolOwnedMetricNames, semanticMetricNodes)
|
|
2747
|
+
: { metricObjects: [] };
|
|
2748
|
+
const semanticProofContext = semanticRouteAggregationProofContext(input.contextPack, input.resolvedAnalyticalPlan, semanticProofAuthority.capability, semanticProofAuthority.metricObjects);
|
|
2749
|
+
const toolOwnedSemanticProofContext = !input.resolvedAnalyticalPlan
|
|
2750
|
+
&& semanticProofAuthority.capability
|
|
2751
|
+
&& (input.allowProviderSemanticMemberSelection === true || compiledSemanticRecords.length > 0)
|
|
2752
|
+
? {
|
|
2753
|
+
...(semanticProofContext ?? {}),
|
|
2754
|
+
questionPlan: {
|
|
2755
|
+
...(semanticProofContext?.questionPlan ?? buildAnalysisQuestionPlan(question)),
|
|
2756
|
+
requestedShape: {
|
|
2757
|
+
...(semanticProofContext?.questionPlan?.requestedShape ?? buildAnalysisQuestionPlan(question).requestedShape),
|
|
2758
|
+
grain: semanticBridgeAnswer?.dimensions[0] ?? semanticProofAuthority.capability.defaultResultGrainId,
|
|
2759
|
+
dimensions: semanticBridgeAnswer?.dimensions ?? [],
|
|
2760
|
+
},
|
|
2761
|
+
},
|
|
2762
|
+
}
|
|
2763
|
+
: semanticProofContext;
|
|
2764
|
+
const semanticAggregationProof = governedMetricAnswer && !certifiedAdaptation
|
|
2765
|
+
? buildAggregationSafetyProof(parsed.sql, toolOwnedSemanticProofContext, input.semanticDriver, input.resolvedAnalyticalPlan?.fingerprint)
|
|
2766
|
+
: undefined;
|
|
2767
|
+
const semanticExecutionRequested = governedMetricAnswer
|
|
2768
|
+
&& !certifiedAdaptation
|
|
2769
|
+
&& Boolean(input.executeGeneratedSql || input.executeDqlArtifact);
|
|
2770
|
+
const semanticAggregationAuthorityBlocked = semanticExecutionRequested
|
|
2771
|
+
&& semanticAggregationProof?.status !== 'safe';
|
|
2482
2772
|
const initialValidation = semanticRuntimeCompiledAnswer
|
|
2483
|
-
?
|
|
2773
|
+
? semanticAggregationProof?.status === 'safe' || !semanticExecutionRequested
|
|
2774
|
+
? {
|
|
2775
|
+
ok: true,
|
|
2776
|
+
warnings: ['SQL compiled by the configured semantic runtime.'],
|
|
2777
|
+
aggregationSafetyProof: semanticAggregationProof,
|
|
2778
|
+
}
|
|
2779
|
+
: {
|
|
2780
|
+
ok: false,
|
|
2781
|
+
code: 'unsafe_aggregation',
|
|
2782
|
+
error: semanticAggregationSafetyFailure(semanticAggregationProof),
|
|
2783
|
+
warnings: ['Semantic runtime compilation did not establish complete aggregation authority.'],
|
|
2784
|
+
offending: undefined,
|
|
2785
|
+
aggregationSafetyProof: semanticAggregationProof,
|
|
2786
|
+
}
|
|
2484
2787
|
: contextLedger.validateSql(parsed.sql, {
|
|
2485
2788
|
question,
|
|
2486
2789
|
intent,
|
|
@@ -2489,6 +2792,9 @@ async function runAnswerLoop(input) {
|
|
|
2489
2792
|
memberBindings: input.followUp?.memberBindings,
|
|
2490
2793
|
enforceAggregationIntegrity: !governedMetricAnswer,
|
|
2491
2794
|
});
|
|
2795
|
+
let aggregationSafetyProof = 'aggregationSafetyProof' in initialValidation
|
|
2796
|
+
? initialValidation.aggregationSafetyProof
|
|
2797
|
+
: undefined;
|
|
2492
2798
|
const rankedGrainGap = missingRankedGrainOutput(questionPlan, parsed.sql, semanticBridgeAnswer?.dimensions);
|
|
2493
2799
|
contextValidation = initialValidation.ok && rankedGrainGap
|
|
2494
2800
|
? {
|
|
@@ -2498,14 +2804,37 @@ async function runAnswerLoop(input) {
|
|
|
2498
2804
|
warnings: initialValidation.warnings,
|
|
2499
2805
|
}
|
|
2500
2806
|
: initialValidation.ok
|
|
2501
|
-
? { ok: true, warnings: initialValidation.warnings }
|
|
2807
|
+
? { ok: true, warnings: initialValidation.warnings, aggregationSafetyProof }
|
|
2502
2808
|
: {
|
|
2503
2809
|
ok: false,
|
|
2504
2810
|
code: initialValidation.code,
|
|
2505
2811
|
error: initialValidation.error,
|
|
2506
2812
|
warnings: initialValidation.warnings,
|
|
2507
2813
|
offending: initialValidation.offending,
|
|
2814
|
+
aggregationSafetyProof,
|
|
2508
2815
|
};
|
|
2816
|
+
// The route-specific semantic proof is authoritative for execution. Context
|
|
2817
|
+
// validation can prove SQL names without proving that the selected metric's
|
|
2818
|
+
// exact capability, grain, additivity and joins own this aggregate. Never let
|
|
2819
|
+
// a broad context pack or warehouse success elevate that blocked state.
|
|
2820
|
+
if (contextValidation.ok
|
|
2821
|
+
&& semanticAggregationAuthorityBlocked) {
|
|
2822
|
+
aggregationSafetyProof = semanticAggregationProof;
|
|
2823
|
+
contextValidation = {
|
|
2824
|
+
ok: false,
|
|
2825
|
+
code: 'unsafe_aggregation',
|
|
2826
|
+
error: semanticAggregationSafetyFailure(semanticAggregationProof),
|
|
2827
|
+
warnings: [
|
|
2828
|
+
...contextValidation.warnings,
|
|
2829
|
+
'The selected semantic route lacks an exact positive aggregation proof.',
|
|
2830
|
+
],
|
|
2831
|
+
aggregationSafetyProof: semanticAggregationProof,
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
else if (semanticAggregationProof) {
|
|
2835
|
+
aggregationSafetyProof = semanticAggregationProof;
|
|
2836
|
+
contextValidation = { ...contextValidation, aggregationSafetyProof: semanticAggregationProof };
|
|
2837
|
+
}
|
|
2509
2838
|
// A canonical member value that resolves to exactly one inspected column is
|
|
2510
2839
|
// compiler-owned request state, not creative SQL. If the proposal omitted
|
|
2511
2840
|
// that predicate entirely, inject it deterministically before spending the
|
|
@@ -2529,6 +2858,7 @@ async function runAnswerLoop(input) {
|
|
|
2529
2858
|
const reboundGrainGap = rebound.ok
|
|
2530
2859
|
? missingRankedGrainOutput(questionPlan, parsed.sql, semanticBridgeAnswer?.dimensions)
|
|
2531
2860
|
: undefined;
|
|
2861
|
+
aggregationSafetyProof = rebound.aggregationSafetyProof;
|
|
2532
2862
|
contextValidation = rebound.ok && !reboundGrainGap
|
|
2533
2863
|
? { ok: true, warnings: [...rebound.warnings, ...injected.notes] }
|
|
2534
2864
|
: rebound.ok
|
|
@@ -2676,7 +3006,20 @@ async function runAnswerLoop(input) {
|
|
|
2676
3006
|
// provider get the same single chance; any provider failure keeps the honest
|
|
2677
3007
|
// refusal below. This mirrors the engine-level repair loop, applied to the
|
|
2678
3008
|
// context-validation gate that previously refused on first failure.
|
|
2679
|
-
|
|
3009
|
+
// Semantic aggregation authority cannot be created by a provider rewrite. In
|
|
3010
|
+
// particular, clearing governedMetricAnswer after a blocked semantic proof
|
|
3011
|
+
// must not turn that refusal into the generic SQL-repair lane: the provider
|
|
3012
|
+
// could return grounded raw-row SQL and silently downgrade the attempt to an
|
|
3013
|
+
// uncertified execution. Keep this semantic proof failure terminal while
|
|
3014
|
+
// retaining only the separate typed warehouse SQL-only repair capability.
|
|
3015
|
+
// Unsafe aggregation/rounding is never rewritten automatically until the SQL
|
|
3016
|
+
// parser supplies exact nested-expression AST spans.
|
|
3017
|
+
if (!contextValidation.ok
|
|
3018
|
+
&& contextValidation.code !== 'unsafe_aggregation'
|
|
3019
|
+
&& input.resolvedAnalyticalPlan?.mode !== 'authoritative'
|
|
3020
|
+
&& !semanticAggregationAuthorityBlocked
|
|
3021
|
+
&& !governedMetricAnswer
|
|
3022
|
+
&& canUseLaneRepair(repairBudgetState, 'validation')) {
|
|
2680
3023
|
recordLaneRepair(repairBudgetState, 'validation');
|
|
2681
3024
|
try {
|
|
2682
3025
|
const failedSql = parsed.sql ?? '';
|
|
@@ -2782,7 +3125,9 @@ async function runAnswerLoop(input) {
|
|
|
2782
3125
|
answer: text,
|
|
2783
3126
|
proposedSql: parsed.sql,
|
|
2784
3127
|
sql: parsed.sql,
|
|
2785
|
-
trustLabel:
|
|
3128
|
+
trustLabel: contextValidation.code === 'unsafe_aggregation'
|
|
3129
|
+
? 'blocked'
|
|
3130
|
+
: input.contextPack?.trustLabel,
|
|
2786
3131
|
sourceCertifiedBlock: followUpSourceBlock?.name ?? input.followUp?.sourceBlockName,
|
|
2787
3132
|
contextPackId: input.contextPack?.id,
|
|
2788
3133
|
validationWarnings: [
|
|
@@ -2808,6 +3153,7 @@ async function runAnswerLoop(input) {
|
|
|
2808
3153
|
contextPack: input.contextPack,
|
|
2809
3154
|
considered,
|
|
2810
3155
|
providerUsed: provider.name,
|
|
3156
|
+
aggregationSafetyProof,
|
|
2811
3157
|
};
|
|
2812
3158
|
}
|
|
2813
3159
|
// The dbt-first join guard adjudicates GENERATED SQL — joins an LLM invented,
|
|
@@ -3050,6 +3396,7 @@ async function runAnswerLoop(input) {
|
|
|
3050
3396
|
memberBindings: input.followUp?.memberBindings,
|
|
3051
3397
|
});
|
|
3052
3398
|
if (repairedValidation.ok) {
|
|
3399
|
+
aggregationSafetyProof = repairedValidation.aggregationSafetyProof;
|
|
3053
3400
|
repairAttempts += 1;
|
|
3054
3401
|
// Adopt the corrected SQL, but do NOT let the repair prose become the answer.
|
|
3055
3402
|
repairNarrative = repaired.text?.trim() || undefined;
|
|
@@ -3282,6 +3629,7 @@ async function runAnswerLoop(input) {
|
|
|
3282
3629
|
contextPack: input.contextPack,
|
|
3283
3630
|
considered,
|
|
3284
3631
|
providerUsed: provider.name,
|
|
3632
|
+
aggregationSafetyProof,
|
|
3285
3633
|
// Carry the governed metric match so the exit point can name a
|
|
3286
3634
|
// `semantic_metric` route (spec 17, part C).
|
|
3287
3635
|
_semanticMetricMatch: governedMetricAnswer ? semanticMetricMatch ?? undefined : undefined,
|
|
@@ -3322,6 +3670,23 @@ function resultColumnNames(result) {
|
|
|
3322
3670
|
return columns?.length ? columns : undefined;
|
|
3323
3671
|
}
|
|
3324
3672
|
function analyticalCapabilityForPlan(plan, contextPack, registry = []) {
|
|
3673
|
+
const selected = normalizeMetricCapabilityContract(plan.selectedCapability);
|
|
3674
|
+
if (selected) {
|
|
3675
|
+
const selectedMetricId = plan.analyticalFrame?.metricConceptIds[0]
|
|
3676
|
+
?? plan.query.measures[0]?.qualifiedId;
|
|
3677
|
+
const exactRegistryAuthority = registry.some((entry) => Boolean(plan.executionId)
|
|
3678
|
+
&& entry.identities.includes(plan.executionId)
|
|
3679
|
+
&& normalizeMetricCapabilityContract(entry.node.payload?.analyticalCapability)?.sourceFingerprint
|
|
3680
|
+
=== selected.sourceFingerprint);
|
|
3681
|
+
if (plan.selectedCapabilityFingerprint !== selected.sourceFingerprint
|
|
3682
|
+
|| (selectedMetricId && selected.metricId !== selectedMetricId && !exactRegistryAuthority))
|
|
3683
|
+
return undefined;
|
|
3684
|
+
return selected;
|
|
3685
|
+
}
|
|
3686
|
+
// New authoritative v2 plans always carry the exact normalized capability.
|
|
3687
|
+
// A missing authority is terminal rather than permission to rematch by name.
|
|
3688
|
+
if (plan.mode === 'authoritative' && plan.schemaVersion === 2)
|
|
3689
|
+
return undefined;
|
|
3325
3690
|
const candidates = contextPack?.retrievalDiagnostics.meaningEvidence?.candidates ?? [];
|
|
3326
3691
|
const exactExecution = candidates.filter((candidate) => candidate.objectKey === plan.executionId || candidate.qualifiedId === plan.executionId);
|
|
3327
3692
|
const metricId = plan.analyticalFrame?.metricConceptIds[0];
|
|
@@ -3343,6 +3708,142 @@ function analyticalCapabilityForPlan(plan, contextPack, registry = []) {
|
|
|
3343
3708
|
const byFingerprint = new Map(normalized.map((capability) => [capability.sourceFingerprint, capability]));
|
|
3344
3709
|
return byFingerprint.size === 1 ? [...byFingerprint.values()][0] : undefined;
|
|
3345
3710
|
}
|
|
3711
|
+
function semanticRouteAggregationProofContext(contextPack, plan, capability, exactMetricObjects = []) {
|
|
3712
|
+
if (!plan || !capability) {
|
|
3713
|
+
return exactMetricObjects.length > 0
|
|
3714
|
+
? { ...(contextPack ?? {}), objects: exactMetricObjects }
|
|
3715
|
+
: contextPack;
|
|
3716
|
+
}
|
|
3717
|
+
const baseQuestionPlan = buildAnalysisQuestionPlan(plan.question);
|
|
3718
|
+
if ((plan.selectedCapabilityFingerprint !== undefined
|
|
3719
|
+
&& plan.selectedCapabilityFingerprint !== capability.sourceFingerprint)
|
|
3720
|
+
|| (plan.analyticalFrame !== undefined
|
|
3721
|
+
&& plan.analyticalFrame.metricConceptIds[0] !== capability.metricId))
|
|
3722
|
+
return {
|
|
3723
|
+
...(contextPack ?? {}),
|
|
3724
|
+
objects: [],
|
|
3725
|
+
questionPlan: {
|
|
3726
|
+
...baseQuestionPlan,
|
|
3727
|
+
requestedShape: { ...baseQuestionPlan.requestedShape, dimensions: [] },
|
|
3728
|
+
},
|
|
3729
|
+
};
|
|
3730
|
+
const contextMetricObjects = (contextPack?.objects ?? []).filter((object) => {
|
|
3731
|
+
const objectCapability = normalizeMetricCapabilityContract(object.payload?.analyticalCapability);
|
|
3732
|
+
if (!objectCapability || objectCapability.sourceFingerprint !== capability.sourceFingerprint)
|
|
3733
|
+
return false;
|
|
3734
|
+
return [object.objectKey, object.fullName, object.name, object.payload?.qualifiedId]
|
|
3735
|
+
.filter((value) => Boolean(value))
|
|
3736
|
+
.includes(plan.executionId ?? capability.metricId);
|
|
3737
|
+
});
|
|
3738
|
+
const relationshipIds = new Set(plan.relationshipPathIds);
|
|
3739
|
+
const relationshipObjects = (contextPack?.objects ?? []).filter((object) => object.objectType.includes('relationship')
|
|
3740
|
+
&& [object.objectKey, object.fullName, object.name, object.payload?.qualifiedId]
|
|
3741
|
+
.filter((value) => Boolean(value))
|
|
3742
|
+
.some((value) => relationshipIds.has(value)));
|
|
3743
|
+
const frozenDimensions = plan.analyticalFrame?.dimensions
|
|
3744
|
+
.filter((dimension) => dimension.role === 'group_by' || dimension.role === 'rank_entity')
|
|
3745
|
+
.map((dimension) => dimension.dimensionId);
|
|
3746
|
+
const legacyCapabilityDimensions = !plan.analyticalFrame && plan.query.dimensions.length > 0
|
|
3747
|
+
? capability.dimensions.filter((dimension) => dimension.supportedRoles.includes('group_by'))
|
|
3748
|
+
: [];
|
|
3749
|
+
const dimensions = frozenDimensions
|
|
3750
|
+
?? (legacyCapabilityDimensions.length === plan.query.dimensions.length
|
|
3751
|
+
? legacyCapabilityDimensions.map((dimension) => dimension.dimensionId)
|
|
3752
|
+
: plan.query.dimensions.flatMap((dimension) => dimension.qualifiedId ? [dimension.qualifiedId] : []));
|
|
3753
|
+
const legacyGrains = legacyCapabilityDimensions.map((dimension) => dimension.entityId)
|
|
3754
|
+
.filter((entityId) => capability.resultGrainIds.includes(entityId));
|
|
3755
|
+
const grain = plan.analyticalFrame?.entityGrainIds[0]
|
|
3756
|
+
?? plan.entityGrain
|
|
3757
|
+
?? (legacyGrains.length === 1 ? legacyGrains[0] : undefined)
|
|
3758
|
+
?? (dimensions.length === 0 ? capability.defaultResultGrainId : undefined);
|
|
3759
|
+
const exactObjects = new Map();
|
|
3760
|
+
for (const object of [...contextMetricObjects, ...exactMetricObjects, ...relationshipObjects]) {
|
|
3761
|
+
const authorityKey = `${object.objectKey}:${normalizeMetricCapabilityContract(object.payload?.analyticalCapability)?.sourceFingerprint ?? ''}`;
|
|
3762
|
+
if (!exactObjects.has(authorityKey))
|
|
3763
|
+
exactObjects.set(authorityKey, object);
|
|
3764
|
+
}
|
|
3765
|
+
return {
|
|
3766
|
+
...(contextPack ?? {}),
|
|
3767
|
+
objects: [...exactObjects.values()],
|
|
3768
|
+
questionPlan: {
|
|
3769
|
+
...baseQuestionPlan,
|
|
3770
|
+
requestedShape: {
|
|
3771
|
+
...baseQuestionPlan.requestedShape,
|
|
3772
|
+
dimensions,
|
|
3773
|
+
...(grain ? { grain } : {}),
|
|
3774
|
+
},
|
|
3775
|
+
},
|
|
3776
|
+
};
|
|
3777
|
+
}
|
|
3778
|
+
function exactSemanticProofAuthority(plan, metricNodes) {
|
|
3779
|
+
if (!plan)
|
|
3780
|
+
return { metricObjects: [] };
|
|
3781
|
+
const embedded = normalizeMetricCapabilityContract(plan.selectedCapability);
|
|
3782
|
+
const planMetricIds = new Set([
|
|
3783
|
+
plan.executionId,
|
|
3784
|
+
...plan.query.measures.flatMap((binding) => binding.qualifiedId ? [binding.qualifiedId] : []),
|
|
3785
|
+
].filter((value) => Boolean(value)));
|
|
3786
|
+
const exactNodes = metricNodes.filter((metric) => {
|
|
3787
|
+
const identities = [
|
|
3788
|
+
metric.nodeId,
|
|
3789
|
+
metric.payload?.qualifiedId,
|
|
3790
|
+
metric.payload?.localId,
|
|
3791
|
+
].filter((value) => typeof value === 'string' && Boolean(value));
|
|
3792
|
+
return identities.some((identity) => planMetricIds.has(identity));
|
|
3793
|
+
});
|
|
3794
|
+
const capabilities = exactNodes.flatMap((metric) => {
|
|
3795
|
+
const capability = normalizeMetricCapabilityContract(metric.payload?.analyticalCapability);
|
|
3796
|
+
return capability ? [capability] : [];
|
|
3797
|
+
});
|
|
3798
|
+
const selected = embedded ?? (new Map(capabilities.map((capability) => [capability.sourceFingerprint, capability])).size === 1
|
|
3799
|
+
? capabilities[0]
|
|
3800
|
+
: undefined);
|
|
3801
|
+
if (!selected || (plan.selectedCapabilityFingerprint && plan.selectedCapabilityFingerprint !== selected.sourceFingerprint)) {
|
|
3802
|
+
return { metricObjects: [] };
|
|
3803
|
+
}
|
|
3804
|
+
return {
|
|
3805
|
+
capability: selected,
|
|
3806
|
+
metricObjects: exactNodes.flatMap((metric) => metric.payload ? [{
|
|
3807
|
+
objectKey: metric.nodeId,
|
|
3808
|
+
objectType: 'semantic_metric',
|
|
3809
|
+
name: metric.name,
|
|
3810
|
+
...(metric.domain ? { domain: metric.domain } : {}),
|
|
3811
|
+
...(metric.status ? { status: metric.status } : {}),
|
|
3812
|
+
payload: metric.payload,
|
|
3813
|
+
}] : []),
|
|
3814
|
+
};
|
|
3815
|
+
}
|
|
3816
|
+
function exactToolSemanticProofAuthority(metricNames, metricNodes) {
|
|
3817
|
+
const exactNodes = [...new Set(metricNames)].flatMap((name) => {
|
|
3818
|
+
const matches = metricNodes.filter((node) => node.name === name || node.name.endsWith(`.${name}`) || name.endsWith(`.${node.name}`));
|
|
3819
|
+
return matches.length === 1 ? matches : [];
|
|
3820
|
+
});
|
|
3821
|
+
if (exactNodes.length !== new Set(metricNames).size)
|
|
3822
|
+
return { metricObjects: [] };
|
|
3823
|
+
const capabilities = exactNodes.flatMap((node) => {
|
|
3824
|
+
const capability = normalizeMetricCapabilityContract(node.payload?.analyticalCapability);
|
|
3825
|
+
return capability ? [capability] : [];
|
|
3826
|
+
});
|
|
3827
|
+
if (capabilities.length !== exactNodes.length)
|
|
3828
|
+
return { metricObjects: [] };
|
|
3829
|
+
return {
|
|
3830
|
+
capability: capabilities[0],
|
|
3831
|
+
metricObjects: exactNodes.map((metric) => ({
|
|
3832
|
+
objectKey: metric.nodeId,
|
|
3833
|
+
objectType: 'semantic_metric',
|
|
3834
|
+
name: metric.name,
|
|
3835
|
+
...(metric.domain ? { domain: metric.domain } : {}),
|
|
3836
|
+
...(metric.status ? { status: metric.status } : {}),
|
|
3837
|
+
payload: metric.payload,
|
|
3838
|
+
})),
|
|
3839
|
+
};
|
|
3840
|
+
}
|
|
3841
|
+
function semanticAggregationSafetyFailure(proof) {
|
|
3842
|
+
const issues = proof?.issueCodes.length
|
|
3843
|
+
? proof.issueCodes.join(', ')
|
|
3844
|
+
: 'AGGREGATION_PROOF_MISSING';
|
|
3845
|
+
return `The selected semantic route did not establish an exact aggregation safety proof (${issues}). No SQL was executed.`;
|
|
3846
|
+
}
|
|
3346
3847
|
function analyticalRouteForPlan(plan) {
|
|
3347
3848
|
if (plan.capability === 'certified_execution' && plan.recommendedRoute === 'certified')
|
|
3348
3849
|
return 'certified';
|
|
@@ -3440,6 +3941,8 @@ async function executeSemanticAnalyticalGraph(input) {
|
|
|
3440
3941
|
const compiledSql = [];
|
|
3441
3942
|
const artifacts = [];
|
|
3442
3943
|
let semanticExecutionTrace;
|
|
3944
|
+
let aggregationCompilerReceipt;
|
|
3945
|
+
let aggregationSafetyProof;
|
|
3443
3946
|
for (const invocation of input.binding.invocations) {
|
|
3444
3947
|
const attemptedArtifact = semanticAttemptArtifact(layer, input.input.question, invocation.selection);
|
|
3445
3948
|
let composed = composeSemanticQueryFromMembers({
|
|
@@ -3453,6 +3956,7 @@ async function executeSemanticAnalyticalGraph(input) {
|
|
|
3453
3956
|
try {
|
|
3454
3957
|
const compiled = await input.input.semanticQueryCompiler(invocation.selection);
|
|
3455
3958
|
semanticExecutionTrace = compiled.trace;
|
|
3959
|
+
aggregationCompilerReceipt = compiled.aggregationCompilerReceipt;
|
|
3456
3960
|
composed = composeSemanticQueryFromCompiledMembers({
|
|
3457
3961
|
semanticLayer: layer,
|
|
3458
3962
|
question: input.input.question,
|
|
@@ -3503,6 +4007,42 @@ async function executeSemanticAnalyticalGraph(input) {
|
|
|
3503
4007
|
failedBindings: [{ role: 'source_invocation', reasonCode: 'SEMANTIC_COMPILE_FAILED' }],
|
|
3504
4008
|
});
|
|
3505
4009
|
}
|
|
4010
|
+
const compilerProof = semanticRatioCompilerProofBinding(layer, invocation.selection.metrics, input.capability, input.input.resolvedAnalyticalPlan, semanticTraceTargetFingerprint(semanticExecutionTrace), composed.sql, aggregationCompilerReceipt);
|
|
4011
|
+
const proof = buildSemanticCompilationAggregationSafetyProof({
|
|
4012
|
+
sql: composed.sql,
|
|
4013
|
+
capability: input.capability,
|
|
4014
|
+
semanticAuthority: input.input.resolvedAnalyticalPlan
|
|
4015
|
+
? buildSemanticProofAuthorityV1({
|
|
4016
|
+
plan: input.input.resolvedAnalyticalPlan,
|
|
4017
|
+
capability: input.capability,
|
|
4018
|
+
contextPack: input.input.contextPack,
|
|
4019
|
+
compilerReceipt: aggregationCompilerReceipt,
|
|
4020
|
+
})
|
|
4021
|
+
: undefined,
|
|
4022
|
+
relationshipProofs: input.input.resolvedAnalyticalPlan?.relationshipProofs,
|
|
4023
|
+
executionId: input.input.resolvedAnalyticalPlan?.executionId,
|
|
4024
|
+
snapshotId: input.input.resolvedAnalyticalPlan?.snapshotId,
|
|
4025
|
+
capabilityFingerprint: input.input.resolvedAnalyticalPlan?.selectedCapabilityFingerprint,
|
|
4026
|
+
route: 'semantic',
|
|
4027
|
+
adapterId: input.graph.adapterId,
|
|
4028
|
+
...compilerProof.binding,
|
|
4029
|
+
...(compilerProof.issueCodes.length
|
|
4030
|
+
? { compilerAuthorityIssueCodes: compilerProof.issueCodes }
|
|
4031
|
+
: {}),
|
|
4032
|
+
contextPack: semanticRouteAggregationProofContext(input.input.contextPack, input.input.resolvedAnalyticalPlan, input.capability),
|
|
4033
|
+
dialect: input.input.semanticDriver,
|
|
4034
|
+
planFingerprint: input.binding.planFingerprint,
|
|
4035
|
+
});
|
|
4036
|
+
aggregationSafetyProof = proof;
|
|
4037
|
+
if (proof.status !== 'safe') {
|
|
4038
|
+
return analyticalGraphFailureAnswer(input, 'EXECUTION_GRAPH_MISMATCH', semanticAggregationSafetyFailure(proof), {
|
|
4039
|
+
phase: 'validation',
|
|
4040
|
+
dqlArtifact: composed.dqlArtifact,
|
|
4041
|
+
compiledSql: composed.sql,
|
|
4042
|
+
semanticExecutionTrace,
|
|
4043
|
+
aggregationSafetyProof: proof,
|
|
4044
|
+
});
|
|
4045
|
+
}
|
|
3506
4046
|
compiledSql.push(composed.sql);
|
|
3507
4047
|
artifacts.push(composed.dqlArtifact);
|
|
3508
4048
|
const executor = input.input.executeDqlArtifact
|
|
@@ -3562,6 +4102,7 @@ async function executeSemanticAnalyticalGraph(input) {
|
|
|
3562
4102
|
sql,
|
|
3563
4103
|
...(artifacts.length === 1 ? { dqlArtifact: artifacts[0] } : {}),
|
|
3564
4104
|
analyticalExecutionGraph: input.graph,
|
|
4105
|
+
aggregationSafetyProof,
|
|
3565
4106
|
...(semanticExecutionTrace
|
|
3566
4107
|
? { semanticExecutionTrace: semanticTraceAfterExecution(semanticExecutionTrace, { executed: false }) }
|
|
3567
4108
|
: {}),
|
|
@@ -3606,6 +4147,7 @@ async function executeSemanticAnalyticalGraph(input) {
|
|
|
3606
4147
|
...(artifacts.length === 1 ? { dqlArtifact: artifacts[0] } : {}),
|
|
3607
4148
|
result,
|
|
3608
4149
|
analyticalExecutionGraph: input.graph,
|
|
4150
|
+
aggregationSafetyProof,
|
|
3609
4151
|
analyticalExecutionReceipt: executed.receipt,
|
|
3610
4152
|
analyticalFacts: story.factSet,
|
|
3611
4153
|
analyticalNarrative: story.narrative,
|
|
@@ -3618,6 +4160,137 @@ async function executeSemanticAnalyticalGraph(input) {
|
|
|
3618
4160
|
providerUsed: input.providerName,
|
|
3619
4161
|
};
|
|
3620
4162
|
}
|
|
4163
|
+
function semanticRatioCompilerProofBinding(layer, selectedMetricNames, capability, plan, targetFingerprint, compiledSql, receipt) {
|
|
4164
|
+
if (capability.aggregation.trim().toLowerCase() !== 'ratio')
|
|
4165
|
+
return { issueCodes: [] };
|
|
4166
|
+
if (receipt) {
|
|
4167
|
+
const outputAlias = capability.declaredOutputIds?.length === 1
|
|
4168
|
+
? capability.declaredOutputIds[0]
|
|
4169
|
+
: capability.metricId.split(/[:.]/).at(-1);
|
|
4170
|
+
const { receiptFingerprint, ...receiptBody } = receipt;
|
|
4171
|
+
const compiledExpression = outputAlias
|
|
4172
|
+
? buildSqlOutputExpressionSignature(compiledSql, outputAlias)
|
|
4173
|
+
: undefined;
|
|
4174
|
+
const dimensionIds = [...new Set(plan.analyticalFrame?.dimensions.map((dimension) => dimension.dimensionId) ?? [])].sort();
|
|
4175
|
+
const relationshipPathIds = [...plan.relationshipPathIds].sort();
|
|
4176
|
+
const relationshipProofFingerprints = [...new Set(plan.relationshipProofs?.map((proof) => proof.authorityFingerprint) ?? [])].sort();
|
|
4177
|
+
const expectedOrderBy = semanticCompilerReceiptOrder(plan, capability);
|
|
4178
|
+
if (receipt.version !== 1
|
|
4179
|
+
|| receipt.executionId !== plan.executionId
|
|
4180
|
+
|| receipt.capabilityFingerprint !== plan.selectedCapabilityFingerprint
|
|
4181
|
+
|| receipt.metricId !== capability.metricId
|
|
4182
|
+
|| receipt.operator !== 'ratio'
|
|
4183
|
+
|| receipt.planFingerprint !== plan.fingerprint
|
|
4184
|
+
|| receipt.snapshotId !== plan.snapshotId
|
|
4185
|
+
|| !targetFingerprint
|
|
4186
|
+
|| receipt.targetFingerprint !== targetFingerprint
|
|
4187
|
+
|| receipt.compiledSqlFingerprint !== semanticExecutionFingerprint(compiledSql)
|
|
4188
|
+
|| !compiledExpression
|
|
4189
|
+
|| receipt.compiledExpressionFingerprint !== semanticExecutionFingerprint(compiledExpression.canonicalExpression)
|
|
4190
|
+
|| receipt.orderedMeasureIds.join('|') !== capability.measureIds.join('|')
|
|
4191
|
+
|| [...receipt.dimensionIds].sort().join('|') !== dimensionIds.join('|')
|
|
4192
|
+
|| [...receipt.relationshipPathIds].sort().join('|') !== relationshipPathIds.join('|')
|
|
4193
|
+
|| [...receipt.relationshipProofFingerprints].sort().join('|') !== relationshipProofFingerprints.join('|')
|
|
4194
|
+
|| receipt.orderBy.length !== expectedOrderBy.length
|
|
4195
|
+
|| receipt.orderBy.some((order, index) => {
|
|
4196
|
+
const expected = expectedOrderBy[index];
|
|
4197
|
+
return !expected
|
|
4198
|
+
|| normalizeSemanticProofId(order.expression) !== normalizeSemanticProofId(expected.expression)
|
|
4199
|
+
|| order.direction !== expected.direction;
|
|
4200
|
+
})
|
|
4201
|
+
|| receipt.outputAlias !== outputAlias
|
|
4202
|
+
|| receiptFingerprint !== semanticExecutionFingerprint(receiptBody)) {
|
|
4203
|
+
return { issueCodes: ['SEMANTIC_COMPILER_RECEIPT_AUTHORITY_MISMATCH'] };
|
|
4204
|
+
}
|
|
4205
|
+
return {
|
|
4206
|
+
binding: {
|
|
4207
|
+
compilerMetricExpressionSql: receipt.metricExpressionSql,
|
|
4208
|
+
compilerMetricId: receipt.metricId,
|
|
4209
|
+
compilerMeasureIds: receipt.orderedMeasureIds,
|
|
4210
|
+
compilerRelation: receipt.physicalRelation,
|
|
4211
|
+
compilerRelationAliases: receipt.relationAliases,
|
|
4212
|
+
},
|
|
4213
|
+
issueCodes: [],
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
const selected = new Set(selectedMetricNames.map(normalizeSemanticProofId));
|
|
4217
|
+
const capabilityMetric = normalizeSemanticProofId(capability.metricId);
|
|
4218
|
+
const metrics = layer.listMetrics(undefined, { includeMeasures: false }).filter((metric) => {
|
|
4219
|
+
const names = [metric.name, metric.cube ? `${metric.cube}.${metric.name}` : undefined]
|
|
4220
|
+
.filter((value) => Boolean(value))
|
|
4221
|
+
.map(normalizeSemanticProofId);
|
|
4222
|
+
return names.includes(capabilityMetric)
|
|
4223
|
+
&& names.some((name) => selected.has(name));
|
|
4224
|
+
});
|
|
4225
|
+
if (metrics.length !== 1 || !metrics[0].sql.trim())
|
|
4226
|
+
return { issueCodes: ['SEMANTIC_COMPILER_RECEIPT_AUTHORITY_MISSING'] };
|
|
4227
|
+
const metric = metrics[0];
|
|
4228
|
+
const measures = capability.measureIds.map((measureId) => {
|
|
4229
|
+
const normalizedId = normalizeSemanticProofId(measureId);
|
|
4230
|
+
const matches = layer.listMeasures().filter((measure) => {
|
|
4231
|
+
const names = [measure.name, measure.cube ? `${measure.cube}.${measure.name}` : undefined]
|
|
4232
|
+
.filter((value) => Boolean(value))
|
|
4233
|
+
.map(normalizeSemanticProofId);
|
|
4234
|
+
return names.includes(normalizedId)
|
|
4235
|
+
&& (!metric.cube || !measure.cube || measure.cube === metric.cube);
|
|
4236
|
+
});
|
|
4237
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
4238
|
+
});
|
|
4239
|
+
if (measures.some((measure) => !measure))
|
|
4240
|
+
return { issueCodes: ['SEMANTIC_COMPILER_RECEIPT_AUTHORITY_MISSING'] };
|
|
4241
|
+
const relations = Array.from(new Set(measures.map((measure) => measure.table.trim()).filter(Boolean)));
|
|
4242
|
+
if (relations.length !== 1)
|
|
4243
|
+
return { issueCodes: ['SEMANTIC_COMPILER_RECEIPT_AUTHORITY_MISSING'] };
|
|
4244
|
+
return {
|
|
4245
|
+
binding: {
|
|
4246
|
+
compilerMetricExpressionSql: metric.sql,
|
|
4247
|
+
compilerMetricId: capability.metricId,
|
|
4248
|
+
compilerMeasureIds: [...capability.measureIds],
|
|
4249
|
+
compilerRelation: relations[0],
|
|
4250
|
+
...(metric.cube ? { compilerRelationAliases: [metric.cube] } : {}),
|
|
4251
|
+
},
|
|
4252
|
+
issueCodes: [],
|
|
4253
|
+
};
|
|
4254
|
+
}
|
|
4255
|
+
function semanticCompilerReceiptOrder(plan, capability) {
|
|
4256
|
+
const ranking = plan.analyticalFrame?.ranking;
|
|
4257
|
+
if (!ranking || ranking.byMetricId !== capability.metricId)
|
|
4258
|
+
return [];
|
|
4259
|
+
const metricOutput = plan.analyticalFrame?.requestedOutputs.find((output) => output.kind === 'metric_value'
|
|
4260
|
+
&& output.metricId === ranking.byMetricId
|
|
4261
|
+
&& (!ranking.byPeriodId || output.periodId === ranking.byPeriodId));
|
|
4262
|
+
if (!metricOutput)
|
|
4263
|
+
return [];
|
|
4264
|
+
const order = [{ expression: metricOutput.id, direction: ranking.direction }];
|
|
4265
|
+
if (ranking.tiePolicy !== 'stable_secondary_key')
|
|
4266
|
+
return order;
|
|
4267
|
+
const dimensionOutputs = plan.analyticalFrame.requestedOutputs
|
|
4268
|
+
.filter((output) => output.kind === 'dimension');
|
|
4269
|
+
return [
|
|
4270
|
+
...order,
|
|
4271
|
+
...dimensionOutputs.map((output) => ({ expression: output.id, direction: 'asc' })),
|
|
4272
|
+
];
|
|
4273
|
+
}
|
|
4274
|
+
function semanticTraceTargetFingerprint(trace) {
|
|
4275
|
+
const targetBinding = trace?.targetBinding;
|
|
4276
|
+
if (!targetBinding || typeof targetBinding !== 'object' || Array.isArray(targetBinding))
|
|
4277
|
+
return undefined;
|
|
4278
|
+
const executionTarget = targetBinding.executionTarget;
|
|
4279
|
+
if (!executionTarget || typeof executionTarget !== 'object' || Array.isArray(executionTarget))
|
|
4280
|
+
return undefined;
|
|
4281
|
+
const identityFingerprint = executionTarget.identityFingerprint;
|
|
4282
|
+
return typeof identityFingerprint === 'string' && identityFingerprint.trim()
|
|
4283
|
+
? identityFingerprint
|
|
4284
|
+
: undefined;
|
|
4285
|
+
}
|
|
4286
|
+
function normalizeSemanticProofId(value) {
|
|
4287
|
+
return value
|
|
4288
|
+
.trim()
|
|
4289
|
+
.toLowerCase()
|
|
4290
|
+
.replace(/^semantic(?:::|:)/, '')
|
|
4291
|
+
.replace(/^(?:metric|measure)[.:]/, '')
|
|
4292
|
+
.replaceAll(':', '.');
|
|
4293
|
+
}
|
|
3621
4294
|
async function executeGovernedRelationalAnalyticalGraph(input) {
|
|
3622
4295
|
const sourceResults = {};
|
|
3623
4296
|
const artifacts = [];
|
|
@@ -3822,6 +4495,7 @@ function analyticalGraphFailureAnswer(input, code, reason, artifacts = {}) {
|
|
|
3822
4495
|
...(artifacts.compiledSql ? { proposedSql: artifacts.compiledSql, sql: artifacts.compiledSql } : {}),
|
|
3823
4496
|
...(artifacts.dqlArtifact ? { dqlArtifact: artifacts.dqlArtifact } : {}),
|
|
3824
4497
|
...(artifacts.semanticExecutionTrace ? { semanticExecutionTrace: artifacts.semanticExecutionTrace } : {}),
|
|
4498
|
+
...(artifacts.aggregationSafetyProof ? { aggregationSafetyProof: artifacts.aggregationSafetyProof } : {}),
|
|
3825
4499
|
citations: contextPackCitations(input.input.contextPack, 8),
|
|
3826
4500
|
considered: input.considered,
|
|
3827
4501
|
contextPack: input.input.contextPack,
|
|
@@ -3834,7 +4508,7 @@ function analyticalFailureForInput(input, diagnostic) {
|
|
|
3834
4508
|
error: diagnostic.error,
|
|
3835
4509
|
code: diagnostic.code,
|
|
3836
4510
|
phase: diagnostic.phase,
|
|
3837
|
-
snapshotId: plan?.snapshotId ?? input.contextPack?.knowledgeLens
|
|
4511
|
+
snapshotId: plan?.snapshotId ?? input.contextPack?.knowledgeLens?.snapshotId ?? 'snapshot-unavailable',
|
|
3838
4512
|
runId: diagnostic.runId ?? plan?.planId,
|
|
3839
4513
|
planFingerprint: plan?.fingerprint,
|
|
3840
4514
|
dqlSource: diagnostic.dqlArtifact?.source,
|
|
@@ -4377,6 +5051,77 @@ function renderContextPrompt(blocks, businessContext, others, activeTier, memory
|
|
|
4377
5051
|
: '';
|
|
4378
5052
|
return `${intentSection}${budgetSection}${dialectSection}\n\n${blockSection}${businessSection}${otherSection}${kgJoinSection}${schemaSection}${contextPackSection}${memorySection}${hintsSection}${conversationSection}${extraSection}${followUpSection}`;
|
|
4379
5053
|
}
|
|
5054
|
+
const ATTRIBUTE_LOOKUP_CONCEPTS = new Set([
|
|
5055
|
+
'account', 'category', 'channel', 'city', 'country', 'department', 'description',
|
|
5056
|
+
'industry', 'label', 'location', 'market', 'name', 'owner', 'region', 'segment',
|
|
5057
|
+
'state', 'status', 'team', 'tier', 'title', 'type', 'user',
|
|
5058
|
+
]);
|
|
5059
|
+
/**
|
|
5060
|
+
* Attribute questions are valid analytical asks even when they have no
|
|
5061
|
+
* measure. A conversational reference such as "what region does he belong to?"
|
|
5062
|
+
* carries a typed member binding from the prior result; forcing a metric here
|
|
5063
|
+
* turns a straightforward dimensional lookup into the old generic governed
|
|
5064
|
+
* error. Keep this as a generation hint, not a deterministic semantic match:
|
|
5065
|
+
* the provider still has to choose a relation and the SQL guard still proves
|
|
5066
|
+
* the resulting query. AGT-031.
|
|
5067
|
+
*/
|
|
5068
|
+
function isMetricIndependentAttributeLookup(question, questionPlan, followUp) {
|
|
5069
|
+
if (questionPlan.mode === 'ranking' || questionPlan.mode === 'comparison' || questionPlan.mode === 'definition')
|
|
5070
|
+
return false;
|
|
5071
|
+
const lower = question.toLowerCase();
|
|
5072
|
+
// Follow-up planning intentionally carries the previous measure so that a
|
|
5073
|
+
// refinement such as "and by region?" remains grounded. That carry must not
|
|
5074
|
+
// turn a new attribute request into a metric request. Only a measure named in
|
|
5075
|
+
// this question (or a non-carried custom term) disables this lane.
|
|
5076
|
+
const carriedTerms = new Set([
|
|
5077
|
+
...(followUp?.priorMeasures ?? []),
|
|
5078
|
+
...(followUp?.priorResultColumns ?? []),
|
|
5079
|
+
...(followUp?.priorDqlArtifact?.metrics ?? []),
|
|
5080
|
+
].map((term) => term.toLowerCase().replace(/[_-]+/g, ' ').trim()));
|
|
5081
|
+
const carriedOnly = (term) => carriedTerms.has(term.toLowerCase().replace(/[_-]+/g, ' ').trim());
|
|
5082
|
+
const directMeasures = questionPlan.requestedShape.measures.filter((term) => !carriedOnly(term));
|
|
5083
|
+
const directMetricTerms = questionPlan.metricTerms.filter((term) => !carriedOnly(term));
|
|
5084
|
+
if (directMeasures.length > 0 || directMetricTerms.length > 0)
|
|
5085
|
+
return false;
|
|
5086
|
+
if (/\b(?:revenue|sales?|amount|spend|cost|margin|profit|orders?|count|quantity|units?|value|rate|score|usage|growth|churn|conversion|tax)\b/i.test(lower))
|
|
5087
|
+
return false;
|
|
5088
|
+
const attributeTerms = new Set([
|
|
5089
|
+
...questionPlan.dimensionTerms,
|
|
5090
|
+
...questionPlan.requestedShape.dimensions,
|
|
5091
|
+
...questionPlan.requestedShape.requiredOutputs,
|
|
5092
|
+
].map((term) => term.toLowerCase().replace(/[_-]+/g, ' ').trim()));
|
|
5093
|
+
const hasAttribute = Array.from(attributeTerms).some((term) => {
|
|
5094
|
+
if (!term)
|
|
5095
|
+
return false;
|
|
5096
|
+
return ATTRIBUTE_LOOKUP_CONCEPTS.has(term) || Array.from(ATTRIBUTE_LOOKUP_CONCEPTS).some((concept) => new RegExp(`\\b${concept}\\b`, 'i').test(term));
|
|
5097
|
+
}) || /\b(?:region|segment|category|channel|location|country|state|city|status|type|name|label|title|description)\b/i.test(lower);
|
|
5098
|
+
if (!hasAttribute)
|
|
5099
|
+
return false;
|
|
5100
|
+
const asksForAttribute = /\b(?:what|which|where|who|show|give|tell|find|list)\b/i.test(lower)
|
|
5101
|
+
|| /\b(?:belongs?|assigned|associated|located|based|part\s+of)\b/i.test(lower);
|
|
5102
|
+
if (!asksForAttribute)
|
|
5103
|
+
return false;
|
|
5104
|
+
const hasTarget = Boolean(followUp?.memberBindings?.length)
|
|
5105
|
+
|| questionPlan.entities.length > 0
|
|
5106
|
+
|| /\b(?:he|she|him|his|her|hers|this|that|these|those|same|above|previous|prior)\b/i.test(lower);
|
|
5107
|
+
return hasTarget;
|
|
5108
|
+
}
|
|
5109
|
+
function renderAttributeLookupInstruction(questionPlan, followUp) {
|
|
5110
|
+
const requested = Array.from(new Set([
|
|
5111
|
+
...questionPlan.dimensionTerms,
|
|
5112
|
+
...questionPlan.requestedShape.dimensions,
|
|
5113
|
+
...questionPlan.requestedShape.requiredOutputs,
|
|
5114
|
+
].filter((term) => ATTRIBUTE_LOOKUP_CONCEPTS.has(term.toLowerCase().replace(/[_-]+/g, ' ').trim()))));
|
|
5115
|
+
const bindings = followUp?.memberBindings?.map((binding) => `${binding.dimension} = ${binding.values.join(' | ')}`).join('; ');
|
|
5116
|
+
return [
|
|
5117
|
+
'METRIC-INDEPENDENT ATTRIBUTE LOOKUP (authoritative for this turn):',
|
|
5118
|
+
'This is a valid lookup of an entity attribute; it does not require a revenue, count, or other measure.',
|
|
5119
|
+
requested.length > 0 ? `Return the requested attribute column(s): ${requested.join(', ')}.` : 'Return the requested descriptive attribute column.',
|
|
5120
|
+
bindings ? `Preserve the typed conversation member binding exactly: ${bindings}.` : 'Preserve the explicitly named entity/member exactly.',
|
|
5121
|
+
'Use the inspected metadata and documented join keys to find the owning relation, select the attribute, and apply the member predicate. Do not invent a metric, add an arbitrary aggregate, or broaden a singular member to all prior rows.',
|
|
5122
|
+
'If the attribute or relationship is absent from inspected metadata, return no SQL and state the precise missing relation/column so the host can surface a typed gap.',
|
|
5123
|
+
].join('\n');
|
|
5124
|
+
}
|
|
4380
5125
|
function buildKgJoinPathHints(kg, nodes, questionPlan) {
|
|
4381
5126
|
const candidates = nodes
|
|
4382
5127
|
.filter((node) => ['block', 'business_view', 'metric', 'entity', 'semantic_model', 'dbt_model', 'dbt_source'].includes(node.kind))
|
|
@@ -6405,6 +7150,7 @@ async function generateProposalWithOptionalTools(input) {
|
|
|
6405
7150
|
...options,
|
|
6406
7151
|
toolPolicy,
|
|
6407
7152
|
maxToolCalls: toolBudget.maxToolCalls,
|
|
7153
|
+
providerPayloadGuard: input.providerPayloadGuard,
|
|
6408
7154
|
onToolCall: (event) => {
|
|
6409
7155
|
const sink = input.toolCalls;
|
|
6410
7156
|
if (sink)
|
|
@@ -6502,22 +7248,9 @@ async function scoreDeepGeneratedProposalCandidate(input, candidate) {
|
|
|
6502
7248
|
validationOk = validation.ok;
|
|
6503
7249
|
if (validation.ok) {
|
|
6504
7250
|
score += 100;
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
score += 40;
|
|
6509
|
-
if (result.rowCount > 0)
|
|
6510
|
-
score += 8;
|
|
6511
|
-
const resultShape = validateAnswerResultShape(input.questionPlan, result);
|
|
6512
|
-
score -= resultShape.warnings.length * 6;
|
|
6513
|
-
if (generatedResultShapeIsPartial(resultShape))
|
|
6514
|
-
score -= 120;
|
|
6515
|
-
}
|
|
6516
|
-
catch (error) {
|
|
6517
|
-
executionError = error instanceof Error ? error.message : String(error);
|
|
6518
|
-
score -= 20;
|
|
6519
|
-
}
|
|
6520
|
-
}
|
|
7251
|
+
// Budget authority: candidate comparison is parser/context validation
|
|
7252
|
+
// only. Executing each candidate would violate the one-SQL-call ordinary
|
|
7253
|
+
// Ask budget and make telemetry dependent on speculative work.
|
|
6521
7254
|
}
|
|
6522
7255
|
else {
|
|
6523
7256
|
validationError = validation.error;
|