@duckcodeailabs/dql-agent 1.9.5 → 1.10.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.
Files changed (61) hide show
  1. package/dist/agent-run-engine.d.ts +13 -0
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +59 -1
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/answer-loop.d.ts +34 -1
  6. package/dist/answer-loop.d.ts.map +1 -1
  7. package/dist/answer-loop.js +295 -35
  8. package/dist/answer-loop.js.map +1 -1
  9. package/dist/embeddings/provider.d.ts +1 -1
  10. package/dist/embeddings/provider.d.ts.map +1 -1
  11. package/dist/embeddings/provider.js.map +1 -1
  12. package/dist/governed-relational-compiler.d.ts +138 -0
  13. package/dist/governed-relational-compiler.d.ts.map +1 -0
  14. package/dist/governed-relational-compiler.js +315 -0
  15. package/dist/governed-relational-compiler.js.map +1 -0
  16. package/dist/index.d.ts +13 -5
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +8 -4
  19. package/dist/index.js.map +1 -1
  20. package/dist/intent-controller.d.ts +5 -0
  21. package/dist/intent-controller.d.ts.map +1 -1
  22. package/dist/intent-controller.js.map +1 -1
  23. package/dist/kg/build.d.ts.map +1 -1
  24. package/dist/kg/build.js +81 -1
  25. package/dist/kg/build.js.map +1 -1
  26. package/dist/meaning-resolution.d.ts +4 -0
  27. package/dist/meaning-resolution.d.ts.map +1 -1
  28. package/dist/meaning-resolution.js +14 -4
  29. package/dist/meaning-resolution.js.map +1 -1
  30. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  31. package/dist/metadata/analysis-planner.js +34 -6
  32. package/dist/metadata/analysis-planner.js.map +1 -1
  33. package/dist/metadata/catalog.d.ts +83 -2
  34. package/dist/metadata/catalog.d.ts.map +1 -1
  35. package/dist/metadata/catalog.js +381 -28
  36. package/dist/metadata/catalog.js.map +1 -1
  37. package/dist/metadata/meaning-evidence.d.ts +10 -1
  38. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  39. package/dist/metadata/meaning-evidence.js +78 -1
  40. package/dist/metadata/meaning-evidence.js.map +1 -1
  41. package/dist/plan-execution-adapter.d.ts +60 -0
  42. package/dist/plan-execution-adapter.d.ts.map +1 -0
  43. package/dist/plan-execution-adapter.js +239 -0
  44. package/dist/plan-execution-adapter.js.map +1 -0
  45. package/dist/research-governance.d.ts +56 -0
  46. package/dist/research-governance.d.ts.map +1 -0
  47. package/dist/research-governance.js +68 -0
  48. package/dist/research-governance.js.map +1 -0
  49. package/dist/research-loop.d.ts +14 -0
  50. package/dist/research-loop.d.ts.map +1 -1
  51. package/dist/research-loop.js +77 -23
  52. package/dist/research-loop.js.map +1 -1
  53. package/dist/resolved-analytical-plan.d.ts +112 -0
  54. package/dist/resolved-analytical-plan.d.ts.map +1 -0
  55. package/dist/resolved-analytical-plan.js +346 -0
  56. package/dist/resolved-analytical-plan.js.map +1 -0
  57. package/dist/router.d.ts +3 -0
  58. package/dist/router.d.ts.map +1 -1
  59. package/dist/router.js +34 -10
  60. package/dist/router.js.map +1 -1
  61. package/package.json +4 -4
@@ -41,6 +41,9 @@ import { questionTypeFromText } from './meaning-resolution.js';
41
41
  import { cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, } from './cascade/cascade.js';
42
42
  import { shouldClarifyBeforeGeneration } from './cascade/triage.js';
43
43
  import { stampTrustLabel } from './trust/stamp.js';
44
+ import { deriveResolvedAnalyticalPlan } from './resolved-analytical-plan.js';
45
+ import { adaptResolvedAnalyticalPlan, buildPlanExecutionRegistry, } from './plan-execution-adapter.js';
46
+ import { buildGovernedRelationalRegistry, compileGovernedRelationalPlan, finalizeGovernedCompilationReceipt, renderGovernedRelationalDqlArtifact, } from './governed-relational-compiler.js';
44
47
  import { QUICK_PROMPT_CONTEXT_BUDGET, canUseLaneRepair, cascadeBudgetTrace, createCascadeBudgetState, deepAlternativeCountForQuestion, promptContextBudgetForQuestion, proposalToolBudgetForQuestion, recordLaneRepair, } from './cascade/budgets.js';
45
48
  /**
46
49
  * Physical column names whose sampled runtime values include `value`. Used by the
@@ -460,13 +463,126 @@ function tryCrossResultAnswer(input) {
460
463
  }
461
464
  return answer;
462
465
  }
466
+ /**
467
+ * Materialize exactly the Skill IDs/hashes selected in the immutable context
468
+ * pack. No downstream answer route may rerun trigger/domain selection or add a
469
+ * Skill that was not recorded in the KnowledgeLens.
470
+ *
471
+ * Acceptance: SKILL-003, AGT-013.
472
+ */
473
+ export function materializeKnowledgeLensSkills(contextPack, available) {
474
+ const byIdentity = new Map();
475
+ for (const skill of available) {
476
+ byIdentity.set(skill.qualifiedId ?? skill.id, skill);
477
+ if (!byIdentity.has(skill.id))
478
+ byIdentity.set(skill.id, skill);
479
+ }
480
+ return (contextPack.skills ?? []).map((selected) => {
481
+ const identity = selected.qualifiedId ?? selected.id;
482
+ const source = byIdentity.get(identity) ?? byIdentity.get(selected.id);
483
+ if (source) {
484
+ // Guidance is the immutable, bounded snapshot body. Retain structured
485
+ // source fields but never reread or inject a newer disk body mid-run.
486
+ return { ...source, body: selected.guidance };
487
+ }
488
+ return {
489
+ id: selected.id,
490
+ localId: selected.id,
491
+ qualifiedId: selected.qualifiedId,
492
+ scope: 'project',
493
+ domain: selected.domain,
494
+ domains: selected.domains,
495
+ modelAreaRefs: selected.modelAreaRefs,
496
+ kind: selected.kind,
497
+ status: selected.status,
498
+ owner: selected.owner,
499
+ triggers: selected.triggers,
500
+ exclusions: selected.exclusions,
501
+ description: selected.description,
502
+ preferredMetrics: selected.preferredMetrics,
503
+ preferredBlocks: selected.preferredBlocks,
504
+ preferredDimensions: selected.preferredDimensions,
505
+ requiredFilters: selected.requiredFilters,
506
+ clarifyWhen: selected.clarifyWhen,
507
+ examples: [],
508
+ sourceRefs: selected.sourceRefs,
509
+ vocabulary: selected.vocabulary,
510
+ body: selected.guidance,
511
+ sourcePath: selected.sourcePath ?? `snapshot:${selected.objectKey}`,
512
+ };
513
+ });
514
+ }
463
515
  export async function answer(input) {
516
+ const inheritedPlan = !input.resolvedAnalyticalPlan
517
+ && input.followUp?.priorResolvedAnalyticalPlan
518
+ && input.followUp.resolvedAnalyticalPlanDelta
519
+ ? deriveResolvedAnalyticalPlan(input.followUp.priorResolvedAnalyticalPlan, input.followUp.resolvedAnalyticalPlanDelta)
520
+ : undefined;
521
+ const normalizedInput = inheritedPlan
522
+ ? { ...input, resolvedAnalyticalPlan: inheritedPlan }
523
+ : input;
524
+ const resolvedPlanExecutionBinding = normalizedInput.resolvedAnalyticalPlan
525
+ ? adaptResolvedAnalyticalPlan({
526
+ plan: normalizedInput.resolvedAnalyticalPlan,
527
+ registry: buildPlanExecutionRegistry({
528
+ nodes: [
529
+ ...normalizedInput.kg.getNodesByKind('block', 100_000),
530
+ ...normalizedInput.kg.getNodesByKind('metric', 100_000),
531
+ ...normalizedInput.kg.getNodesByKind('dimension', 100_000),
532
+ ],
533
+ objects: normalizedInput.contextPack?.objects,
534
+ }),
535
+ semanticLayer: normalizedInput.semanticLayer,
536
+ expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens.snapshotId,
537
+ })
538
+ : undefined;
539
+ const planBoundInput = resolvedPlanExecutionBinding
540
+ ? { ...normalizedInput, resolvedPlanExecutionBinding }
541
+ : normalizedInput;
542
+ const relationalSchemaContext = planBoundInput.schemaContext?.length
543
+ ? planBoundInput.schemaContext
544
+ : (planBoundInput.contextPack?.allowedSqlContext.relations ?? []).map((relation) => ({
545
+ relation: relation.relation,
546
+ name: relation.name,
547
+ columns: relation.columns.map((column) => ({
548
+ name: column.name,
549
+ type: column.type,
550
+ description: column.description,
551
+ })),
552
+ }));
553
+ const governedRelationalCompilation = planBoundInput.resolvedAnalyticalPlan?.mode === 'authoritative'
554
+ && planBoundInput.resolvedAnalyticalPlan.capability === 'governed_relational'
555
+ ? compileGovernedRelationalPlan({
556
+ plan: planBoundInput.resolvedAnalyticalPlan,
557
+ registry: buildGovernedRelationalRegistry({
558
+ snapshotId: planBoundInput.resolvedAnalyticalPlan.snapshotId,
559
+ schemaContext: relationalSchemaContext,
560
+ manifest: planBoundInput.manifest,
561
+ }),
562
+ driver: planBoundInput.semanticDriver,
563
+ })
564
+ : undefined;
565
+ const compiledInput = governedRelationalCompilation
566
+ ? { ...planBoundInput, governedRelationalCompilation }
567
+ : planBoundInput;
568
+ const executionInput = compiledInput.contextPack
569
+ ? {
570
+ ...compiledInput,
571
+ skills: materializeKnowledgeLensSkills(compiledInput.contextPack, compiledInput.skills ?? []),
572
+ skillsSelectionLocked: true,
573
+ }
574
+ : compiledInput;
464
575
  // Cross-result follow-up ("of these, the average") — computed from the prior
465
576
  // rows, before the cascade, so it never re-queries or times out.
466
- const crossResult = tryCrossResultAnswer(input);
467
- if (crossResult)
468
- return crossResult;
469
- const result = applyHollowAnswerGate(await runAnswerLoop(input));
577
+ const crossResult = tryCrossResultAnswer(executionInput);
578
+ if (crossResult) {
579
+ return {
580
+ ...crossResult,
581
+ resolvedAnalyticalPlan: executionInput.resolvedAnalyticalPlan,
582
+ executablePlan: executionInput.resolvedPlanExecutionBinding,
583
+ };
584
+ }
585
+ const result = applyHollowAnswerGate(await runAnswerLoop(executionInput));
470
586
  // Attach the canonical trust label once, at the single exit point, so every
471
587
  // return site inside runAnswerLoop stays untouched and backward compatible.
472
588
  // Freshness-aware trust: for a certified answer, fold the source block's data
@@ -495,6 +611,8 @@ export async function answer(input) {
495
611
  ...publicResult,
496
612
  domainContext: input.domainContext,
497
613
  intentDecision,
614
+ resolvedAnalyticalPlan: executionInput.resolvedAnalyticalPlan,
615
+ executablePlan: executionInput.resolvedPlanExecutionBinding,
498
616
  trustLabelInfo,
499
617
  provenanceFooter: buildProvenanceFooter(result, trustLabelInfo),
500
618
  cascade: publicResult.cascade ?? createCascadeAnswerResult({
@@ -514,15 +632,17 @@ export async function answer(input) {
514
632
  // Stamp the SELECTED skills that shaped the answer (transparency). Computed
515
633
  // here so every return site inside runAnswerLoop stays untouched.
516
634
  appliedSkills: result.appliedSkills ??
517
- selectRelevantSkills(input.skills ?? [], input.question, {
518
- userId: input.userId ?? null,
519
- modelAreaIds: input.domainContext?.modelAreaId ? [input.domainContext.modelAreaId] : [],
520
- domains: Array.from(new Set([
521
- ...domainContextSearchDomains(input.domainContext),
522
- ...(input.domain ? [input.domain] : []),
523
- ...(input.contextPack?.objects ?? []).slice(0, 20).flatMap((object) => object.domain ? [object.domain] : []),
524
- ])),
525
- }).map((s) => ({
635
+ (executionInput.skillsSelectionLocked
636
+ ? executionInput.skills ?? []
637
+ : selectRelevantSkills(executionInput.skills ?? [], executionInput.question, {
638
+ userId: executionInput.userId ?? null,
639
+ modelAreaIds: executionInput.domainContext?.modelAreaId ? [executionInput.domainContext.modelAreaId] : [],
640
+ domains: Array.from(new Set([
641
+ ...domainContextSearchDomains(executionInput.domainContext),
642
+ ...(executionInput.domain ? [executionInput.domain] : []),
643
+ ...(executionInput.contextPack?.objects ?? []).slice(0, 20).flatMap((object) => object.domain ? [object.domain] : []),
644
+ ])),
645
+ })).map((s) => ({
526
646
  id: s.id,
527
647
  description: s.description,
528
648
  })),
@@ -614,11 +734,13 @@ async function runAnswerLoop(input) {
614
734
  ? (input.contextPack?.objects ?? []).slice(0, 20).flatMap((object) => object.domain ? [object.domain] : [])
615
735
  : []),
616
736
  ]));
617
- const selectedSkills = selectRelevantSkills(skills, question, {
618
- userId: userId ?? null,
619
- domains: inferredDomains,
620
- modelAreaIds: input.domainContext?.modelAreaId ? [input.domainContext.modelAreaId] : [],
621
- });
737
+ const selectedSkills = input.skillsSelectionLocked
738
+ ? skills
739
+ : selectRelevantSkills(skills, question, {
740
+ userId: userId ?? null,
741
+ domains: inferredDomains,
742
+ modelAreaIds: input.domainContext?.modelAreaId ? [input.domainContext.modelAreaId] : [],
743
+ });
622
744
  const effectiveBlockHints = Array.from(new Set([
623
745
  ...blockHints,
624
746
  // Only selected skills may influence block ranking. Previously a preferred
@@ -654,6 +776,85 @@ async function runAnswerLoop(input) {
654
776
  // columns—not copy a customer-grain worked example into a product-grain ask.
655
777
  const promptContextPack = contextPackForRequestedShape(scopedContextPack, question, questionPlan, kg);
656
778
  const repairBudgetState = createCascadeBudgetState(input.cascadeBudgetModel);
779
+ const authoritativePlanBinding = input.resolvedAnalyticalPlan?.mode === 'authoritative'
780
+ ? input.resolvedPlanExecutionBinding
781
+ : undefined;
782
+ const governedRelationalCompilation = input.governedRelationalCompilation;
783
+ if (governedRelationalCompilation?.status === 'blocked') {
784
+ const text = `The governed relational plan cannot compile safely: ${governedRelationalCompilation.reason}`;
785
+ return {
786
+ kind: 'no_answer',
787
+ sourceTier: 'no_answer',
788
+ certification: 'analyst_review_required',
789
+ reviewStatus: 'none',
790
+ confidence: 0,
791
+ text,
792
+ answer: text,
793
+ refusalCode: governedRelationalCompilation.code.startsWith('RELATIONSHIP_') ? 'modeling_gap' : 'grounding_gap',
794
+ refusalDetails: { code: 'grounding_gap', message: `${governedRelationalCompilation.code}: ${governedRelationalCompilation.reason}` },
795
+ citations: contextPackCitations(input.contextPack, 8),
796
+ considered,
797
+ contextPack: input.contextPack,
798
+ providerUsed: provider.name,
799
+ };
800
+ }
801
+ if (governedRelationalCompilation?.status === 'compiled') {
802
+ const dqlArtifact = renderGovernedRelationalDqlArtifact(governedRelationalCompilation);
803
+ let result;
804
+ let executionError;
805
+ let receipt = governedRelationalCompilation.receipt;
806
+ if (input.executeDqlArtifact) {
807
+ try {
808
+ result = await input.executeDqlArtifact(dqlArtifact);
809
+ receipt = finalizeGovernedCompilationReceipt(receipt, result);
810
+ }
811
+ catch (error) {
812
+ executionError = error instanceof Error ? error.message : String(error);
813
+ }
814
+ }
815
+ const text = executionError
816
+ ? `The governed relational query compiled, but execution failed: ${executionError}`
817
+ : result
818
+ ? `Compiled and executed the snapshot-bound governed relational plan. Returned ${result.rowCount} row${result.rowCount === 1 ? '' : 's'}.`
819
+ : 'Compiled the snapshot-bound governed relational plan. Execution was not requested.';
820
+ return {
821
+ kind: executionError ? 'no_answer' : 'uncertified',
822
+ sourceTier: executionError ? 'no_answer' : 'dbt_manifest',
823
+ certification: executionError ? 'analyst_review_required' : 'governed',
824
+ reviewStatus: executionError ? 'analyst_review_required' : 'governed',
825
+ confidence: executionError ? 0 : 0.9,
826
+ text,
827
+ answer: text,
828
+ ...(executionError ? { executionError, refusalCode: 'grounding_gap' } : {}),
829
+ proposedSql: governedRelationalCompilation.sql,
830
+ sql: governedRelationalCompilation.sql,
831
+ dqlArtifact,
832
+ result,
833
+ governedCompilationReceipt: receipt,
834
+ citations: schemaCitations(schemaContext, 8),
835
+ considered,
836
+ contextPack: input.contextPack,
837
+ providerUsed: provider.name,
838
+ };
839
+ }
840
+ if (authoritativePlanBinding?.status === 'blocked') {
841
+ const text = `The resolved analytical plan cannot execute safely: ${authoritativePlanBinding.reason}`;
842
+ return {
843
+ kind: 'no_answer',
844
+ sourceTier: 'no_answer',
845
+ certification: 'analyst_review_required',
846
+ reviewStatus: 'none',
847
+ confidence: 0,
848
+ text,
849
+ answer: text,
850
+ refusalCode: authoritativePlanBinding.code === 'PLAN_BLOCKED' ? 'ambiguous' : 'grounding_gap',
851
+ refusalDetails: { code: authoritativePlanBinding.code, message: authoritativePlanBinding.reason },
852
+ citations: contextPackCitations(input.contextPack, 8),
853
+ considered,
854
+ contextPack: input.contextPack,
855
+ providerUsed: provider.name,
856
+ };
857
+ }
657
858
  const fallbackIntent = classifyAgentIntent({
658
859
  question,
659
860
  followUp: input.followUp,
@@ -673,16 +874,25 @@ async function runAnswerLoop(input) {
673
874
  if (node && !semanticMetricNodes.some((candidate) => candidate.nodeId === node.nodeId))
674
875
  semanticMetricNodes.push(node);
675
876
  }
676
- const preferredSemanticMetric = resolvePreferredSemanticMetric([input.preferredExecutionId, ...(input.preferredEvidenceIds ?? [])], semanticMetricNodes, kg);
877
+ const authoritativeSemanticBinding = authoritativePlanBinding?.status === 'ready'
878
+ && authoritativePlanBinding.kind === 'semantic'
879
+ ? authoritativePlanBinding
880
+ : undefined;
881
+ const preferredSemanticMetric = authoritativeSemanticBinding?.metricNode
882
+ ?? (input.resolvedAnalyticalPlan?.mode === 'authoritative'
883
+ ? undefined
884
+ : resolvePreferredSemanticMetric([input.preferredExecutionId, ...(input.preferredEvidenceIds ?? [])], semanticMetricNodes, kg));
677
885
  const semanticLayerForExec = input.semanticLayer;
678
886
  const canExecuteSemanticMetricForMatch = input.canExecuteSemanticMetric
679
887
  ?? (semanticLayerForExec ? (name) => semanticLayerForExec.canComposeMetric(name) : undefined);
680
888
  let semanticMetricMatch = preferredSemanticMetric
681
889
  ? { metric: preferredSemanticMetric, score: 1, basis: 'name' }
682
- : await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
683
- measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
684
- ...(canExecuteSemanticMetricForMatch ? { canExecute: canExecuteSemanticMetricForMatch } : {}),
685
- }).catch(() => null);
890
+ : input.resolvedAnalyticalPlan?.mode === 'authoritative'
891
+ ? null
892
+ : await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
893
+ measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
894
+ ...(canExecuteSemanticMetricForMatch ? { canExecute: canExecuteSemanticMetricForMatch } : {}),
895
+ }).catch(() => null);
686
896
  // Stage 1: certified artifact match. Blocks can be executed; dashboards,
687
897
  // Apps, and notebooks are returned as governed citations/navigation targets.
688
898
  const drilldownCertifiedHit = input.followUp?.kind === 'drilldown'
@@ -726,8 +936,16 @@ async function runAnswerLoop(input) {
726
936
  excludedArtifactIds,
727
937
  kg,
728
938
  }) : null;
729
- let artifactHit = drilldownCertifiedHit ?? unsafeCatalogCertifiedHit
730
- ?? (catalogCertifiedHit ? null : fallbackCertifiedHit);
939
+ const authoritativeCertifiedBinding = authoritativePlanBinding?.status === 'ready'
940
+ && authoritativePlanBinding.kind === 'certified'
941
+ ? authoritativePlanBinding
942
+ : undefined;
943
+ let artifactHit = authoritativeCertifiedBinding
944
+ ? { node: authoritativeCertifiedBinding.node, score: 1 }
945
+ : input.resolvedAnalyticalPlan?.mode === 'authoritative'
946
+ ? null
947
+ : drilldownCertifiedHit ?? unsafeCatalogCertifiedHit
948
+ ?? (catalogCertifiedHit ? null : fallbackCertifiedHit);
731
949
  let certifiedExecutionFallback;
732
950
  // Certified remains first when it actually covers the question. If the
733
951
  // retrieved block does not fit but a governed semantic metric does, never
@@ -875,8 +1093,10 @@ async function runAnswerLoop(input) {
875
1093
  const dqlArtifact = buildCertifiedBlockDqlArtifact(artifactHit.node, result, questionPlan.requestedShape.topN?.scope === 'per_group'
876
1094
  ? undefined
877
1095
  : questionPlan.requestedShape.topN?.n);
1096
+ const authoritativeCertifiedFailure = Boolean(authoritativeCertifiedBinding && executionError);
878
1097
  const recoverableCertifiedFailure = artifactHit.node.kind === 'block'
879
1098
  && executionError !== undefined
1099
+ && !authoritativeCertifiedFailure
880
1100
  && isRetryableCertifiedExecutionError(executionError);
881
1101
  if (recoverableCertifiedFailure) {
882
1102
  // A certified artifact is trusted evidence, not an obligation to return a
@@ -889,16 +1109,17 @@ async function runAnswerLoop(input) {
889
1109
  }
890
1110
  else {
891
1111
  return {
892
- kind: certifiedShapePassed ? 'certified' : 'uncertified',
893
- sourceTier,
1112
+ kind: authoritativeCertifiedFailure ? 'no_answer' : certifiedShapePassed ? 'certified' : 'uncertified',
1113
+ sourceTier: authoritativeCertifiedFailure ? 'no_answer' : sourceTier,
894
1114
  certification: certifiedShapePassed ? 'certified' : 'analyst_review_required',
895
- reviewStatus: certifiedShapePassed ? 'certified' : 'analyst_review_required',
1115
+ reviewStatus: authoritativeCertifiedFailure ? 'none' : certifiedShapePassed ? 'certified' : 'analyst_review_required',
896
1116
  confidence: certifiedShapePassed ? 0.95 : 0.45,
897
1117
  text,
898
1118
  answer: text,
899
1119
  block: artifactHit.node.kind === 'block' ? artifactHit.node : undefined,
900
1120
  result,
901
1121
  executionError,
1122
+ ...(authoritativeCertifiedFailure ? { refusalCode: 'grounding_gap' } : {}),
902
1123
  sql: result?.sql,
903
1124
  dqlArtifact,
904
1125
  trustLabel: certifiedShapePassed ? input.contextPack?.trustLabel ?? 'certified' : 'mixed',
@@ -1095,7 +1316,44 @@ async function runAnswerLoop(input) {
1095
1316
  let semanticBridgeAnswer;
1096
1317
  let semanticRuntimeFailure;
1097
1318
  let semanticRuntimeCompiledAnswer = false;
1098
- if (input.semanticLayer && semanticMetricMatch) {
1319
+ if (authoritativeSemanticBinding && input.semanticLayer) {
1320
+ const selection = authoritativeSemanticBinding.selection;
1321
+ semanticBridgeAnswer = composeSemanticQueryFromMembers({
1322
+ semanticLayer: input.semanticLayer,
1323
+ question,
1324
+ selection,
1325
+ ...(input.semanticDriver ? { driver: input.semanticDriver } : {}),
1326
+ ...(input.semanticTableMapping ? { tableMapping: input.semanticTableMapping } : {}),
1327
+ });
1328
+ if (!semanticBridgeAnswer && input.semanticQueryCompiler) {
1329
+ try {
1330
+ const compiled = await input.semanticQueryCompiler(selection);
1331
+ semanticBridgeAnswer = composeSemanticQueryFromCompiledMembers({
1332
+ semanticLayer: input.semanticLayer,
1333
+ question,
1334
+ selection: compiled.selection ?? selection,
1335
+ sql: compiled.sql,
1336
+ });
1337
+ semanticRuntimeCompiledAnswer = Boolean(semanticBridgeAnswer && compiled.engine !== 'native');
1338
+ }
1339
+ catch (error) {
1340
+ semanticRuntimeFailure = error instanceof Error ? error.message : String(error);
1341
+ }
1342
+ }
1343
+ if (semanticBridgeAnswer) {
1344
+ semanticBridgeToolCalls.push({
1345
+ name: 'compile_resolved_analytical_plan',
1346
+ status: 'checked',
1347
+ inputSummary: `plan: ${authoritativeSemanticBinding.planId}; metric: ${selection.metrics.join(', ')}`,
1348
+ outputSummary: 'Compiled the exact snapshot-bound member selection without rematching.',
1349
+ order: 1,
1350
+ });
1351
+ }
1352
+ else if (!semanticRuntimeFailure) {
1353
+ semanticRuntimeFailure = `The exact plan ${authoritativeSemanticBinding.planId} is not composable by the pinned semantic runtime.`;
1354
+ }
1355
+ }
1356
+ if (!authoritativeSemanticBinding && input.semanticLayer && semanticMetricMatch) {
1099
1357
  semanticBridgeAnswer = composeSemanticQueryForQuestion({
1100
1358
  semanticLayer: input.semanticLayer,
1101
1359
  question,
@@ -2146,7 +2404,7 @@ async function runAnswerLoop(input) {
2146
2404
  catch (err) {
2147
2405
  executionError = err instanceof Error ? err.message : String(err);
2148
2406
  }
2149
- if (executionError && !fanoutContradiction) {
2407
+ if (executionError && !fanoutContradiction && !authoritativeSemanticBinding) {
2150
2408
  if (isRetryableGeneratedSqlError(executionError)) {
2151
2409
  const localRepairSql = repairGeneratedSqlLocally(parsed.sql, executionError, schemaContext);
2152
2410
  if (localRepairSql && canUseLaneRepair(repairBudgetState, 'execution')) {
@@ -2368,19 +2626,21 @@ async function runAnswerLoop(input) {
2368
2626
  ? semanticMetricMatch?.metric.certification
2369
2627
  : undefined;
2370
2628
  const certifiedMetricAnswer = semanticMetricCertification === 'certified' || semanticMetricCertification === 'reviewed';
2629
+ const governedMetricExecutionFailure = governedMetricAnswer && Boolean(executionError);
2371
2630
  return {
2372
- kind: 'uncertified',
2373
- sourceTier: governedMetricAnswer ? 'semantic_layer' : activeTier,
2374
- certification: governedMetricAnswer ? 'governed' : 'ai_generated',
2375
- reviewStatus: governedMetricAnswer ? 'governed' : 'draft_ready',
2631
+ kind: governedMetricExecutionFailure ? 'no_answer' : 'uncertified',
2632
+ sourceTier: governedMetricExecutionFailure ? 'no_answer' : governedMetricAnswer ? 'semantic_layer' : activeTier,
2633
+ certification: governedMetricExecutionFailure ? 'analyst_review_required' : governedMetricAnswer ? 'governed' : 'ai_generated',
2634
+ reviewStatus: governedMetricExecutionFailure ? 'none' : governedMetricAnswer ? 'governed' : 'draft_ready',
2376
2635
  semanticMetricCertification,
2377
- confidence: certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
2636
+ confidence: governedMetricExecutionFailure ? 0 : certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
2378
2637
  text: generatedText,
2379
2638
  answer: generatedText,
2380
2639
  proposedSql: parsed.sql,
2381
2640
  sql: parsed.sql,
2382
2641
  result,
2383
2642
  executionError,
2643
+ ...(governedMetricExecutionFailure ? { refusalCode: 'grounding_gap' } : {}),
2384
2644
  suggestedViz: parsed.viz ?? 'table',
2385
2645
  dqlArtifact: answerDqlArtifact,
2386
2646
  draftBlock,