@duckcodeailabs/dql-agent 1.14.3-rc.1 → 1.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-run-engine.d.ts +177 -3
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +2061 -57
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +51 -3
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +544 -9
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +2 -0
- package/dist/agentic/analyst-loop.d.ts.map +1 -1
- package/dist/agentic/analyst-loop.js +41 -4
- package/dist/agentic/analyst-loop.js.map +1 -1
- package/dist/agentic/ledger-tools.d.ts.map +1 -1
- package/dist/agentic/ledger-tools.js +8 -1
- package/dist/agentic/ledger-tools.js.map +1 -1
- package/dist/agentic/orchestrator-policy.d.ts +3 -1
- package/dist/agentic/orchestrator-policy.d.ts.map +1 -1
- package/dist/agentic/orchestrator-policy.js +1 -1
- package/dist/agentic/orchestrator-policy.js.map +1 -1
- package/dist/agentic/tool-loop.d.ts +13 -1
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +441 -32
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/analytical-frame.d.ts +19 -0
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +170 -12
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +949 -10
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +499 -56
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +166 -2
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +654 -18
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +27 -0
- package/dist/answer-shape.d.ts.map +1 -1
- package/dist/answer-shape.js +55 -0
- package/dist/answer-shape.js.map +1 -1
- package/dist/ask-observability/instrumentation.d.ts.map +1 -1
- package/dist/ask-observability/instrumentation.js +52 -1
- package/dist/ask-observability/instrumentation.js.map +1 -1
- package/dist/ask-observability/portable.js +562 -0
- package/dist/ask-observability/portable.js.map +1 -1
- package/dist/ask-observability/types.d.ts +26 -3
- package/dist/ask-observability/types.d.ts.map +1 -1
- package/dist/ask-observability/types.js.map +1 -1
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts +850 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts.map +1 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js +1716 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts +95 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.js +8280 -0
- package/dist/ask-runtime/ask-analyst-runtime.js.map +1 -0
- package/dist/conversation/session-store.d.ts +31 -0
- package/dist/conversation/session-store.d.ts.map +1 -1
- package/dist/conversation/session-store.js +38 -0
- package/dist/conversation/session-store.js.map +1 -1
- package/dist/conversation/snapshot.d.ts +4 -1
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +85 -3
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/conversation/turn-trust.d.ts.map +1 -1
- package/dist/conversation/turn-trust.js +2 -0
- package/dist/conversation/turn-trust.js.map +1 -1
- package/dist/embeddings/provider.d.ts +1 -0
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js +18 -0
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/generated-analytical-proposal.js +37 -2
- package/dist/generated-analytical-proposal.js.map +1 -1
- package/dist/index.d.ts +15 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +77 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.js +32 -2
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +129 -0
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +108 -15
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.js +57 -6
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts +14 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +28 -1
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +19 -0
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +113 -6
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/provider-egress.d.ts +4 -1
- package/dist/provider-egress.d.ts.map +1 -1
- package/dist/provider-egress.js +50 -5
- package/dist/provider-egress.js.map +1 -1
- package/dist/providers/claude.d.ts +2 -2
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +191 -28
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +2 -2
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +191 -21
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +79 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/requirement-clauses.d.ts +23 -0
- package/dist/requirement-clauses.d.ts.map +1 -0
- package/dist/requirement-clauses.js +70 -0
- package/dist/requirement-clauses.js.map +1 -0
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +201 -15
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +109 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +753 -76
- package/dist/router.js.map +1 -1
- package/package.json +4 -4
package/dist/router.js
CHANGED
|
@@ -22,7 +22,7 @@ import { buildResolvedAnalyticalPlan, } from "./resolved-analytical-plan.js";
|
|
|
22
22
|
import { buildAnalyticalCascadeDecision, buildAnalyticalRequirementSeedV1, buildAnalyticalRequirementSet, categoricalDimensionRequirementTerms, candidateConflictsWithExplicitRankingMeasure, candidateMatchesCategoricalDimensionRequirement, evidenceCandidateRoles, isEntityAttributeCandidate, } from './analytical-orchestration.js';
|
|
23
23
|
import { askTraceObserverForV1 } from './ask-observability/index.js';
|
|
24
24
|
import { normalizeEvidenceAnalyticalCapability, solveAnalyticalCompatibility, } from "./analytical-compatibility.js";
|
|
25
|
-
import { buildDeterministicAnalyticalFrame, projectResolvedAnalyticalFrame, resolveMetricCapabilityDimension, } from "./analytical-frame.js";
|
|
25
|
+
import { buildDeterministicAnalyticalFrame, projectResolvedAnalyticalFrame, proveSameSnapshotMetricflowRoleExtensionV1, resolveMetricCapabilityDimension, } from "./analytical-frame.js";
|
|
26
26
|
import { conversationHistoryFromContext, renderConversationEnvelopeForPrompt, } from "./conversation/snapshot.js";
|
|
27
27
|
const DEFAULT_THRESHOLD = 0.7;
|
|
28
28
|
const DEFAULT_CACHE_SIZE = 200;
|
|
@@ -195,7 +195,7 @@ function parseClassification(raw) {
|
|
|
195
195
|
rationale,
|
|
196
196
|
};
|
|
197
197
|
}
|
|
198
|
-
function buildMeaningSystemPrompt() {
|
|
198
|
+
export function buildMeaningSystemPrompt() {
|
|
199
199
|
return [
|
|
200
200
|
"You resolve business meaning for DQL, a governed analytics system.",
|
|
201
201
|
"The host already performed broad retrieval and supplied a host-owned requirement seed. Compare ONLY the supplied candidate cards against that seed.",
|
|
@@ -210,7 +210,7 @@ function buildMeaningSystemPrompt() {
|
|
|
210
210
|
"`selectedConceptIds` is accepted only for legacy compatibility. All omitted optional fields receive host-owned safe defaults.",
|
|
211
211
|
].join("\n");
|
|
212
212
|
}
|
|
213
|
-
function buildMeaningUserPrompt(request, evidence, candidates, requirementSeed) {
|
|
213
|
+
export function buildMeaningUserPrompt(request, evidence, candidates, requirementSeed) {
|
|
214
214
|
const cards = candidates.map((candidate) => ({
|
|
215
215
|
id: candidate.id,
|
|
216
216
|
kind: candidate.kind,
|
|
@@ -222,6 +222,7 @@ function buildMeaningUserPrompt(request, evidence, candidates, requirementSeed)
|
|
|
222
222
|
aggregation: compactText(candidate.aggregation, 120),
|
|
223
223
|
domain: compactText(candidate.domain, 120),
|
|
224
224
|
semanticModel: compactText(candidate.semanticModel, 160),
|
|
225
|
+
dataType: compactText(candidate.dataType, 80),
|
|
225
226
|
primaryEntity: compactText(candidate.primaryEntity, 160),
|
|
226
227
|
dimensions: compactArray(candidate.dimensions, 16, 120),
|
|
227
228
|
timeGrains: compactArray(candidate.timeGrains, 8, 80),
|
|
@@ -469,6 +470,7 @@ function retrievalTrace(evidence, candidates) {
|
|
|
469
470
|
return {
|
|
470
471
|
...(evidence.snapshotId ? { snapshotId: evidence.snapshotId } : {}),
|
|
471
472
|
...(evidence.sourceFingerprint ? { sourceFingerprint: evidence.sourceFingerprint } : {}),
|
|
473
|
+
...(evidence.continuityFingerprint ? { continuityFingerprint: evidence.continuityFingerprint } : {}),
|
|
472
474
|
candidateCount: candidates.length,
|
|
473
475
|
candidateIds: candidates.map((candidate) => candidate.id),
|
|
474
476
|
...(candidateTraceMetadata.length ? { candidateTraceMetadata } : {}),
|
|
@@ -504,7 +506,12 @@ function traceSourceForCandidate(candidate) {
|
|
|
504
506
|
return 'runtime_schema';
|
|
505
507
|
return 'exploratory';
|
|
506
508
|
}
|
|
507
|
-
|
|
509
|
+
/**
|
|
510
|
+
* Persist the immutable snapshot's candidate lifecycle for either the legacy
|
|
511
|
+
* router or AskAnalystRuntimeV1. This is an observability projection only: it
|
|
512
|
+
* cannot retrieve, rank, admit, or alter a program.
|
|
513
|
+
*/
|
|
514
|
+
export function recordAskCandidateLifecycleV1(request, evidence, candidates, packageCandidates) {
|
|
508
515
|
const observer = askTraceObserverForV1(request);
|
|
509
516
|
if (!observer.enabled)
|
|
510
517
|
return;
|
|
@@ -522,9 +529,15 @@ function traceCandidateLifecycleBeforePruning(request, evidence, candidates, pac
|
|
|
522
529
|
...(requirements.dimensions.length ? ['categorical_dimension'] : []),
|
|
523
530
|
...(requirements.dimensions.length > 1 || requirements.entityTerms.length ? ['relationship'] : []),
|
|
524
531
|
]);
|
|
525
|
-
|
|
532
|
+
// Candidate lifecycle is an Advanced trace projection over the immutable
|
|
533
|
+
// prequalification/search backing pool. It intentionally has a larger cap
|
|
534
|
+
// than EvidenceWorkspaceV2 (32) so users can see a relevant candidate was
|
|
535
|
+
// pruned as `not_admitted` rather than falsely told it was absent. This
|
|
536
|
+
// projection is not an admission path: planner cards stay <=16 and the
|
|
537
|
+
// frozen execution closure stays <=32 plus an explicit bounded extension.
|
|
538
|
+
const rawCandidateTraceLimit = 80;
|
|
526
539
|
const hasRequestedEntityLabel = requirements.entityTerms.length > 0 || requirements.entityDisplayTerms.length > 0;
|
|
527
|
-
for (const [index, candidate] of candidates.slice(0,
|
|
540
|
+
for (const [index, candidate] of candidates.slice(0, rawCandidateTraceLimit).entries()) {
|
|
528
541
|
const roles = evidenceCandidateRoles(candidate);
|
|
529
542
|
const source = traceSourceForCandidate(candidate);
|
|
530
543
|
// This receipt must retain the actual retrieval memberships captured by
|
|
@@ -536,9 +549,13 @@ function traceCandidateLifecycleBeforePruning(request, evidence, candidates, pac
|
|
|
536
549
|
|| left.lane.localeCompare(right.lane));
|
|
537
550
|
const lane = lanes?.[0]?.lane;
|
|
538
551
|
const laneRank = lanes?.[0]?.rank;
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
552
|
+
const configuredRuntimeValueGroundingPin = candidate.matchReasons
|
|
553
|
+
.includes('host configured runtime value grounding pin');
|
|
554
|
+
const initialReason = configuredRuntimeValueGroundingPin
|
|
555
|
+
? 'configured_runtime_value_grounding_pin'
|
|
556
|
+
: candidate.exactMatch
|
|
557
|
+
? 'exact_name_match'
|
|
558
|
+
: 'unknown';
|
|
542
559
|
const compatibilityCode = candidate.compatibility === 'compatible' ? 'compatible'
|
|
543
560
|
: candidate.compatibility === 'incompatible' ? 'operation_unsupported'
|
|
544
561
|
: 'unknown';
|
|
@@ -582,15 +599,22 @@ function traceCandidateLifecycleBeforePruning(request, evidence, candidates, pac
|
|
|
582
599
|
}
|
|
583
600
|
if (packageIds.has(candidate.id)) {
|
|
584
601
|
const reservedForRole = requestedRoles.has(role);
|
|
602
|
+
const unresolvedRoleAdmission = candidate.matchReasons.some((reason) => reason.startsWith(`candidate_for_unresolved_role:${role}:`));
|
|
585
603
|
observer.recordCandidateDecision({
|
|
586
604
|
...common,
|
|
587
605
|
decision: 'reserved',
|
|
588
|
-
reasonCode:
|
|
606
|
+
reasonCode: configuredRuntimeValueGroundingPin ? 'configured_runtime_value_grounding_pin'
|
|
607
|
+
: candidate.exactMatch ? 'exact_name_match'
|
|
608
|
+
: unresolvedRoleAdmission ? 'candidate_for_unresolved_role'
|
|
609
|
+
: reservedForRole ? 'role_reserved' : 'fused_relevance_fill',
|
|
589
610
|
});
|
|
590
611
|
observer.recordCandidateDecision({
|
|
591
612
|
...common,
|
|
592
613
|
decision: 'admitted',
|
|
593
|
-
reasonCode:
|
|
614
|
+
reasonCode: configuredRuntimeValueGroundingPin ? 'configured_runtime_value_grounding_pin'
|
|
615
|
+
: candidate.exactMatch ? 'exact_name_match'
|
|
616
|
+
: unresolvedRoleAdmission ? 'candidate_for_unresolved_role'
|
|
617
|
+
: reservedForRole ? 'role_reserved' : 'fused_relevance_fill',
|
|
594
618
|
});
|
|
595
619
|
}
|
|
596
620
|
else {
|
|
@@ -845,6 +869,34 @@ function relationshipSafetyAllowsExploratoryJoin(safety, requiredJoinKey) {
|
|
|
845
869
|
const lifecycleAllowsExploration = safety.status === 'certified'
|
|
846
870
|
|| safety.status === 'validated'
|
|
847
871
|
|| safety.status === 'draft';
|
|
872
|
+
// A local runtime may mint this *after* it has evaluated the immutable DQL
|
|
873
|
+
// relationship graph. It is not a weaker spelling of automaticJoinAllowed:
|
|
874
|
+
// it can only admit a declared same-domain draft/review edge to the
|
|
875
|
+
// review-required exploratory compiler. Keep generic retrieved evidence on
|
|
876
|
+
// the stricter validated/automatic route below.
|
|
877
|
+
const hostAttestedExploratory = safety.exploratoryJoinAllowed === true
|
|
878
|
+
&& Boolean(safety.exploratoryPathFingerprint?.trim());
|
|
879
|
+
const lifecycleAllowsHostAttestedExploration = safety.status === 'draft'
|
|
880
|
+
|| safety.status === 'evaluated'
|
|
881
|
+
|| safety.status === 'review'
|
|
882
|
+
|| safety.status === 'reviewed';
|
|
883
|
+
const keysAreUsable = safety.keys.length > 0
|
|
884
|
+
&& safety.keys.every((key) => Boolean(key.from.trim() && key.to.trim()));
|
|
885
|
+
if (hostAttestedExploratory) {
|
|
886
|
+
if (!lifecycleAllowsHostAttestedExploration
|
|
887
|
+
|| safety.staleCertification === true
|
|
888
|
+
|| safety.fanout !== 'safe'
|
|
889
|
+
|| !['one_to_one', 'one_to_many', 'many_to_one'].includes(safety.cardinality ?? '')
|
|
890
|
+
|| !safety.from?.trim()
|
|
891
|
+
|| !safety.to?.trim()
|
|
892
|
+
|| !keysAreUsable)
|
|
893
|
+
return false;
|
|
894
|
+
if (!requiredJoinKey)
|
|
895
|
+
return true;
|
|
896
|
+
const normalizedKey = normalizeMetricPhrase(requiredJoinKey);
|
|
897
|
+
return safety.keys.some((key) => metricTermsMatch(normalizeMetricPhrase(key.from), normalizedKey)
|
|
898
|
+
|| metricTermsMatch(normalizeMetricPhrase(key.to), normalizedKey));
|
|
899
|
+
}
|
|
848
900
|
if (!lifecycleAllowsExploration
|
|
849
901
|
|| safety.staleCertification === true
|
|
850
902
|
|| safety.automaticJoinAllowed !== true
|
|
@@ -858,8 +910,7 @@ function relationshipSafetyAllowsExploratoryJoin(safety, requiredJoinKey) {
|
|
|
858
910
|
|| !validation.proofFingerprint?.trim()
|
|
859
911
|
|| !Number.isFinite(checkedAt)
|
|
860
912
|
|| expirationInvalid
|
|
861
|
-
||
|
|
862
|
-
|| safety.keys.some((key) => !key.from.trim() || !key.to.trim()))
|
|
913
|
+
|| !keysAreUsable)
|
|
863
914
|
return false;
|
|
864
915
|
if (!requiredJoinKey)
|
|
865
916
|
return true;
|
|
@@ -867,6 +918,77 @@ function relationshipSafetyAllowsExploratoryJoin(safety, requiredJoinKey) {
|
|
|
867
918
|
return safety.keys.some((key) => metricTermsMatch(normalizeMetricPhrase(key.from), normalizedKey)
|
|
868
919
|
|| metricTermsMatch(normalizeMetricPhrase(key.to), normalizedKey));
|
|
869
920
|
}
|
|
921
|
+
/**
|
|
922
|
+
* Resolve a card's complete same-snapshot relationship proof set.
|
|
923
|
+
*
|
|
924
|
+
* Every evidence edge must map to exactly one structured proof (an alias may
|
|
925
|
+
* perform that mapping); every supplied proof must be used by an evidence
|
|
926
|
+
* edge. This rejects partial cards, unrelated safety records, and aliases
|
|
927
|
+
* that ambiguously name two proofs before a host-authored path card can carry
|
|
928
|
+
* them into planner context. The router/compiler still revalidates the
|
|
929
|
+
* selected raw edges at freeze time.
|
|
930
|
+
*/
|
|
931
|
+
export function relationshipSafetyProofSelectionForCandidateV1(candidate) {
|
|
932
|
+
const evidenceByIdentity = new Map();
|
|
933
|
+
for (const rawEvidenceId of candidate.relationshipEvidence ?? []) {
|
|
934
|
+
const canonicalInput = rawEvidenceId.trim();
|
|
935
|
+
const identity = normalizedRelationshipIdentity(canonicalInput);
|
|
936
|
+
if (!identity)
|
|
937
|
+
return undefined;
|
|
938
|
+
// Duplicate spellings of the same input identity do not create another
|
|
939
|
+
// edge. They are canonicalized below to the matched safety ID.
|
|
940
|
+
if (!evidenceByIdentity.has(identity))
|
|
941
|
+
evidenceByIdentity.set(identity, canonicalInput);
|
|
942
|
+
}
|
|
943
|
+
if (evidenceByIdentity.size === 0)
|
|
944
|
+
return undefined;
|
|
945
|
+
const proofs = candidate.relationshipSafety ?? [];
|
|
946
|
+
if (proofs.length === 0)
|
|
947
|
+
return undefined;
|
|
948
|
+
const matchedProofIndexes = new Set();
|
|
949
|
+
const matchedProofs = [];
|
|
950
|
+
for (const evidenceIdentity of evidenceByIdentity.keys()) {
|
|
951
|
+
const matches = proofs
|
|
952
|
+
.map((safety, index) => ({ safety, index }))
|
|
953
|
+
.filter(({ safety }) => {
|
|
954
|
+
// A canonical record without an ID cannot be safely serialized even
|
|
955
|
+
// if a loose alias happens to match the retrieval edge.
|
|
956
|
+
if (!safety.id?.trim())
|
|
957
|
+
return false;
|
|
958
|
+
return relationshipSafetyIdentities(safety).includes(evidenceIdentity);
|
|
959
|
+
});
|
|
960
|
+
// One evidence edge must identify one and only one canonical proof. A
|
|
961
|
+
// duplicate ID, duplicate alias, or conflicting alias is unsafe rather
|
|
962
|
+
// than a tie the path-card builder may resolve heuristically.
|
|
963
|
+
if (matches.length !== 1)
|
|
964
|
+
return undefined;
|
|
965
|
+
const match = matches[0];
|
|
966
|
+
matchedProofIndexes.add(match.index);
|
|
967
|
+
if (!matchedProofs.some((proof) => proof === match.safety))
|
|
968
|
+
matchedProofs.push(match.safety);
|
|
969
|
+
}
|
|
970
|
+
// A card that carries an unrelated proof is not a compact proof set. Do
|
|
971
|
+
// not silently discard it: the raw card may represent a stale/mixed
|
|
972
|
+
// snapshot and cannot become a trusted atomic planner path.
|
|
973
|
+
if (matchedProofIndexes.size !== proofs.length)
|
|
974
|
+
return undefined;
|
|
975
|
+
const proofClass = matchedProofs.every((safety) => relationshipSafetyAllowsAutomaticJoin(safety))
|
|
976
|
+
? 'governed'
|
|
977
|
+
: matchedProofs.every((safety) => relationshipSafetyAllowsExploratoryJoin(safety))
|
|
978
|
+
? 'exploratory'
|
|
979
|
+
: undefined;
|
|
980
|
+
if (!proofClass)
|
|
981
|
+
return undefined;
|
|
982
|
+
return {
|
|
983
|
+
proofClass,
|
|
984
|
+
relationshipEvidence: [...new Set(matchedProofs.map((safety) => safety.id.trim()))].sort(),
|
|
985
|
+
relationshipSafety: [...matchedProofs]
|
|
986
|
+
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
export function relationshipSafetyProofClassForCandidateV1(candidate) {
|
|
990
|
+
return relationshipSafetyProofSelectionForCandidateV1(candidate)?.proofClass;
|
|
991
|
+
}
|
|
870
992
|
function relationshipSafetyAllowsJoin(safety, authority, requiredJoinKey) {
|
|
871
993
|
return authority === 'governed'
|
|
872
994
|
? relationshipSafetyAllowsAutomaticJoin(safety, requiredJoinKey)
|
|
@@ -913,13 +1035,30 @@ function relationshipEndpointMatchesEntityTerms(endpoint, entityTerms) {
|
|
|
913
1035
|
const normalizedEndpoint = normalizeMetricPhrase(endpoint);
|
|
914
1036
|
return entityTerms.some((term) => metricTermsMatch(normalizedEndpoint, normalizeMetricPhrase(term)));
|
|
915
1037
|
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Modeling files commonly use local entity IDs for relationship endpoints while
|
|
1040
|
+
* candidate cards retain the domain-qualified entity identity. Relationship
|
|
1041
|
+
* safety is only meaningful inside the declared relationship's domain, so
|
|
1042
|
+
* bridge that representation here rather than falling back to an unscoped
|
|
1043
|
+
* leaf-name match. This helper is used exclusively by the attribution-policy
|
|
1044
|
+
* graph below; it never authorizes a join.
|
|
1045
|
+
*/
|
|
1046
|
+
function scopedRelationshipEndpointIdentity(endpoint, relationship) {
|
|
1047
|
+
const normalizedEndpoint = normalizedRelationshipIdentity(endpoint ?? '');
|
|
1048
|
+
if (!normalizedEndpoint || normalizedEndpoint.includes('::entity::'))
|
|
1049
|
+
return normalizedEndpoint;
|
|
1050
|
+
const relationshipIdentity = relationshipSafetyIdentities(relationship)
|
|
1051
|
+
.find((identity) => identity.includes('::relationship::'));
|
|
1052
|
+
const domainPrefix = relationshipIdentity?.split('::relationship::')[0];
|
|
1053
|
+
return domainPrefix ? `${domainPrefix}::entity::${normalizedEndpoint}` : normalizedEndpoint;
|
|
1054
|
+
}
|
|
916
1055
|
function relationshipGraphReaches(starts, targets, relationships) {
|
|
917
1056
|
if (starts.size === 0 || targets.size === 0)
|
|
918
1057
|
return false;
|
|
919
1058
|
const graph = new Map();
|
|
920
1059
|
for (const relationship of relationships) {
|
|
921
|
-
const from =
|
|
922
|
-
const to =
|
|
1060
|
+
const from = scopedRelationshipEndpointIdentity(relationship.from, relationship);
|
|
1061
|
+
const to = scopedRelationshipEndpointIdentity(relationship.to, relationship);
|
|
923
1062
|
if (!from || !to)
|
|
924
1063
|
continue;
|
|
925
1064
|
const fromNeighbors = graph.get(from) ?? new Set();
|
|
@@ -944,7 +1083,14 @@ function relationshipGraphReaches(starts, targets, relationships) {
|
|
|
944
1083
|
}
|
|
945
1084
|
return false;
|
|
946
1085
|
}
|
|
947
|
-
|
|
1086
|
+
/**
|
|
1087
|
+
* Evaluate an attribution/allocation policy boundary from one immutable
|
|
1088
|
+
* snapshot. AskAnalystRuntimeV1 invokes this before an optional provider
|
|
1089
|
+
* meaning call: a declared denial is neither an ambiguity nor a provider
|
|
1090
|
+
* failure. This helper only evaluates supplied evidence; it never retrieves
|
|
1091
|
+
* additional context or executes a query.
|
|
1092
|
+
*/
|
|
1093
|
+
export function attributionRequiredRelationshipGapDecision(input) {
|
|
948
1094
|
if (!EXPLICIT_ATTRIBUTION_REQUEST_RE.test(input.request.question)
|
|
949
1095
|
|| input.requirements.entityTerms.length === 0)
|
|
950
1096
|
return undefined;
|
|
@@ -969,8 +1115,8 @@ function attributionRequiredRelationshipGapDecision(input) {
|
|
|
969
1115
|
return undefined;
|
|
970
1116
|
const attributed = attributionRequired.find((relationship) => {
|
|
971
1117
|
const endpoints = [
|
|
972
|
-
|
|
973
|
-
|
|
1118
|
+
scopedRelationshipEndpointIdentity(relationship.from, relationship),
|
|
1119
|
+
scopedRelationshipEndpointIdentity(relationship.to, relationship),
|
|
974
1120
|
].filter(Boolean);
|
|
975
1121
|
const requestedSignalEndpoints = new Set(endpoints.filter((endpoint) => relationshipEndpointMatchesQuestionConcept(endpoint, input.request.question)));
|
|
976
1122
|
if (requestedSignalEndpoints.size === 0)
|
|
@@ -982,7 +1128,17 @@ function attributionRequiredRelationshipGapDecision(input) {
|
|
|
982
1128
|
const allReachable = relationshipGraphReaches(requestedSignalEndpoints, targetEndpoints, relationships);
|
|
983
1129
|
const safeRelationships = relationships.filter((candidate) => relationshipSafetyAllowsAutomaticJoin(candidate));
|
|
984
1130
|
const safelyReachable = relationshipGraphReaches(requestedSignalEndpoints, targetEndpoints, safeRelationships);
|
|
985
|
-
|
|
1131
|
+
if (safelyReachable)
|
|
1132
|
+
return false;
|
|
1133
|
+
if (allReachable)
|
|
1134
|
+
return true;
|
|
1135
|
+
// The compact route workspace may omit an otherwise-safe continuation
|
|
1136
|
+
// edge, but it must not erase a declared attribution boundary for the
|
|
1137
|
+
// exact signal the user named. We have already established a direct,
|
|
1138
|
+
// snapshot-bound match for that signal and a requested entity. Returning
|
|
1139
|
+
// a typed modeling gap is safer than presenting unrelated metric choices
|
|
1140
|
+
// or attempting an unapproved attribution join.
|
|
1141
|
+
return true;
|
|
986
1142
|
});
|
|
987
1143
|
if (!attributed)
|
|
988
1144
|
return undefined;
|
|
@@ -1362,7 +1518,7 @@ function hasSafeExploratoryPhysicalPath(requirements, candidates, missingDimensi
|
|
|
1362
1518
|
function preFreezePhysicalCascadeDecision(input) {
|
|
1363
1519
|
// `candidates` can be the capped meaning package. Physical eligibility is
|
|
1364
1520
|
// allowed one same-snapshot extension, never a new retrieval/domain scope.
|
|
1365
|
-
const snapshotCandidates =
|
|
1521
|
+
const snapshotCandidates = targetScopedSameSnapshotCandidates(input.evidence, input.candidates, input.targetScope, input.executionCandidateIds);
|
|
1366
1522
|
const physicalPath = hasSafeExploratoryPhysicalPath(input.requirements, snapshotCandidates, input.missingTerms, input.requiredPhysicalFieldTerms ?? []);
|
|
1367
1523
|
const missingRankingMetric = Boolean(physicalPath.ok
|
|
1368
1524
|
&& input.requireRankingMetric
|
|
@@ -1429,14 +1585,17 @@ function preFreezePhysicalCascadeDecision(input) {
|
|
|
1429
1585
|
const coverageFor = (source) => coverage.find((item) => item.source === source);
|
|
1430
1586
|
const skippedAttempt = (tier, source) => {
|
|
1431
1587
|
const item = coverageFor(source);
|
|
1588
|
+
const semanticCompilerUnavailable = tier === 'semantic' && input.semanticTierUnavailable;
|
|
1432
1589
|
return {
|
|
1433
1590
|
version: 1,
|
|
1434
1591
|
tier,
|
|
1435
|
-
outcome: item?.status
|
|
1592
|
+
outcome: semanticCompilerUnavailable || item?.status !== 'available' ? 'unavailable' : 'ineligible',
|
|
1436
1593
|
candidateIds: item?.candidateIds ?? [],
|
|
1437
|
-
reason:
|
|
1438
|
-
?
|
|
1439
|
-
:
|
|
1594
|
+
reason: semanticCompilerUnavailable
|
|
1595
|
+
? 'The selected semantic compiler was unavailable before plan freeze.'
|
|
1596
|
+
: item?.status === 'available'
|
|
1597
|
+
? `The ${tier} tier did not prove the complete requested tuple before plan freeze.`
|
|
1598
|
+
: `The ${tier} source was ${item?.status ?? 'unavailable'} in this snapshot.`,
|
|
1440
1599
|
planFrozen: false,
|
|
1441
1600
|
};
|
|
1442
1601
|
};
|
|
@@ -1535,7 +1694,7 @@ function preFreezePhysicalCascadeDecision(input) {
|
|
|
1535
1694
|
* structured-continuation checks, while retaining a caller's server-issued
|
|
1536
1695
|
* selection when it was supplied as a same-snapshot clarification card.
|
|
1537
1696
|
*/
|
|
1538
|
-
function immutableSnapshotCandidates(evidence, candidates) {
|
|
1697
|
+
function immutableSnapshotCandidates(evidence, candidates, executionCandidateIds) {
|
|
1539
1698
|
const byId = new Map();
|
|
1540
1699
|
for (const candidate of [
|
|
1541
1700
|
...evidence.candidates,
|
|
@@ -1545,7 +1704,41 @@ function immutableSnapshotCandidates(evidence, candidates) {
|
|
|
1545
1704
|
if (!byId.has(candidate.id))
|
|
1546
1705
|
byId.set(candidate.id, candidate);
|
|
1547
1706
|
}
|
|
1548
|
-
|
|
1707
|
+
const snapshot = [...byId.values()];
|
|
1708
|
+
if (!executionCandidateIds)
|
|
1709
|
+
return snapshot;
|
|
1710
|
+
const allowed = new Set(executionCandidateIds);
|
|
1711
|
+
return snapshot.filter((candidate) => allowed.has(candidate.id) || allowed.has(candidate.qualifiedId ?? candidate.id));
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* The one permitted physical extension stays inside the already acquired
|
|
1715
|
+
* snapshot and, when the active target is represented in source provenance,
|
|
1716
|
+
* restricts physical cards to that target. This prevents a semantic target
|
|
1717
|
+
* mismatch from being misreported as global schema absence while retaining
|
|
1718
|
+
* relationship cards needed to prove a safe closure.
|
|
1719
|
+
*/
|
|
1720
|
+
function targetScopedSameSnapshotCandidates(evidence, candidates, targetScope, executionCandidateIds) {
|
|
1721
|
+
const snapshot = immutableSnapshotCandidates(evidence, candidates, executionCandidateIds);
|
|
1722
|
+
if (!targetScope)
|
|
1723
|
+
return snapshot;
|
|
1724
|
+
const normalized = targetScope.toLowerCase();
|
|
1725
|
+
const physical = snapshot.filter((candidate) => candidate.kind === 'sql_table'
|
|
1726
|
+
|| candidate.kind === 'sql_column' || candidate.kind === 'dbt_model' || candidate.kind === 'dbt_source');
|
|
1727
|
+
const scopedPhysical = physical.filter((candidate) => [
|
|
1728
|
+
...(candidate.sourceObjects ?? []),
|
|
1729
|
+
candidate.provenance ?? '',
|
|
1730
|
+
candidate.semanticModel ?? '',
|
|
1731
|
+
candidate.domain ?? '',
|
|
1732
|
+
].some((value) => value.toLowerCase().includes(normalized)));
|
|
1733
|
+
// Provenance often does not carry a connection name in older indexes. In
|
|
1734
|
+
// that case retain the immutable snapshot rather than inventing absence.
|
|
1735
|
+
if (scopedPhysical.length === 0)
|
|
1736
|
+
return snapshot;
|
|
1737
|
+
const scopedIds = new Set(scopedPhysical.map((candidate) => candidate.id));
|
|
1738
|
+
return snapshot.filter((candidate) => !physical.includes(candidate)
|
|
1739
|
+
|| scopedIds.has(candidate.id)
|
|
1740
|
+
|| candidate.kind === 'dql_modeling'
|
|
1741
|
+
|| (candidate.relationshipEvidence?.length ?? 0) > 0);
|
|
1549
1742
|
}
|
|
1550
1743
|
/**
|
|
1551
1744
|
* A normal meaning call may nominate either a semantic metric or a governed
|
|
@@ -1590,7 +1783,7 @@ function continuePreFreezeModelingGapThroughPhysicalSnapshot(input) {
|
|
|
1590
1783
|
const physicalContinuation = preFreezePhysicalCascadeDecision({
|
|
1591
1784
|
base: input.base,
|
|
1592
1785
|
evidence,
|
|
1593
|
-
candidates: immutableSnapshotCandidates(evidence, input.candidates),
|
|
1786
|
+
candidates: immutableSnapshotCandidates(evidence, input.candidates, input.executionCandidateIds),
|
|
1594
1787
|
question: input.question,
|
|
1595
1788
|
requirements,
|
|
1596
1789
|
missingTerms,
|
|
@@ -1599,6 +1792,7 @@ function continuePreFreezeModelingGapThroughPhysicalSnapshot(input) {
|
|
|
1599
1792
|
? 'The selected governed relational interpretation did not prove a compiler-owned DQL projection before plan freeze.'
|
|
1600
1793
|
: 'The selected semantic interpretation was pre-freeze-ineligible for the complete requested tuple.',
|
|
1601
1794
|
terminalCandidateIds: plan.selectedConceptIds,
|
|
1795
|
+
...(input.executionCandidateIds ? { executionCandidateIds: input.executionCandidateIds } : {}),
|
|
1602
1796
|
});
|
|
1603
1797
|
if (physicalContinuation.action === 'answer'
|
|
1604
1798
|
&& physicalContinuation.analyticalCascadeDecision?.selectedTier === 'exploratory_sql') {
|
|
@@ -1989,6 +2183,264 @@ function routeDecisionForResolution(base, evidence, candidates, resolution, sour
|
|
|
1989
2183
|
: {}),
|
|
1990
2184
|
};
|
|
1991
2185
|
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Compiler-only bridge for AskAnalystRuntimeV1. The runtime owns question
|
|
2188
|
+
* framing, candidate admission, meaning selection, and the immutable program;
|
|
2189
|
+
* this adapter only validates that selected program against existing safe
|
|
2190
|
+
* certified/MetricFlow/relational/exploratory compilers. It must never call a
|
|
2191
|
+
* provider, retrieve a new snapshot, or nominate a different business meaning.
|
|
2192
|
+
*/
|
|
2193
|
+
export function compileAskAnalyticalProgramV1(input) {
|
|
2194
|
+
const readiness = input.request.askAnalystTierReadiness;
|
|
2195
|
+
// A compiler must never widen the program that the runtime froze. A stale
|
|
2196
|
+
// adapter-selected identity is a compilation failure, not an invitation to
|
|
2197
|
+
// rank/retrieve/interpret the question again.
|
|
2198
|
+
const executionCandidates = input.executionCandidates ?? input.candidates;
|
|
2199
|
+
const executionIds = new Set(input.program.executionCandidateIds ?? input.program.candidateIds);
|
|
2200
|
+
const executionCandidateIds = [...executionIds];
|
|
2201
|
+
const isProgramExecutionCandidate = (candidate) => executionIds.has(candidate.id) || executionIds.has(candidate.qualifiedId ?? candidate.id);
|
|
2202
|
+
if (executionCandidates.some((candidate) => !isProgramExecutionCandidate(candidate))) {
|
|
2203
|
+
return immutableProgramBlocked(input.base, 'A compiler received a candidate outside the frozen Ask execution closure.');
|
|
2204
|
+
}
|
|
2205
|
+
const seedFilters = input.request.hostRequirementSeed?.queryIntent.filters ?? [];
|
|
2206
|
+
if (!validProgramFilterBindings(input.program, seedFilters)) {
|
|
2207
|
+
return immutableProgramBlocked(input.base, 'The immutable Ask program did not retain valid field, operator, and value bindings for every requested filter.');
|
|
2208
|
+
}
|
|
2209
|
+
const unboundCurrentQuestionLiterals = unboundProgramV3CurrentQuestionLiterals(input.program);
|
|
2210
|
+
if (unboundCurrentQuestionLiterals.length > 0) {
|
|
2211
|
+
// This is a normal pre-freeze analytical coverage gap, not an immutable
|
|
2212
|
+
// program corruption or policy breach. The current question named a
|
|
2213
|
+
// value, but no single selected qualified field/value binding survived
|
|
2214
|
+
// verification. Reporting it as a typed gap prevents an unknown or
|
|
2215
|
+
// ambiguous literal from broadening the SQL while preserving the ordinary
|
|
2216
|
+
// cascade vocabulary for repair and trace rendering.
|
|
2217
|
+
return currentQuestionLiteralCoverageGapDecision({
|
|
2218
|
+
base: input.base,
|
|
2219
|
+
evidence: input.evidence,
|
|
2220
|
+
executionCandidates,
|
|
2221
|
+
requirements: input.requirements,
|
|
2222
|
+
program: input.program,
|
|
2223
|
+
literals: unboundCurrentQuestionLiterals,
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
const programIds = new Set(input.program.candidateIds);
|
|
2227
|
+
// The compiler receives the full frozen 32-card execution closure for
|
|
2228
|
+
// relationship/physical compilation, but only the program's verified
|
|
2229
|
+
// selected cards may authorize meaning. Do not treat closure membership as
|
|
2230
|
+
// selection authority: that would let a tail candidate change the question
|
|
2231
|
+
// after planner verification.
|
|
2232
|
+
const programIdentityIds = new Set(input.candidates
|
|
2233
|
+
.filter((candidate) => programIds.has(candidate.id) || programIds.has(candidate.qualifiedId ?? candidate.id))
|
|
2234
|
+
.flatMap((candidate) => [candidate.id, candidate.qualifiedId].filter((id) => Boolean(id))));
|
|
2235
|
+
const selectedIds = [
|
|
2236
|
+
...input.resolution.selectedConceptIds,
|
|
2237
|
+
...(input.resolution.recommendedExecutionId ? [input.resolution.recommendedExecutionId] : []),
|
|
2238
|
+
];
|
|
2239
|
+
if (selectedIds.some((id) => !programIds.has(id) && !programIdentityIds.has(id))) {
|
|
2240
|
+
return {
|
|
2241
|
+
...input.base,
|
|
2242
|
+
action: 'block',
|
|
2243
|
+
confidence: 1,
|
|
2244
|
+
source: 'heuristic',
|
|
2245
|
+
followsUp: false,
|
|
2246
|
+
reason: 'The immutable Ask program did not authorize every selected compiler identity.',
|
|
2247
|
+
terminalOutcome: {
|
|
2248
|
+
kind: 'policy_blocked',
|
|
2249
|
+
code: 'ANALYTICAL_POLICY_BLOCKED',
|
|
2250
|
+
message: 'A compiler attempted to consume a candidate outside the frozen Ask program.',
|
|
2251
|
+
candidateIds: selectedIds,
|
|
2252
|
+
},
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
// `candidates` is the runtime-verified business tuple. The larger frozen
|
|
2256
|
+
// execution closure is deliberately supplied only to the physical
|
|
2257
|
+
// continuation below. Passing it into the semantic router lets a related
|
|
2258
|
+
// metric or numeric attribute re-open meaning selection after program
|
|
2259
|
+
// verification (for example Orders + Drink Orders + customer order number
|
|
2260
|
+
// for the singular request "order count for each customer").
|
|
2261
|
+
const selectedMeaningCandidates = input.candidates.filter((candidate) => programIds.has(candidate.id) || programIds.has(candidate.qualifiedId ?? candidate.id));
|
|
2262
|
+
const decision = routeDecisionForResolution(input.base, input.evidence, selectedMeaningCandidates, input.resolution, 'heuristic', input.request.question, input.mode ?? 'authoritative');
|
|
2263
|
+
// A material business choice is resolved before target readiness. A target
|
|
2264
|
+
// being unavailable does not make two independently valid semantic display
|
|
2265
|
+
// meanings become one physical meaning; collapsing it first caused offline
|
|
2266
|
+
// structured clarification/restart flows to report a false coverage gap.
|
|
2267
|
+
// Once an immutable meaning is selected, readiness still advances an
|
|
2268
|
+
// unavailable semantic target through the same-snapshot physical cascade.
|
|
2269
|
+
if (decision.requiresClarification === true || decision.action === 'clarify') {
|
|
2270
|
+
return decision;
|
|
2271
|
+
}
|
|
2272
|
+
// Connector/active-target readiness is recorded separately because it is an
|
|
2273
|
+
// execution boundary, not necessarily a semantic *compiler* boundary. A
|
|
2274
|
+
// native semantic plan can be proven and frozen from an authored local
|
|
2275
|
+
// snapshot without a configured connection; execution then reports the
|
|
2276
|
+
// normal post-freeze setup failure. Adapter/target binding for MetricFlow or
|
|
2277
|
+
// another external semantic compiler is folded into semanticCompiler by the
|
|
2278
|
+
// host readiness probe, so only that state authorizes pre-freeze fallback.
|
|
2279
|
+
const semanticUnavailable = readiness?.semanticCompiler === 'unavailable';
|
|
2280
|
+
// A semantic adapter/target mismatch is a pre-freeze availability result,
|
|
2281
|
+
// not a modeling absence. A safe physical closure from this exact snapshot
|
|
2282
|
+
// may still answer as review-required exploratory SQL.
|
|
2283
|
+
if (semanticUnavailable && input.resolution.recommendedRoute === 'semantic') {
|
|
2284
|
+
return preFreezePhysicalCascadeDecision({
|
|
2285
|
+
base: input.base,
|
|
2286
|
+
evidence: input.evidence,
|
|
2287
|
+
candidates: executionCandidates,
|
|
2288
|
+
question: input.request.question,
|
|
2289
|
+
requirements: input.requirements,
|
|
2290
|
+
missingTerms: [
|
|
2291
|
+
...input.requirements.measures,
|
|
2292
|
+
...input.requirements.dimensions,
|
|
2293
|
+
...input.requirements.entityTerms,
|
|
2294
|
+
...input.requirements.entityDisplayTerms,
|
|
2295
|
+
],
|
|
2296
|
+
requiredPhysicalFieldTerms: [
|
|
2297
|
+
...input.requirements.measures,
|
|
2298
|
+
...input.requirements.dimensions,
|
|
2299
|
+
...input.requirements.entityTerms,
|
|
2300
|
+
...input.requirements.entityDisplayTerms,
|
|
2301
|
+
],
|
|
2302
|
+
messagePrefix: 'The semantic compiler or active target was unavailable before plan freeze; DQL evaluated the same-snapshot safe physical path independently.',
|
|
2303
|
+
terminalCandidateIds: input.resolution.selectedConceptIds,
|
|
2304
|
+
...(input.request.executionTarget?.target === 'connection' && input.request.executionTarget.connectionName
|
|
2305
|
+
? { targetScope: input.request.executionTarget.connectionName }
|
|
2306
|
+
: {}),
|
|
2307
|
+
executionCandidateIds,
|
|
2308
|
+
semanticTierUnavailable: true,
|
|
2309
|
+
requireRankingMetric: Boolean(input.requirements.ranking && input.requirements.ranking.metricTerms.length === 0),
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
// A selected semantic/governed plan may be invalid before it freezes. That
|
|
2313
|
+
// is explicitly allowed to advance, but a frozen, denied, or unsafe plan is
|
|
2314
|
+
// never silently downgraded.
|
|
2315
|
+
return continuePreFreezeModelingGapThroughPhysicalSnapshot({
|
|
2316
|
+
decision,
|
|
2317
|
+
base: input.base,
|
|
2318
|
+
evidence: input.evidence,
|
|
2319
|
+
candidates: executionCandidates,
|
|
2320
|
+
question: input.request.question,
|
|
2321
|
+
executionCandidateIds,
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
function immutableProgramBlocked(base, message) {
|
|
2325
|
+
return {
|
|
2326
|
+
...base,
|
|
2327
|
+
action: 'block',
|
|
2328
|
+
confidence: 1,
|
|
2329
|
+
source: 'heuristic',
|
|
2330
|
+
followsUp: false,
|
|
2331
|
+
reason: message,
|
|
2332
|
+
terminalOutcome: {
|
|
2333
|
+
kind: 'policy_blocked',
|
|
2334
|
+
code: 'ANALYTICAL_POLICY_BLOCKED',
|
|
2335
|
+
message,
|
|
2336
|
+
candidateIds: [],
|
|
2337
|
+
},
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
function validProgramFilterBindings(program, seedFilters) {
|
|
2341
|
+
if (program.filters.some((filter) => filter.fieldTerms.length === 0 || !filter.fieldTerms.every(Boolean) || !filter.value.trim() || filter.operator === 'unknown'))
|
|
2342
|
+
return false;
|
|
2343
|
+
// Direct compiler consumers from before AskAnalystRuntimeV1 do not carry a
|
|
2344
|
+
// host requirement seed. They still must provide internally valid filters,
|
|
2345
|
+
// but cannot be compared to a seed that was never supplied. Authoritative
|
|
2346
|
+
// Ask always supplies one and therefore takes the exact-binding branch.
|
|
2347
|
+
if (seedFilters.length === 0)
|
|
2348
|
+
return true;
|
|
2349
|
+
// A program with no predicate is valid only when the host seed also has no
|
|
2350
|
+
// predicate. When a seed exists, exact normalized bindings make it
|
|
2351
|
+
// impossible for a compiler to drop, broaden, or replace a user filter.
|
|
2352
|
+
const normalize = (field, operator, value) => `${field.trim().toLowerCase()}|${operator.trim().toLowerCase()}|${value.trim().toLowerCase()}`;
|
|
2353
|
+
const expected = seedFilters.map((filter) => normalize(filter.field, 'equals', filter.value)).sort();
|
|
2354
|
+
const actual = program.filters
|
|
2355
|
+
.map((filter) => filter.fieldTerms.map((field) => normalize(field, filter.operator, filter.value)))
|
|
2356
|
+
.flat()
|
|
2357
|
+
.sort();
|
|
2358
|
+
return expected.length === actual.length && expected.every((value, index) => value === actual[index]);
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* V3 carries raw current-turn atoms specifically so an incomplete parser or
|
|
2362
|
+
* compatibility adapter cannot erase a literal and allow a broad certified
|
|
2363
|
+
* block to look complete. A literal must be represented by a frozen predicate
|
|
2364
|
+
* before any compiler can select a tier. Prior-result anchors are distinct
|
|
2365
|
+
* host-bound continuity evidence and therefore have their own source.
|
|
2366
|
+
*/
|
|
2367
|
+
function unboundProgramV3CurrentQuestionLiterals(program) {
|
|
2368
|
+
if (program.version !== 3)
|
|
2369
|
+
return [];
|
|
2370
|
+
const normalize = (value) => value.trim().toLowerCase();
|
|
2371
|
+
const frozenValues = new Set(program.filters.map((filter) => normalize(filter.value)));
|
|
2372
|
+
const trustedValues = new Set(program.trustedTaskAnchors
|
|
2373
|
+
.filter((anchor) => anchor.kind === 'member_binding' || anchor.kind === undefined)
|
|
2374
|
+
.flatMap((anchor) => anchor.values)
|
|
2375
|
+
.map(normalize));
|
|
2376
|
+
return program.inputAtoms
|
|
2377
|
+
.filter((atom) => atom.source === 'current_question' && atom.role === 'member')
|
|
2378
|
+
.map((atom) => atom.term)
|
|
2379
|
+
.filter((term) => !frozenValues.has(normalize(term)) && !trustedValues.has(normalize(term)));
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* A missing/ambiguous literal binding is an answerability gap, not evidence
|
|
2383
|
+
* that the already-built program was tampered with. Keep it pre-freeze and
|
|
2384
|
+
* show the actual terminal cause, so Ask can safely explain why it did not
|
|
2385
|
+
* generate a broad exploratory query.
|
|
2386
|
+
*/
|
|
2387
|
+
function currentQuestionLiteralCoverageGapDecision(input) {
|
|
2388
|
+
const normalize = (value) => value.trim().toLowerCase();
|
|
2389
|
+
const literalSet = new Set(input.literals.map(normalize));
|
|
2390
|
+
const matchingColumns = input.executionCandidates.filter((candidate) => candidate.kind === 'sql_column'
|
|
2391
|
+
&& evidenceCandidateRoles(candidate).includes('categorical_dimension')
|
|
2392
|
+
&& (candidate.safeValueEvidence ?? []).some((evidence) => literalSet.has(normalize(evidence.normalizedValue)) || literalSet.has(normalize(evidence.value))));
|
|
2393
|
+
const qualifiedColumnIds = [...new Set(matchingColumns.map((candidate) => candidate.qualifiedId ?? candidate.id))].sort();
|
|
2394
|
+
const ambiguity = qualifiedColumnIds.length > 1;
|
|
2395
|
+
const literalLabel = input.literals.map((literal) => `“${literal}”`).join(', ');
|
|
2396
|
+
const message = ambiguity
|
|
2397
|
+
? `The current-question value ${literalLabel} matched multiple qualified categorical fields in this metadata snapshot, so DQL did not choose or broaden a generated SQL filter.`
|
|
2398
|
+
: `The current-question value ${literalLabel} was not verified on one selected qualified categorical field in this metadata snapshot, so DQL did not broaden a generated SQL filter.`;
|
|
2399
|
+
const candidateIds = qualifiedColumnIds.length > 0
|
|
2400
|
+
? qualifiedColumnIds
|
|
2401
|
+
: [...new Set(input.program.candidateIds)].sort();
|
|
2402
|
+
const coverage = sourceCoverageFromEvidence(input.evidence, input.executionCandidates);
|
|
2403
|
+
const attempts = [
|
|
2404
|
+
{ version: 1, tier: 'certified', outcome: 'ineligible', candidateIds: coverage.find((item) => item.source === 'certified')?.candidateIds ?? [], reason: 'A current-question literal is not bound in the frozen program.', planFrozen: false },
|
|
2405
|
+
{ version: 1, tier: 'semantic', outcome: 'ineligible', candidateIds: coverage.find((item) => item.source === 'semantic')?.candidateIds ?? [], reason: 'No selected semantic member binding proved the current literal.', planFrozen: false },
|
|
2406
|
+
{ version: 1, tier: 'governed_relational', outcome: 'unavailable', candidateIds: coverage.find((item) => item.source === 'governed_relational')?.candidateIds ?? [], reason: 'A relationship cannot bind an unverified literal value by itself.', planFrozen: false },
|
|
2407
|
+
{ version: 1, tier: 'exploratory_sql', outcome: 'unavailable', candidateIds: qualifiedColumnIds, reason: ambiguity ? 'More than one qualified physical field matched the literal.' : 'No selected qualified physical field had exact safe-value evidence for the literal.', planFrozen: false },
|
|
2408
|
+
{ version: 1, tier: 'clarify_or_gap', outcome: 'unavailable', candidateIds, reason: message, planFrozen: false },
|
|
2409
|
+
];
|
|
2410
|
+
return {
|
|
2411
|
+
...input.base,
|
|
2412
|
+
action: 'block',
|
|
2413
|
+
confidence: 1,
|
|
2414
|
+
followsUp: false,
|
|
2415
|
+
source: 'heuristic',
|
|
2416
|
+
category: 'data_lookup',
|
|
2417
|
+
depth: 'quick',
|
|
2418
|
+
reason: message,
|
|
2419
|
+
requiresClarification: false,
|
|
2420
|
+
retrievalEvidence: retrievalTrace(input.evidence, input.executionCandidates),
|
|
2421
|
+
resolvedAnalyticalPlan: undefined,
|
|
2422
|
+
analyticalCascadeDecision: buildAnalyticalCascadeDecision({
|
|
2423
|
+
requirements: input.requirements,
|
|
2424
|
+
sourceCoverage: coverage,
|
|
2425
|
+
attempts,
|
|
2426
|
+
planFrozen: false,
|
|
2427
|
+
stopReason: 'coverage_gap',
|
|
2428
|
+
}),
|
|
2429
|
+
terminalOutcome: {
|
|
2430
|
+
kind: 'modeling_gap',
|
|
2431
|
+
code: 'ANALYTICAL_MODELING_GAP',
|
|
2432
|
+
message,
|
|
2433
|
+
candidateIds,
|
|
2434
|
+
gap: {
|
|
2435
|
+
code: 'MISSING_ATTRIBUTE',
|
|
2436
|
+
missing: ambiguity
|
|
2437
|
+
? ['one unambiguous qualified categorical field for the current-question value']
|
|
2438
|
+
: ['one qualified categorical field with exact safe-value evidence for the current-question value'],
|
|
2439
|
+
witnessCandidateIds: candidateIds,
|
|
2440
|
+
},
|
|
2441
|
+
},
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
1992
2444
|
/**
|
|
1993
2445
|
* A block may be relevant to a metric without returning that metric. When a
|
|
1994
2446
|
* stale catalog result or provider selection nominates such a block, continue
|
|
@@ -2291,7 +2743,7 @@ function clarificationOptionsForQualifiedIds(ids, candidates, requirements) {
|
|
|
2291
2743
|
.map(({ id, candidate }) => {
|
|
2292
2744
|
return {
|
|
2293
2745
|
id: candidate?.id ?? id,
|
|
2294
|
-
label: candidate?.name
|
|
2746
|
+
label: clarificationOptionLabel(candidate?.name, candidate?.qualifiedId ?? id),
|
|
2295
2747
|
...(candidate?.definition?.trim() ? { description: candidate.definition.trim() } : {}),
|
|
2296
2748
|
kind: candidate?.kind ?? 'semantic_member',
|
|
2297
2749
|
};
|
|
@@ -2301,6 +2753,19 @@ function qualifiedIdLabel(id) {
|
|
|
2301
2753
|
const local = id.split(/[:./]/).filter(Boolean).at(-1) ?? id;
|
|
2302
2754
|
return local.replace(/[_-]+/g, ' ').replace(/\b\w/g, (character) => character.toUpperCase());
|
|
2303
2755
|
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Candidate names are canonical metadata values, not necessarily presentation
|
|
2758
|
+
* labels. Keep a curated label intact, but humanize local snake/kebab names so
|
|
2759
|
+
* a stable clarification never asks a business user to choose `account_name`.
|
|
2760
|
+
*/
|
|
2761
|
+
function clarificationOptionLabel(name, fallbackId) {
|
|
2762
|
+
const trimmed = name?.trim();
|
|
2763
|
+
if (!trimmed)
|
|
2764
|
+
return qualifiedIdLabel(fallbackId);
|
|
2765
|
+
return /[_-]/.test(trimmed)
|
|
2766
|
+
? trimmed.replace(/[_-]+/g, ' ').replace(/\b\w/g, (character) => character.toUpperCase())
|
|
2767
|
+
: trimmed;
|
|
2768
|
+
}
|
|
2304
2769
|
function routedClarificationQuestion(resolution, bindings, options) {
|
|
2305
2770
|
if (resolution.clarifyingQuestion && !/^The analytical frame has unresolved ambiguity:/i.test(resolution.clarifyingQuestion)) {
|
|
2306
2771
|
return resolution.clarifyingQuestion;
|
|
@@ -2636,8 +3101,8 @@ function buildClarificationOptions(candidates, requirements) {
|
|
|
2636
3101
|
return {
|
|
2637
3102
|
id: candidate.id,
|
|
2638
3103
|
label: ambiguousName
|
|
2639
|
-
? `${candidate.name} (${candidateKindLabel(candidate.kind)})`
|
|
2640
|
-
: candidate.name,
|
|
3104
|
+
? `${clarificationOptionLabel(candidate.name, candidate.qualifiedId ?? candidate.id)} (${candidateKindLabel(candidate.kind)})`
|
|
3105
|
+
: clarificationOptionLabel(candidate.name, candidate.qualifiedId ?? candidate.id),
|
|
2641
3106
|
...(description ? { description } : {}),
|
|
2642
3107
|
kind: candidate.kind,
|
|
2643
3108
|
};
|
|
@@ -3251,6 +3716,7 @@ function preventDegenerateRankingResolution(resolution, evidence, candidates, qu
|
|
|
3251
3716
|
if (hasExplicitRankingMeasure(question, evidence))
|
|
3252
3717
|
return resolution;
|
|
3253
3718
|
const selected = candidates.find((candidate) => candidate.id === resolution.recommendedExecutionId
|
|
3719
|
+
|| candidate.qualifiedId === resolution.recommendedExecutionId
|
|
3254
3720
|
|| resolution.selectedConceptIds.includes(candidate.id));
|
|
3255
3721
|
if (!selected || !isDegenerateRankingMetric(question, evidence, selected))
|
|
3256
3722
|
return resolution;
|
|
@@ -3280,25 +3746,64 @@ function preventDegenerateRankingResolution(resolution, evidence, candidates, qu
|
|
|
3280
3746
|
};
|
|
3281
3747
|
}
|
|
3282
3748
|
function isSameSnapshotCategoricalExtensionForMetric(candidate, metricCandidate) {
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3749
|
+
return Boolean(proveSameSnapshotMetricflowRoleExtensionV1({ candidate, metricCandidate }));
|
|
3750
|
+
}
|
|
3751
|
+
/**
|
|
3752
|
+
* A planner/runtime selection can name a semantic display card through its
|
|
3753
|
+
* registry identity while the metric capability names the same field through
|
|
3754
|
+
* its MetricFlow identity. That is common for dbt MetricFlow exports:
|
|
3755
|
+
* `semantic:dimension:customers.customer_name` is the selected registry card,
|
|
3756
|
+
* while the metric declares
|
|
3757
|
+
* `semantic:uncategorized:dimension:customers.customer_name` as its native
|
|
3758
|
+
* grouping field.
|
|
3759
|
+
*
|
|
3760
|
+
* Do not reopen retrieval to bridge those forms. When (and only when) the
|
|
3761
|
+
* already-selected semantic member maps uniquely to an exact group-by
|
|
3762
|
+
* dimension on the already-selected metric capability, record the
|
|
3763
|
+
* same-snapshot proof on that selected card. The existing semantic compiler
|
|
3764
|
+
* still checks the normalized capability, additivity, and native grouping
|
|
3765
|
+
* path before freezing. This lets a complete singular tuple execute without
|
|
3766
|
+
* permitting a correlated metric, a numeric order attribute, or any
|
|
3767
|
+
* unselected workspace card to become meaning authority.
|
|
3768
|
+
*/
|
|
3769
|
+
function bindSelectedMetricFlowGroupingExtensions(metricCandidate, candidates) {
|
|
3299
3770
|
const capability = normalizeEvidenceAnalyticalCapability(metricCandidate).capability;
|
|
3300
|
-
|
|
3301
|
-
|
|
3771
|
+
if (!capability)
|
|
3772
|
+
return candidates;
|
|
3773
|
+
const normalizedIdentitySet = (candidate) => new Set([
|
|
3774
|
+
candidate.id,
|
|
3775
|
+
candidate.qualifiedId,
|
|
3776
|
+
candidate.name,
|
|
3777
|
+
...(candidate.aliases ?? []),
|
|
3778
|
+
].filter((identity) => Boolean(identity)).map(normalizeMetricPhrase).filter(Boolean));
|
|
3779
|
+
const dimensionIdentities = (dimension) => new Set([
|
|
3780
|
+
dimension.dimensionId,
|
|
3781
|
+
dimension.label,
|
|
3782
|
+
...(dimension.aliases ?? []),
|
|
3783
|
+
].filter((identity) => Boolean(identity)).map(normalizeMetricPhrase).filter(Boolean));
|
|
3784
|
+
return candidates.map((candidate) => {
|
|
3785
|
+
if (candidate.kind !== 'semantic_member'
|
|
3786
|
+
|| candidate.compatibility === 'incompatible'
|
|
3787
|
+
|| candidate.sameSnapshotRoleExtension)
|
|
3788
|
+
return candidate;
|
|
3789
|
+
const identities = normalizedIdentitySet(candidate);
|
|
3790
|
+
const matchedDimensions = capability.dimensions.filter((dimension) => dimension.supportedRoles.includes('group_by')
|
|
3791
|
+
&& [...dimensionIdentities(dimension)].some((identity) => identities.has(identity)));
|
|
3792
|
+
if (matchedDimensions.length !== 1)
|
|
3793
|
+
return candidate;
|
|
3794
|
+
const dimension = matchedDimensions[0];
|
|
3795
|
+
return {
|
|
3796
|
+
...candidate,
|
|
3797
|
+
sameSnapshotRoleExtension: {
|
|
3798
|
+
version: 1,
|
|
3799
|
+
role: 'categorical_dimension',
|
|
3800
|
+
requestedTerm: candidate.name,
|
|
3801
|
+
metricId: capability.metricId,
|
|
3802
|
+
dimensionId: dimension.dimensionId,
|
|
3803
|
+
basis: 'exact_metricflow_grouping_dimension',
|
|
3804
|
+
},
|
|
3805
|
+
};
|
|
3806
|
+
});
|
|
3302
3807
|
}
|
|
3303
3808
|
/**
|
|
3304
3809
|
* A model chooses from the bounded package, but it cannot remove a unique
|
|
@@ -3323,11 +3828,14 @@ function directResolution(request, evidence, candidate, candidates, selectedDime
|
|
|
3323
3828
|
// model path. Raw parser terms are retrieval hints only: they may not turn a
|
|
3324
3829
|
// normalized business alias back into a phantom metric or dimension after
|
|
3325
3830
|
// the meaning boundary has been intentionally skipped.
|
|
3326
|
-
const requirementSeed =
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3831
|
+
const requirementSeed = request.hostRequirementSeed?.version === 1
|
|
3832
|
+
&& request.hostRequirementSeed.sourceQuestion === request.question
|
|
3833
|
+
? request.hostRequirementSeed
|
|
3834
|
+
: buildAnalyticalRequirementSeedV1({
|
|
3835
|
+
question: request.question,
|
|
3836
|
+
parsedIntent: evidence.parsedIntent,
|
|
3837
|
+
fiscalCalendar: declaredFiscalCalendar(evidence, candidates),
|
|
3838
|
+
});
|
|
3331
3839
|
const hostOwnedEvidence = {
|
|
3332
3840
|
...evidence,
|
|
3333
3841
|
parsedIntent: {
|
|
@@ -3448,7 +3956,14 @@ function directResolution(request, evidence, candidate, candidates, selectedDime
|
|
|
3448
3956
|
.map((item) => item.id)
|
|
3449
3957
|
.concat(selectedDimensionConceptIds, hostBoundDimensionConceptIds)
|
|
3450
3958
|
.filter((id, index, all) => all.indexOf(id) === index),
|
|
3451
|
-
|
|
3959
|
+
// Meaning receipts retain the source-qualified identity selected from the
|
|
3960
|
+
// immutable workspace. The frozen semantic compiler plan may normalize
|
|
3961
|
+
// that handle to its canonical execution ID later, but rewriting it here
|
|
3962
|
+
// breaks the provenance link between the model/meaning choice and the
|
|
3963
|
+
// selected semantic evidence card.
|
|
3964
|
+
recommendedExecutionId: candidate.kind === 'semantic_metric'
|
|
3965
|
+
? candidate.qualifiedId ?? candidate.id
|
|
3966
|
+
: candidate.id,
|
|
3452
3967
|
queryIntent: { ...queryIntentWithSelectedDimensions, filters: canonicalFilters },
|
|
3453
3968
|
rejectedCandidates: [],
|
|
3454
3969
|
confidence: "high",
|
|
@@ -3571,6 +4086,86 @@ function attachHostOwnedAnalyticalFrame(input) {
|
|
|
3571
4086
|
...(overrideReceipts?.length ? { overrideReceipts } : {}),
|
|
3572
4087
|
};
|
|
3573
4088
|
}
|
|
4089
|
+
/**
|
|
4090
|
+
* Bind an already validated runtime meaning selection to the immutable
|
|
4091
|
+
* host-owned analytical frame. This is intentionally a narrow compiler
|
|
4092
|
+
* adapter: it does not retrieve, rank candidates, call a provider, or change
|
|
4093
|
+
* the runtime program. Keeping the V2 frame construction here lets the
|
|
4094
|
+
* existing MetricFlow compiler retain its capability/additivity validation
|
|
4095
|
+
* without returning ownership of interpretation to the legacy router.
|
|
4096
|
+
*/
|
|
4097
|
+
export function bindAskAnalystProgramMeaningV1(input) {
|
|
4098
|
+
// The runtime has already verified which cards express the business tuple.
|
|
4099
|
+
// `input.candidates` can still contain the 16-card planner package so the
|
|
4100
|
+
// compiler has definition/relationship context, but it is not permission
|
|
4101
|
+
// to rebuild the frame with every correlated metric in that package. In the
|
|
4102
|
+
// retained Jaffle snapshot, that turned the singular Orders metric into
|
|
4103
|
+
// Orders + Large Orders + Food Orders + Drink Orders after verification.
|
|
4104
|
+
// Preserve only server-validated selection identities here; execution
|
|
4105
|
+
// closure remains a separate compiler input later in the cascade.
|
|
4106
|
+
const selectedIdentityIds = new Set([
|
|
4107
|
+
...input.resolution.selectedConceptIds,
|
|
4108
|
+
...(input.resolution.recommendedExecutionId ? [input.resolution.recommendedExecutionId] : []),
|
|
4109
|
+
]);
|
|
4110
|
+
const selectedCandidates = input.candidates.filter((candidate) => [candidate.id, candidate.qualifiedId]
|
|
4111
|
+
.filter((identity) => Boolean(identity))
|
|
4112
|
+
.some((identity) => selectedIdentityIds.has(identity)));
|
|
4113
|
+
// Legacy/direct callers can lack a selected card only when they supplied an
|
|
4114
|
+
// incomplete resolution. Keep their prior behavior rather than fabricating
|
|
4115
|
+
// an empty frame; authoritative Ask always takes the narrow branch above.
|
|
4116
|
+
const framingCandidates = selectedCandidates.length > 0 ? selectedCandidates : input.candidates;
|
|
4117
|
+
const metricCandidate = framingCandidates.find((candidate) => candidate.kind === 'semantic_metric'
|
|
4118
|
+
&& (candidate.id === input.resolution.recommendedExecutionId
|
|
4119
|
+
|| candidate.qualifiedId === input.resolution.recommendedExecutionId
|
|
4120
|
+
|| input.resolution.selectedConceptIds.some((identity) => candidate.id === identity || candidate.qualifiedId === identity)));
|
|
4121
|
+
const compilerFramingCandidates = metricCandidate
|
|
4122
|
+
? bindSelectedMetricFlowGroupingExtensions(metricCandidate, framingCandidates)
|
|
4123
|
+
: framingCandidates;
|
|
4124
|
+
// The Ask runtime may rebuild the compiler-facing seed after verifying
|
|
4125
|
+
// canonical planner bindings. `attachHostOwnedAnalyticalFrame` deliberately
|
|
4126
|
+
// preserves most of the incoming resolution, so explicitly carry that
|
|
4127
|
+
// verified seed forward here. Otherwise a valid same-snapshot extension
|
|
4128
|
+
// can produce a frame with `locations.location_name`, only for RAP binding
|
|
4129
|
+
// to reopen the stale pre-planner phrase `region` and mark it unresolved.
|
|
4130
|
+
const frameBound = {
|
|
4131
|
+
...attachHostOwnedAnalyticalFrame({ ...input, candidates: compilerFramingCandidates }),
|
|
4132
|
+
hostRequirementSeed: input.requirementSeed,
|
|
4133
|
+
};
|
|
4134
|
+
const protectedResolution = preventDegenerateRankingResolution(frameBound, input.evidence, compilerFramingCandidates, input.requirementSeed.sourceQuestion);
|
|
4135
|
+
// This adapter may enrich a selected MetricFlow tuple with host-owned
|
|
4136
|
+
// dimensions, but it may not silently subtract a verified runtime
|
|
4137
|
+
// selection. `attachHostOwnedAnalyticalFrame` rebuilds legacy resolution
|
|
4138
|
+
// fields for compiler compatibility; some native Measure -> MetricFlow
|
|
4139
|
+
// Metric pairs intentionally have different text identities (for example
|
|
4140
|
+
// `order_count` -> `Orders`). Keep every already-validated candidate ID
|
|
4141
|
+
// from the authoritative program, limited to this same candidate package.
|
|
4142
|
+
// The verifier/compiler still prove the metric's capability, grain, and
|
|
4143
|
+
// relationship closure before freeze.
|
|
4144
|
+
// The MetricFlow framing subset is allowed to shape legacy metric/grouping
|
|
4145
|
+
// metadata, but it is not the authority boundary for a V3 selection. A
|
|
4146
|
+
// verified planner can legitimately retain a qualified physical output or
|
|
4147
|
+
// context field beside its semantic metric. Keep those already-admitted
|
|
4148
|
+
// identities through the compatibility carrier; they remain subject to the
|
|
4149
|
+
// immutable compiler program's physical/semantic safety proof below.
|
|
4150
|
+
const allowed = new Set(input.candidates.flatMap((candidate) => [
|
|
4151
|
+
candidate.id,
|
|
4152
|
+
candidate.qualifiedId ?? candidate.id,
|
|
4153
|
+
]));
|
|
4154
|
+
const preservedSelectedConceptIds = input.resolution.selectedConceptIds
|
|
4155
|
+
.filter((identity) => allowed.has(identity));
|
|
4156
|
+
const selectedConceptIds = [...new Set([
|
|
4157
|
+
...protectedResolution.selectedConceptIds,
|
|
4158
|
+
...preservedSelectedConceptIds,
|
|
4159
|
+
])];
|
|
4160
|
+
const originalRecommendedExecutionId = input.resolution.recommendedExecutionId;
|
|
4161
|
+
return {
|
|
4162
|
+
...protectedResolution,
|
|
4163
|
+
selectedConceptIds,
|
|
4164
|
+
...(originalRecommendedExecutionId && allowed.has(originalRecommendedExecutionId)
|
|
4165
|
+
? { recommendedExecutionId: originalRecommendedExecutionId }
|
|
4166
|
+
: {}),
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
3574
4169
|
function explicitlyRequestedMetricCandidates(question, evidence, primary, candidates) {
|
|
3575
4170
|
const requirements = buildAnalyticalRequirementSet({ question, parsedIntent: evidence.parsedIntent });
|
|
3576
4171
|
const requested = requirements.ranking?.metricTerms.length
|
|
@@ -4775,13 +5370,7 @@ function sameSnapshotRoleTargetedMeaningExtensions(input) {
|
|
|
4775
5370
|
const requestedCategoricalTerms = categoricalDimensionRequirementTerms(input.requirements);
|
|
4776
5371
|
for (const candidate of input.clarificationCandidates) {
|
|
4777
5372
|
const extension = candidate.sameSnapshotRoleExtension;
|
|
4778
|
-
if (!extension
|
|
4779
|
-
|| extension.version !== 1
|
|
4780
|
-
|| extension.role !== 'categorical_dimension'
|
|
4781
|
-
|| (extension.basis !== 'sole_metricflow_grouping_dimension'
|
|
4782
|
-
&& extension.basis !== 'exact_metricflow_grouping_dimension')
|
|
4783
|
-
|| candidate.kind !== 'semantic_member'
|
|
4784
|
-
|| (candidate.qualifiedId ?? candidate.id) !== extension.dimensionId)
|
|
5373
|
+
if (!extension)
|
|
4785
5374
|
continue;
|
|
4786
5375
|
if (!requestedCategoricalTerms.some((requested) => normalizeMetricPhrase(requested) === normalizeMetricPhrase(extension.requestedTerm)))
|
|
4787
5376
|
continue;
|
|
@@ -4795,9 +5384,18 @@ function sameSnapshotRoleTargetedMeaningExtensions(input) {
|
|
|
4795
5384
|
if (input.candidates.some((admitted) => admitted.kind === 'semantic_member'
|
|
4796
5385
|
&& candidateMatchesCategoricalDimensionRequirement(admitted, [extension.requestedTerm])))
|
|
4797
5386
|
continue;
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
5387
|
+
// The compact package may retain a metric card across reloads, but only
|
|
5388
|
+
// its *current* normalized capability can authorize the companion
|
|
5389
|
+
// grouping card. Reuse the router/compiler/workspace proof rather than
|
|
5390
|
+
// accepting matching strings for metric/dimension identities.
|
|
5391
|
+
const sourceMetricProof = input.candidates.flatMap((admitted) => {
|
|
5392
|
+
const proof = proveSameSnapshotMetricflowRoleExtensionV1({
|
|
5393
|
+
candidate,
|
|
5394
|
+
metricCandidate: admitted,
|
|
5395
|
+
});
|
|
5396
|
+
return proof ? [proof] : [];
|
|
5397
|
+
});
|
|
5398
|
+
if (sourceMetricProof.length === 0)
|
|
4801
5399
|
continue;
|
|
4802
5400
|
const role = normalizeMetricPhrase(extension.requestedTerm);
|
|
4803
5401
|
const values = extensionsByRole.get(role) ?? [];
|
|
@@ -4879,8 +5477,12 @@ export function createHybridRouter(options = {}) {
|
|
|
4879
5477
|
if (request.runBudget && !request.runBudget.mayStartDiscovery(initialDiscoveryRoute)) {
|
|
4880
5478
|
return softBoundaryDecision(request, base, initialDiscoveryRoute);
|
|
4881
5479
|
}
|
|
4882
|
-
|
|
4883
|
-
|
|
5480
|
+
// AskAnalystRuntimeV1 acquires one immutable snapshot before invoking
|
|
5481
|
+
// this compiler broker. Reusing that host-only handoff prevents a second
|
|
5482
|
+
// retrieval/ranking decision from silently becoming another source of
|
|
5483
|
+
// truth. Legacy callers retain the original retrieval behavior.
|
|
5484
|
+
let evidence = request.askAnalystEvidence;
|
|
5485
|
+
if (!evidence && options.getEvidence) {
|
|
4884
5486
|
try {
|
|
4885
5487
|
evidence = await options.getEvidence(request);
|
|
4886
5488
|
}
|
|
@@ -4911,6 +5513,47 @@ export function createHybridRouter(options = {}) {
|
|
|
4911
5513
|
]).filter((candidate) => evidence.clarificationCandidates.some((item) => item.id === candidate.id)) }
|
|
4912
5514
|
: {}),
|
|
4913
5515
|
};
|
|
5516
|
+
// Authoritative AskAnalystRuntimeV1 already completed the bounded
|
|
5517
|
+
// interpretation/tool phase. Do not re-run legacy candidate ranking,
|
|
5518
|
+
// clarification heuristics, or a provider call here: this layer is a
|
|
5519
|
+
// compiler broker only and can validate exactly the immutable program
|
|
5520
|
+
// selection against safe compiler primitives.
|
|
5521
|
+
if (request.askAnalystProgram && request.askAnalystMeaningResolution) {
|
|
5522
|
+
const programIds = new Set(request.askAnalystProgram.candidateIds);
|
|
5523
|
+
const executionIds = new Set(request.askAnalystProgram.executionCandidateIds
|
|
5524
|
+
?? request.askAnalystProgram.candidateIds);
|
|
5525
|
+
const programCandidates = evidence.candidates.filter((candidate) => programIds.has(candidate.id) || programIds.has(candidate.qualifiedId ?? candidate.id));
|
|
5526
|
+
const executionCandidates = evidence.candidates.filter((candidate) => executionIds.has(candidate.id) || executionIds.has(candidate.qualifiedId ?? candidate.id));
|
|
5527
|
+
const requirements = request.askAnalystState?.frame.requirements
|
|
5528
|
+
?? request.hostRequirementSeed?.requirements;
|
|
5529
|
+
if (!requirements || programCandidates.length === 0) {
|
|
5530
|
+
return {
|
|
5531
|
+
...base,
|
|
5532
|
+
action: 'block',
|
|
5533
|
+
confidence: 1,
|
|
5534
|
+
reason: 'The Ask runtime program could not be compiled because its snapshot-bound candidate selection was unavailable.',
|
|
5535
|
+
source: 'heuristic',
|
|
5536
|
+
followsUp: false,
|
|
5537
|
+
terminalOutcome: {
|
|
5538
|
+
kind: 'modeling_gap',
|
|
5539
|
+
code: 'ANALYTICAL_MODELING_GAP',
|
|
5540
|
+
message: 'The immutable Ask runtime program did not retain a compiler-eligible candidate selection.',
|
|
5541
|
+
candidateIds: request.askAnalystProgram.candidateIds,
|
|
5542
|
+
},
|
|
5543
|
+
};
|
|
5544
|
+
}
|
|
5545
|
+
return compileAskAnalyticalProgramV1({
|
|
5546
|
+
base,
|
|
5547
|
+
request,
|
|
5548
|
+
evidence,
|
|
5549
|
+
program: request.askAnalystProgram,
|
|
5550
|
+
candidates: programCandidates,
|
|
5551
|
+
executionCandidates,
|
|
5552
|
+
resolution: request.askAnalystMeaningResolution,
|
|
5553
|
+
requirements,
|
|
5554
|
+
mode: options.resolvedPlanMode ?? 'authoritative',
|
|
5555
|
+
});
|
|
5556
|
+
}
|
|
4914
5557
|
// Ranking measure identity is resolved from the same immutable
|
|
4915
5558
|
// request/evidence frame that drives package reservation. A metric
|
|
4916
5559
|
// may remain in broad retrieval context, but it cannot become a
|
|
@@ -4976,7 +5619,7 @@ export function createHybridRouter(options = {}) {
|
|
|
4976
5619
|
// supplied so explicit revenue, entity labels, and time roles cannot be
|
|
4977
5620
|
// pruned by unrelated lexical matches.
|
|
4978
5621
|
let candidates = buildMeaningEvidencePackage(evidence, options.maxMeaningCandidates ?? 16, continuationQuestion);
|
|
4979
|
-
|
|
5622
|
+
recordAskCandidateLifecycleV1(request, evidence, retrievedCandidates, candidates);
|
|
4980
5623
|
// The complete already-retrieved set is retained only to validate a
|
|
4981
5624
|
// server-issued stable selection before any route can fall through to
|
|
4982
5625
|
// a generic/generated answer. New free-text choices are constrained to
|
|
@@ -5118,11 +5761,14 @@ export function createHybridRouter(options = {}) {
|
|
|
5118
5761
|
// matched semantic metric remains the only execution/measure
|
|
5119
5762
|
// authority. A selected dimension must therefore never flow through
|
|
5120
5763
|
// the metric-only direct-resolution path as the primary candidate.
|
|
5764
|
+
const canonicalLiteral = canonicalLiteralEvidenceReference(request.question, candidates);
|
|
5121
5765
|
const explicit = selectedDimensionBinding?.metricCandidate
|
|
5122
5766
|
?? selectedEvidence
|
|
5123
|
-
?? findExplicitEvidenceReference(request.question, candidates)
|
|
5767
|
+
?? findExplicitEvidenceReference(request.question, candidates)
|
|
5768
|
+
?? canonicalLiteral;
|
|
5124
5769
|
const explicitMeaningBinding = Boolean(explicit && (request.selectedEvidenceId
|
|
5125
|
-
|| /@(metric|block|model|table|column)\(/i.test(request.question)
|
|
5770
|
+
|| /@(metric|block|model|table|column)\(/i.test(request.question)
|
|
5771
|
+
|| canonicalLiteral?.id === explicit.id));
|
|
5126
5772
|
const shouldUseMeaningCall = requireMeaningCall
|
|
5127
5773
|
&& !explicitMeaningBinding
|
|
5128
5774
|
&& Boolean(options.resolveMeaning || options.complete);
|
|
@@ -5199,8 +5845,12 @@ export function createHybridRouter(options = {}) {
|
|
|
5199
5845
|
// did run, committing to the best governed reading is the whole point.
|
|
5200
5846
|
let meaningResolverReachable = true;
|
|
5201
5847
|
try {
|
|
5202
|
-
|
|
5203
|
-
|
|
5848
|
+
// Meaning is the one bounded planning continuation of an ordinary
|
|
5849
|
+
// generated Ask, not a user clarification. Charging it to the
|
|
5850
|
+
// short clarify target was the direct cause of the 1.14.x
|
|
5851
|
+
// "discovery window ended" loop before semantic/raw fallback.
|
|
5852
|
+
if (request.runBudget && !request.runBudget.mayStartDiscovery('generated_answer')) {
|
|
5853
|
+
return softBoundaryDecision(request, base, 'generated_answer');
|
|
5204
5854
|
}
|
|
5205
5855
|
let resolution;
|
|
5206
5856
|
try {
|
|
@@ -5490,15 +6140,42 @@ function softBoundaryDecision(request, base, route) {
|
|
|
5490
6140
|
const seconds = Math.round((request.runBudget?.softTargetMs(route) ?? 15_000) / 1_000);
|
|
5491
6141
|
return {
|
|
5492
6142
|
...base,
|
|
5493
|
-
|
|
6143
|
+
// A budget/admission boundary is an operational condition, never a
|
|
6144
|
+
// business ambiguity. Returning `clarify` here made the UI ask users to
|
|
6145
|
+
// choose a metric or grain even though no competing executable meanings
|
|
6146
|
+
// had been validated. Keep this a typed terminal block so diagnostics can
|
|
6147
|
+
// offer a retry/recovery action without teaching users to compensate for a
|
|
6148
|
+
// runtime deadline.
|
|
6149
|
+
action: 'block',
|
|
5494
6150
|
confidence: 1,
|
|
5495
6151
|
source: 'heuristic',
|
|
5496
|
-
requiresClarification:
|
|
6152
|
+
requiresClarification: false,
|
|
6153
|
+
clarifyingQuestion: undefined,
|
|
5497
6154
|
reason: `The ${seconds}-second discovery target elapsed before a plan was frozen, so DQL did not start another retrieval or provider branch.`,
|
|
5498
|
-
clarifyingQuestion: request.runBudget?.mode === 'research'
|
|
5499
|
-
? 'Research has stopped starting new branches. Would you like to narrow the question and retry?'
|
|
5500
|
-
: 'The discovery window ended before an exact plan was frozen. Which metric or grain should DQL use on retry?',
|
|
5501
6155
|
};
|
|
5502
6156
|
}
|
|
6157
|
+
/**
|
|
6158
|
+
* A literal stable metric/block identifier is already a user-selected meaning.
|
|
6159
|
+
* It is intentionally narrower than a human label such as "revenue": only
|
|
6160
|
+
* an identifier-shaped value (`foo_bar`, `namespace.metric`, `metric:foo`) or
|
|
6161
|
+
* a supplied exact qualified ID can bypass the one meaning call. This restores
|
|
6162
|
+
* the useful 1.13 zero-call semantic path without turning lexical retrieval
|
|
6163
|
+
* into permission to guess between similarly named business metrics.
|
|
6164
|
+
*/
|
|
6165
|
+
function canonicalLiteralEvidenceReference(question, candidates) {
|
|
6166
|
+
const normalizedQuestion = question.toLowerCase();
|
|
6167
|
+
const matches = candidates.filter((candidate) => {
|
|
6168
|
+
if (candidate.compatibility === 'incompatible' || candidate.eligible === false)
|
|
6169
|
+
return false;
|
|
6170
|
+
const identities = [candidate.qualifiedId, candidate.id, candidate.name, ...(candidate.aliases ?? [])]
|
|
6171
|
+
.filter((value) => Boolean(value))
|
|
6172
|
+
.filter((value) => /[_:.]/.test(value));
|
|
6173
|
+
return identities.some((identity) => {
|
|
6174
|
+
const escaped = identity.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
6175
|
+
return new RegExp(`(^|[^a-z0-9_.:-])${escaped}($|[^a-z0-9_.:-])`, 'i').test(normalizedQuestion);
|
|
6176
|
+
});
|
|
6177
|
+
});
|
|
6178
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
6179
|
+
}
|
|
5503
6180
|
export { intentForCategory, parseMeaningResolution };
|
|
5504
6181
|
//# sourceMappingURL=router.js.map
|