АРЧ АП11 - Вынести exact-lane truth gate для factual-negative и limited ответов в отдельный policy-owner

This commit is contained in:
dctouch 2026-04-17 09:41:29 +03:00
parent 6b14946f7e
commit dc8dfcf237
12 changed files with 1001 additions and 154 deletions

View File

@ -10,6 +10,7 @@ const composeStage_1 = require("./address_runtime/composeStage");
const addressCapabilityPolicy_1 = require("./addressCapabilityPolicy");
const addressRouteExpectations_1 = require("./addressRouteExpectations");
const assistantOrganizationMatcher_1 = require("./assistantOrganizationMatcher");
const addressTruthGatePolicy_1 = require("./addressTruthGatePolicy");
const openaiResponsesClient_1 = require("./openaiResponsesClient");
const files_1 = require("../utils/files");
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"];
@ -2509,20 +2510,8 @@ function buildLimitedExecutionResult(input) {
requestedResultMode: requestedResultMode,
resultMode: resultSemantics.result_mode
});
return {
handled: true,
reply_text: composeLimitedReply({
category: input.category,
reason: input.reasonText,
nextStep: input.nextStep,
shape: input.shape,
intent: input.intent.intent,
filters: input.filters,
missingRequiredFilters: input.missingRequiredFilters
}),
reply_type: "partial_coverage",
response_type: "LIMITED_WITH_REASON",
debug: {
const runtimeReadiness = runtimeReadinessForLimitedCategory(input.category);
const debugPayload = (0, addressTruthGatePolicy_1.attachAddressTruthGate)({
detected_mode: input.mode.mode,
detected_mode_confidence: input.mode.confidence,
query_shape: input.shape.shape,
@ -2556,7 +2545,7 @@ function buildLimitedExecutionResult(input) {
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
runtime_readiness: runtimeReadinessForLimitedCategory(input.category),
runtime_readiness: runtimeReadiness,
limited_reason_category: input.category,
semantic_frame: input.semanticFrame ?? null,
response_type: "LIMITED_WITH_REASON",
@ -2576,7 +2565,35 @@ function buildLimitedExecutionResult(input) {
...resultSemantics,
limitations: input.limitations,
reasons
}
}, {
intent: input.intent.intent,
filters: input.filters,
semanticFrame: input.semanticFrame ?? null,
selectedRecipe: input.selectedRecipe,
rowsMatched: input.rowsMatched,
limitedReasonCategory: input.category,
runtimeReadiness,
missingRequiredFilters: input.missingRequiredFilters,
limitations: input.limitations,
reasons,
routeExpectationStatus: routeExpectationAudit.status,
routeExpectationReason: routeExpectationAudit.reason,
replyType: "partial_coverage"
});
return {
handled: true,
reply_text: composeLimitedReply({
category: input.category,
reason: input.reasonText,
nextStep: input.nextStep,
shape: input.shape,
intent: input.intent.intent,
filters: input.filters,
missingRequiredFilters: input.missingRequiredFilters
}),
reply_type: "partial_coverage",
response_type: "LIMITED_WITH_REASON",
debug: debugPayload
};
}
function composeOrganizationClarificationReply(organizations) {
@ -3322,12 +3339,15 @@ class AddressQueryService {
responseType,
rowsMatched: 0
}), undefined);
return {
handled: true,
reply_text: replyText,
reply_type: (0, composeStage_1.inferReplyType)(responseType),
response_type: responseType,
debug: {
const factualNoRowsLimitations = [...filters.warnings, ...extraLimitations];
const factualNoRowsReasons = withConfirmedBalanceFallbackReason([...baseReasons, noRowsReason], requestedResultMode, undefined, semantics.result_mode);
const routeExpectationAudit = buildRouteExpectationAudit({
intent: routeExpectationIntent,
selectedRecipe: effectiveRecipeId,
requestedResultMode,
resultMode: semantics.result_mode
});
const debugPayload = (0, addressTruthGatePolicy_1.attachAddressTruthGate)({
detected_mode: mode.mode,
detected_mode_confidence: mode.confidence,
query_shape: shape.shape,
@ -3364,9 +3384,14 @@ class AddressQueryService {
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
limited_reason_category: null,
response_type: responseType,
route_expectation_status: routeExpectationAudit.status,
route_expectation_reason: routeExpectationAudit.reason,
route_expectation_expected_selected_recipes: routeExpectationAudit.expectedSelectedRecipes,
route_expectation_expected_requested_result_modes: routeExpectationAudit.expectedRequestedResultModes,
route_expectation_expected_result_modes: routeExpectationAudit.expectedResultModes,
...semantics,
limitations: [...filters.warnings, ...extraLimitations],
reasons: withConfirmedBalanceFallbackReason([...baseReasons, noRowsReason], requestedResultMode, undefined, semantics.result_mode),
limitations: factualNoRowsLimitations,
reasons: factualNoRowsReasons,
...(capabilityAudit
? {
capability_id: capabilityAudit.capabilityId,
@ -3378,10 +3403,31 @@ class AddressQueryService {
: {}),
...(shadowRouteAudit
? {
shadow_route_intent: shadowRouteAudit.intent,
shadow_route_selected_recipe: shadowRouteAudit.selectedRecipe,
shadow_route_status: shadowRouteAudit.status
}
: {})
}
}, {
intent: intent.intent,
filters: filters.extracted_filters,
semanticFrame,
selectedRecipe: effectiveRecipeId,
rowsMatched: 0,
limitedReasonCategory: "empty_match",
runtimeReadiness: "LIVE_QUERYABLE_WITH_LIMITS",
limitations: factualNoRowsLimitations,
reasons: factualNoRowsReasons,
routeExpectationStatus: routeExpectationAudit.status,
routeExpectationReason: routeExpectationAudit.reason,
replyType: (0, composeStage_1.inferReplyType)(responseType)
});
return {
handled: true,
reply_text: replyText,
reply_type: (0, composeStage_1.inferReplyType)(responseType),
response_type: responseType,
debug: debugPayload
};
};
if (organizationWarehouseRecoveryApplied) {

View File

@ -0,0 +1,243 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ADDRESS_TRUTH_GATE_SCHEMA_VERSION = void 0;
exports.resolveAddressTruthGate = resolveAddressTruthGate;
exports.toAddressTruthGateContract = toAddressTruthGateContract;
exports.buildAddressTruthGateDebugFields = buildAddressTruthGateDebugFields;
exports.attachAddressTruthGate = attachAddressTruthGate;
exports.ADDRESS_TRUTH_GATE_SCHEMA_VERSION = "address_truth_gate_v1";
function toRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value;
}
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function toStringList(value) {
if (!Array.isArray(value)) {
return [];
}
return value
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item));
}
function isTruthGateStatus(value) {
return (value === "full_confirmed" ||
value === "partial_supported" ||
value === "blocked_missing_anchor" ||
value === "blocked_route_expectation_failure" ||
value === "blocked_execution_error" ||
value === "limited_temporal_or_contextual" ||
value === "unknown");
}
function isCarryoverDepth(value) {
return value === "full" || value === "root_only" || value === "object_only" || value === "meta_only" || value === "none";
}
function isLimitedReasonCategory(value) {
return (value === "empty_match" ||
value === "missing_anchor" ||
value === "recipe_visibility_gap" ||
value === "execution_error" ||
value === "unsupported");
}
function isRuntimeReadiness(value) {
return (value === "LIVE_QUERYABLE" ||
value === "LIVE_QUERYABLE_WITH_LIMITS" ||
value === "REQUIRES_SPECIALIZED_RECIPE" ||
value === "DEEP_ONLY" ||
value === "UNKNOWN");
}
function normalizeReasonCode(value) {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target, value) {
const text = toNonEmptyString(value);
if (!text) {
return;
}
const normalized = normalizeReasonCode(text);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function hasFilterScope(filters) {
if (!filters) {
return false;
}
const scopeKeys = [
"period_from",
"period_to",
"as_of_date",
"organization",
"counterparty",
"contract",
"account",
"item",
"warehouse",
"document_type",
"document_ref",
"status"
];
return scopeKeys.some((key) => toNonEmptyString(filters[key]) !== null);
}
function hasObjectFocus(input) {
if (toNonEmptyString(input.filters?.item)) {
return true;
}
if (input.semanticFrame?.selected_object_scope_detected) {
return true;
}
if (input.semanticFrame?.anchor_kind === "selected_object" || input.semanticFrame?.anchor_kind === "item") {
return true;
}
return (input.intent === "inventory_purchase_provenance_for_item" ||
input.intent === "inventory_purchase_documents_for_item" ||
input.intent === "inventory_sale_trace_for_item" ||
input.intent === "inventory_profitability_for_item" ||
input.intent === "inventory_purchase_to_sale_chain" ||
input.intent === "inventory_aging_by_purchase_date");
}
function hasReusableRootScope(input) {
if (hasFilterScope(input.filters)) {
return true;
}
return Boolean(input.semanticFrame && input.semanticFrame.scope_kind !== "none");
}
function truthGateStatusFrom(input) {
const missingRequiredFilters = input.missingRequiredFilters ?? [];
if (input.routeExpectationStatus === "mismatch") {
return "blocked_route_expectation_failure";
}
if (input.limitedReasonCategory === "execution_error") {
return "blocked_execution_error";
}
if (missingRequiredFilters.length > 0 || input.limitedReasonCategory === "missing_anchor") {
return "blocked_missing_anchor";
}
if (input.temporalGuardOutcome === "ambiguous_limited" || input.temporalAlignmentStatus === "conflicting") {
return "limited_temporal_or_contextual";
}
if (input.replyType === "factual" && input.limitedReasonCategory === "empty_match") {
return "full_confirmed";
}
if (input.limitedReasonCategory === "empty_match" ||
input.limitedReasonCategory === "recipe_visibility_gap" ||
input.limitedReasonCategory === "unsupported" ||
input.replyType === "partial_coverage") {
return "partial_supported";
}
if ((input.rowsMatched ?? 0) > 0) {
return "full_confirmed";
}
return "unknown";
}
function carryoverEligibilityFor(input, truthGateStatus) {
if (truthGateStatus.startsWith("blocked")) {
return "none";
}
if (input.limitedReasonCategory === "recipe_visibility_gap" || input.limitedReasonCategory === "unsupported") {
return "meta_only";
}
if (hasObjectFocus(input)) {
return "object_only";
}
if (truthGateStatus === "limited_temporal_or_contextual") {
return hasReusableRootScope(input) ? "root_only" : "meta_only";
}
if (truthGateStatus === "full_confirmed" || truthGateStatus === "partial_supported") {
return hasReusableRootScope(input) ? "root_only" : "meta_only";
}
return "none";
}
function explanationFor(truthGateStatus, limitedReasonCategory) {
if (truthGateStatus === "full_confirmed") {
return null;
}
if (truthGateStatus === "blocked_missing_anchor") {
return "required_anchor_missing";
}
if (truthGateStatus === "blocked_route_expectation_failure") {
return "route_expectation_failed";
}
if (truthGateStatus === "blocked_execution_error") {
return "execution_failed";
}
if (truthGateStatus === "limited_temporal_or_contextual") {
return "temporal_or_contextual_limit";
}
if (limitedReasonCategory === "recipe_visibility_gap") {
return "specialized_recipe_required";
}
if (limitedReasonCategory === "unsupported") {
return "unsupported_in_current_contour";
}
return truthGateStatus === "partial_supported" ? "evidence_or_coverage_is_partial" : "truth_gate_not_confirmed";
}
function collectReasonCodes(input, truthGateStatus) {
const reasons = [];
pushReason(reasons, `address_truth_gate_${truthGateStatus}`);
pushReason(reasons, input.routeExpectationReason);
pushReason(reasons, input.limitedReasonCategory ? `limited_category_${input.limitedReasonCategory}` : null);
(input.missingRequiredFilters ?? []).forEach((item) => pushReason(reasons, `missing_filter_${item}`));
(input.limitations ?? []).forEach((item) => pushReason(reasons, item));
(input.reasons ?? []).forEach((item) => pushReason(reasons, item));
return reasons.slice(0, 32);
}
function resolveAddressTruthGate(input) {
const truthGateStatus = truthGateStatusFrom(input);
return {
schema_version: exports.ADDRESS_TRUTH_GATE_SCHEMA_VERSION,
policy_owner: "addressTruthGatePolicy",
truth_gate_status: truthGateStatus,
carryover_eligibility: carryoverEligibilityFor(input, truthGateStatus),
limited_reason_category: input.limitedReasonCategory ?? null,
runtime_readiness: input.runtimeReadiness ?? "UNKNOWN",
reason_codes: collectReasonCodes(input, truthGateStatus),
blocked_or_limited_explanation: explanationFor(truthGateStatus, input.limitedReasonCategory ?? null)
};
}
function toAddressTruthGateContract(value) {
const record = toRecordObject(value);
if (!record) {
return null;
}
const truthGateStatus = toNonEmptyString(record.truth_gate_status);
const carryoverEligibility = toNonEmptyString(record.carryover_eligibility);
const limitedReasonCategory = toNonEmptyString(record.limited_reason_category);
const runtimeReadiness = toNonEmptyString(record.runtime_readiness);
if (!isTruthGateStatus(truthGateStatus) || !isCarryoverDepth(carryoverEligibility) || !isRuntimeReadiness(runtimeReadiness)) {
return null;
}
return {
schema_version: exports.ADDRESS_TRUTH_GATE_SCHEMA_VERSION,
policy_owner: "addressTruthGatePolicy",
truth_gate_status: truthGateStatus,
carryover_eligibility: carryoverEligibility,
limited_reason_category: isLimitedReasonCategory(limitedReasonCategory) ? limitedReasonCategory : null,
runtime_readiness: runtimeReadiness,
reason_codes: toStringList(record.reason_codes).slice(0, 32),
blocked_or_limited_explanation: toNonEmptyString(record.blocked_or_limited_explanation)
};
}
function buildAddressTruthGateDebugFields(input) {
return {
address_truth_gate_v1: resolveAddressTruthGate(input)
};
}
function attachAddressTruthGate(debugPayload, input) {
return {
...debugPayload,
...buildAddressTruthGateDebugFields(input)
};
}

View File

@ -4,6 +4,7 @@ exports.resolveAssistantRuntimeContractShadow = resolveAssistantRuntimeContractS
exports.buildAssistantRuntimeContractShadowFields = buildAssistantRuntimeContractShadowFields;
exports.attachAssistantRuntimeContractShadow = attachAssistantRuntimeContractShadow;
const assistantRuntimeContracts_1 = require("../types/assistantRuntimeContracts");
const addressTruthGatePolicy_1 = require("./addressTruthGatePolicy");
const assistantRuntimeContractRegistry_1 = require("./assistantRuntimeContractRegistry");
function toRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
@ -134,6 +135,9 @@ function resolveTransitionId(input) {
return { transitionId: null, reasons: ["transition_contract_not_resolved"] };
}
function resolveTruthGateStatus(input) {
if (input.explicitTruthGateStatus) {
return input.explicitTruthGateStatus;
}
if (input.groundingStatus === "route_mismatch_blocked" || input.debug.route_expectation_status === "mismatch") {
return "blocked_route_expectation_failure";
}
@ -157,10 +161,13 @@ function resolveTruthGateStatus(input) {
}
return "unknown";
}
function carryoverEligibilityFor(transitionId, truthGateStatus) {
function carryoverEligibilityFor(transitionId, truthGateStatus, explicitCarryoverEligibility) {
if (truthGateStatus.startsWith("blocked")) {
return "none";
}
if (explicitCarryoverEligibility) {
return explicitCarryoverEligibility;
}
if (transitionId === "T3" || transitionId === "T4" || transitionId === "T5") {
return "object_only";
}
@ -178,6 +185,7 @@ function carryoverEligibilityFor(transitionId, truthGateStatus) {
function resolveAssistantRuntimeContractShadow(input) {
const debug = toRecordObject(input.addressDebug) ?? {};
const meta = runtimeMetaFrom(input);
const addressTruthGate = (0, addressTruthGatePolicy_1.toAddressTruthGateContract)(debug.address_truth_gate_v1);
const groundingStatus = toNonEmptyString(input.groundingStatus) ??
toNonEmptyString(toRecordObject(debug.answer_grounding_check)?.status);
const capability = resolveCapabilityContractId(debug, meta);
@ -188,7 +196,11 @@ function resolveAssistantRuntimeContractShadow(input) {
groundingStatus
});
const transitionContract = transition.transitionId ? (0, assistantRuntimeContractRegistry_1.getAssistantTransitionContract)(transition.transitionId) : null;
const truthGateStatus = resolveTruthGateStatus({ debug, groundingStatus });
const truthGateStatus = resolveTruthGateStatus({
debug,
groundingStatus,
explicitTruthGateStatus: addressTruthGate?.truth_gate_status ?? null
});
return {
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_contract_id: transition.transitionId,
@ -197,7 +209,7 @@ function resolveAssistantRuntimeContractShadow(input) {
capability_contract_id: capability.capabilityId,
capability_contract_reason: capability.reasons,
truth_gate_contract_status: truthGateStatus,
carryover_eligibility: carryoverEligibilityFor(transition.transitionId, truthGateStatus)
carryover_eligibility: carryoverEligibilityFor(transition.transitionId, truthGateStatus, addressTruthGate?.carryover_eligibility ?? null)
};
}
function buildAssistantRuntimeContractShadowFields(input) {

View File

@ -4,6 +4,7 @@ exports.resolveAssistantTruthAnswerPolicyRuntime = resolveAssistantTruthAnswerPo
exports.buildAssistantTruthAnswerPolicyRuntimeFields = buildAssistantTruthAnswerPolicyRuntimeFields;
exports.attachAssistantTruthAnswerPolicy = attachAssistantTruthAnswerPolicy;
const assistantRuntimeContracts_1 = require("../types/assistantRuntimeContracts");
const addressTruthGatePolicy_1 = require("./addressTruthGatePolicy");
const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver");
function toRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
@ -164,6 +165,7 @@ function collectReasonCodes(input) {
const reasons = [];
pushReason(reasons, `truth_gate_${input.truthGateStatus}`);
pushReason(reasons, `truth_mode_${input.truthMode}`);
input.explicitGateReasonCodes.forEach((item) => pushReason(reasons, item));
input.shadow.transition_contract_reason.forEach((item) => pushReason(reasons, item));
input.shadow.capability_contract_reason.forEach((item) => pushReason(reasons, item));
toStringList(input.debug.missing_required_filters).forEach((item) => pushReason(reasons, `missing_filter_${item}`));
@ -232,12 +234,13 @@ function requiredSectionsFor(shape) {
}
function resolveAssistantTruthAnswerPolicyRuntime(input) {
const debug = toRecordObject(input.addressDebug) ?? {};
const explicitAddressTruthGate = (0, addressTruthGatePolicy_1.toAddressTruthGateContract)(debug.address_truth_gate_v1);
const shadow = (0, assistantRuntimeContractResolver_1.resolveAssistantRuntimeContractShadow)({
addressDebug: debug,
addressRuntimeMeta: input.addressRuntimeMeta,
groundingStatus: input.groundingStatus
});
const truthGateStatus = shadow.truth_gate_contract_status;
const truthGateStatus = explicitAddressTruthGate?.truth_gate_status ?? shadow.truth_gate_contract_status;
const groundingStatus = groundingStatusFrom(debug, input, truthGateStatus);
const coverageStatus = coverageStatusFrom(debug, input, truthGateStatus, groundingStatus);
const truthMode = truthModeFrom({
@ -253,14 +256,17 @@ function resolveAssistantTruthAnswerPolicyRuntime(input) {
coverageReport,
shadow,
truthMode,
truthGateStatus
truthGateStatus,
explicitGateReasonCodes: explicitAddressTruthGate?.reason_codes ?? []
});
const shape = answerShapeFrom({
coverageStatus,
truthMode,
truthGateStatus
});
const carryoverEligibility = coverageStatus === "blocked" || truthMode === "unsupported" ? "none" : shadow.carryover_eligibility;
const carryoverEligibility = coverageStatus === "blocked" || truthMode === "unsupported"
? "none"
: explicitAddressTruthGate?.carryover_eligibility ?? shadow.carryover_eligibility;
const truthGate = {
schema_version: assistantRuntimeContracts_1.ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
@ -271,7 +277,7 @@ function resolveAssistantTruthAnswerPolicyRuntime(input) {
carryover_eligibility: carryoverEligibility,
reason_codes: reasonCodes,
source_truth_gate_status: truthGateStatus,
blocked_or_limited_explanation: explanationFor(truthGateStatus, truthMode, coverageStatus)
blocked_or_limited_explanation: explicitAddressTruthGate?.blocked_or_limited_explanation ?? explanationFor(truthGateStatus, truthMode, coverageStatus)
};
const answerShape = {
schema_version: assistantRuntimeContracts_1.ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,

View File

@ -63,6 +63,7 @@ import {
normalizeOrganizationScopeValue,
resolveOrganizationSelectionFromMessage
} from "./assistantOrganizationMatcher";
import { attachAddressTruthGate } from "./addressTruthGatePolicy";
import { OpenAIResponsesClient, type OpenAIRequestConfig } from "./openaiResponsesClient";
import { readJsonFile } from "../utils/files";
@ -3157,20 +3158,9 @@ function buildLimitedExecutionResult(input: {
requestedResultMode: requestedResultMode,
resultMode: resultSemantics.result_mode
});
return {
handled: true,
reply_text: composeLimitedReply({
category: input.category,
reason: input.reasonText,
nextStep: input.nextStep,
shape: input.shape,
intent: input.intent.intent,
filters: input.filters,
missingRequiredFilters: input.missingRequiredFilters
}),
reply_type: "partial_coverage",
response_type: "LIMITED_WITH_REASON",
debug: {
const runtimeReadiness = runtimeReadinessForLimitedCategory(input.category);
const debugPayload = attachAddressTruthGate(
{
detected_mode: input.mode.mode,
detected_mode_confidence: input.mode.confidence,
query_shape: input.shape.shape,
@ -3204,10 +3194,10 @@ function buildLimitedExecutionResult(input: {
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
runtime_readiness: runtimeReadinessForLimitedCategory(input.category),
runtime_readiness: runtimeReadiness,
limited_reason_category: input.category,
semantic_frame: input.semanticFrame ?? null,
response_type: "LIMITED_WITH_REASON",
response_type: "LIMITED_WITH_REASON" as const,
capability_id: input.capabilityAudit?.capabilityId ?? null,
capability_layer: input.capabilityAudit?.layer ?? null,
capability_route_mode: input.capabilityAudit?.routeMode ?? null,
@ -3224,7 +3214,37 @@ function buildLimitedExecutionResult(input: {
...resultSemantics,
limitations: input.limitations,
reasons
},
{
intent: input.intent.intent,
filters: input.filters,
semanticFrame: input.semanticFrame ?? null,
selectedRecipe: input.selectedRecipe,
rowsMatched: input.rowsMatched,
limitedReasonCategory: input.category,
runtimeReadiness,
missingRequiredFilters: input.missingRequiredFilters,
limitations: input.limitations,
reasons,
routeExpectationStatus: routeExpectationAudit.status,
routeExpectationReason: routeExpectationAudit.reason,
replyType: "partial_coverage"
}
);
return {
handled: true,
reply_text: composeLimitedReply({
category: input.category,
reason: input.reasonText,
nextStep: input.nextStep,
shape: input.shape,
intent: input.intent.intent,
filters: input.filters,
missingRequiredFilters: input.missingRequiredFilters
}),
reply_type: "partial_coverage",
response_type: "LIMITED_WITH_REASON",
debug: debugPayload
};
}
@ -4088,12 +4108,21 @@ export class AddressQueryService {
}),
undefined
);
return {
handled: true,
reply_text: replyText,
reply_type: inferReplyType(responseType),
response_type: responseType,
debug: {
const factualNoRowsLimitations = [...filters.warnings, ...extraLimitations];
const factualNoRowsReasons = withConfirmedBalanceFallbackReason(
[...baseReasons, noRowsReason],
requestedResultMode,
undefined,
semantics.result_mode
);
const routeExpectationAudit = buildRouteExpectationAudit({
intent: routeExpectationIntent,
selectedRecipe: effectiveRecipeId,
requestedResultMode,
resultMode: semantics.result_mode
});
const debugPayload = attachAddressTruthGate(
{
detected_mode: mode.mode,
detected_mode_confidence: mode.confidence,
query_shape: shape.shape,
@ -4127,17 +4156,17 @@ export class AddressQueryService {
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS" as const,
limited_reason_category: null,
response_type: responseType,
route_expectation_status: routeExpectationAudit.status,
route_expectation_reason: routeExpectationAudit.reason,
route_expectation_expected_selected_recipes: routeExpectationAudit.expectedSelectedRecipes,
route_expectation_expected_requested_result_modes: routeExpectationAudit.expectedRequestedResultModes,
route_expectation_expected_result_modes: routeExpectationAudit.expectedResultModes,
...semantics,
limitations: [...filters.warnings, ...extraLimitations],
reasons: withConfirmedBalanceFallbackReason(
[...baseReasons, noRowsReason],
requestedResultMode,
undefined,
semantics.result_mode
),
limitations: factualNoRowsLimitations,
reasons: factualNoRowsReasons,
...(capabilityAudit
? {
capability_id: capabilityAudit.capabilityId,
@ -4149,10 +4178,33 @@ export class AddressQueryService {
: {}),
...(shadowRouteAudit
? {
shadow_route_intent: shadowRouteAudit.intent,
shadow_route_selected_recipe: shadowRouteAudit.selectedRecipe,
shadow_route_status: shadowRouteAudit.status
}
: {})
},
{
intent: intent.intent,
filters: filters.extracted_filters,
semanticFrame,
selectedRecipe: effectiveRecipeId,
rowsMatched: 0,
limitedReasonCategory: "empty_match",
runtimeReadiness: "LIVE_QUERYABLE_WITH_LIMITS",
limitations: factualNoRowsLimitations,
reasons: factualNoRowsReasons,
routeExpectationStatus: routeExpectationAudit.status,
routeExpectationReason: routeExpectationAudit.reason,
replyType: inferReplyType(responseType)
}
);
return {
handled: true,
reply_text: replyText,
reply_type: inferReplyType(responseType),
response_type: responseType,
debug: debugPayload
};
};
if (organizationWarehouseRecoveryApplied) {

View File

@ -0,0 +1,321 @@
import type {
AssistantCarryoverDepth,
AssistantTruthGateContractStatus
} from "../types/assistantRuntimeContracts";
import type {
AddressFilterSet,
AddressIntent,
AddressLimitedReasonCategory,
AddressRuntimeReadiness,
AddressSemanticFrame
} from "../types/addressQuery";
export const ADDRESS_TRUTH_GATE_SCHEMA_VERSION = "address_truth_gate_v1" as const;
export interface AddressTruthGateContract {
schema_version: typeof ADDRESS_TRUTH_GATE_SCHEMA_VERSION;
policy_owner: "addressTruthGatePolicy";
truth_gate_status: AssistantTruthGateContractStatus;
carryover_eligibility: AssistantCarryoverDepth;
limited_reason_category: AddressLimitedReasonCategory | null;
runtime_readiness: AddressRuntimeReadiness;
reason_codes: string[];
blocked_or_limited_explanation: string | null;
}
export interface ResolveAddressTruthGateInput {
intent: AddressIntent;
filters?: AddressFilterSet | null;
semanticFrame?: AddressSemanticFrame | null;
selectedRecipe?: string | null;
rowsMatched?: number;
limitedReasonCategory?: AddressLimitedReasonCategory | null;
runtimeReadiness?: AddressRuntimeReadiness | null;
missingRequiredFilters?: string[];
limitations?: string[];
reasons?: string[];
routeExpectationStatus?: "matched" | "mismatch" | "not_found" | null;
routeExpectationReason?: string | null;
temporalGuardOutcome?: string | null;
temporalAlignmentStatus?: string | null;
replyType?: "factual" | "partial_coverage" | "deep_analysis" | "unknown" | null;
}
function toRecordObject(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function toStringList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item));
}
function isTruthGateStatus(value: string | null): value is AssistantTruthGateContractStatus {
return (
value === "full_confirmed" ||
value === "partial_supported" ||
value === "blocked_missing_anchor" ||
value === "blocked_route_expectation_failure" ||
value === "blocked_execution_error" ||
value === "limited_temporal_or_contextual" ||
value === "unknown"
);
}
function isCarryoverDepth(value: string | null): value is AssistantCarryoverDepth {
return value === "full" || value === "root_only" || value === "object_only" || value === "meta_only" || value === "none";
}
function isLimitedReasonCategory(value: string | null): value is AddressLimitedReasonCategory {
return (
value === "empty_match" ||
value === "missing_anchor" ||
value === "recipe_visibility_gap" ||
value === "execution_error" ||
value === "unsupported"
);
}
function isRuntimeReadiness(value: string | null): value is AddressRuntimeReadiness {
return (
value === "LIVE_QUERYABLE" ||
value === "LIVE_QUERYABLE_WITH_LIMITS" ||
value === "REQUIRES_SPECIALIZED_RECIPE" ||
value === "DEEP_ONLY" ||
value === "UNKNOWN"
);
}
function normalizeReasonCode(value: string): string | null {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target: string[], value: unknown): void {
const text = toNonEmptyString(value);
if (!text) {
return;
}
const normalized = normalizeReasonCode(text);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function hasFilterScope(filters: AddressFilterSet | null | undefined): boolean {
if (!filters) {
return false;
}
const scopeKeys: Array<keyof AddressFilterSet> = [
"period_from",
"period_to",
"as_of_date",
"organization",
"counterparty",
"contract",
"account",
"item",
"warehouse",
"document_type",
"document_ref",
"status"
];
return scopeKeys.some((key) => toNonEmptyString(filters[key]) !== null);
}
function hasObjectFocus(input: ResolveAddressTruthGateInput): boolean {
if (toNonEmptyString(input.filters?.item)) {
return true;
}
if (input.semanticFrame?.selected_object_scope_detected) {
return true;
}
if (input.semanticFrame?.anchor_kind === "selected_object" || input.semanticFrame?.anchor_kind === "item") {
return true;
}
return (
input.intent === "inventory_purchase_provenance_for_item" ||
input.intent === "inventory_purchase_documents_for_item" ||
input.intent === "inventory_sale_trace_for_item" ||
input.intent === "inventory_profitability_for_item" ||
input.intent === "inventory_purchase_to_sale_chain" ||
input.intent === "inventory_aging_by_purchase_date"
);
}
function hasReusableRootScope(input: ResolveAddressTruthGateInput): boolean {
if (hasFilterScope(input.filters)) {
return true;
}
return Boolean(input.semanticFrame && input.semanticFrame.scope_kind !== "none");
}
function truthGateStatusFrom(input: ResolveAddressTruthGateInput): AssistantTruthGateContractStatus {
const missingRequiredFilters = input.missingRequiredFilters ?? [];
if (input.routeExpectationStatus === "mismatch") {
return "blocked_route_expectation_failure";
}
if (input.limitedReasonCategory === "execution_error") {
return "blocked_execution_error";
}
if (missingRequiredFilters.length > 0 || input.limitedReasonCategory === "missing_anchor") {
return "blocked_missing_anchor";
}
if (input.temporalGuardOutcome === "ambiguous_limited" || input.temporalAlignmentStatus === "conflicting") {
return "limited_temporal_or_contextual";
}
if (input.replyType === "factual" && input.limitedReasonCategory === "empty_match") {
return "full_confirmed";
}
if (
input.limitedReasonCategory === "empty_match" ||
input.limitedReasonCategory === "recipe_visibility_gap" ||
input.limitedReasonCategory === "unsupported" ||
input.replyType === "partial_coverage"
) {
return "partial_supported";
}
if ((input.rowsMatched ?? 0) > 0) {
return "full_confirmed";
}
return "unknown";
}
function carryoverEligibilityFor(
input: ResolveAddressTruthGateInput,
truthGateStatus: AssistantTruthGateContractStatus
): AssistantCarryoverDepth {
if (truthGateStatus.startsWith("blocked")) {
return "none";
}
if (input.limitedReasonCategory === "recipe_visibility_gap" || input.limitedReasonCategory === "unsupported") {
return "meta_only";
}
if (hasObjectFocus(input)) {
return "object_only";
}
if (truthGateStatus === "limited_temporal_or_contextual") {
return hasReusableRootScope(input) ? "root_only" : "meta_only";
}
if (truthGateStatus === "full_confirmed" || truthGateStatus === "partial_supported") {
return hasReusableRootScope(input) ? "root_only" : "meta_only";
}
return "none";
}
function explanationFor(
truthGateStatus: AssistantTruthGateContractStatus,
limitedReasonCategory: AddressLimitedReasonCategory | null
): string | null {
if (truthGateStatus === "full_confirmed") {
return null;
}
if (truthGateStatus === "blocked_missing_anchor") {
return "required_anchor_missing";
}
if (truthGateStatus === "blocked_route_expectation_failure") {
return "route_expectation_failed";
}
if (truthGateStatus === "blocked_execution_error") {
return "execution_failed";
}
if (truthGateStatus === "limited_temporal_or_contextual") {
return "temporal_or_contextual_limit";
}
if (limitedReasonCategory === "recipe_visibility_gap") {
return "specialized_recipe_required";
}
if (limitedReasonCategory === "unsupported") {
return "unsupported_in_current_contour";
}
return truthGateStatus === "partial_supported" ? "evidence_or_coverage_is_partial" : "truth_gate_not_confirmed";
}
function collectReasonCodes(
input: ResolveAddressTruthGateInput,
truthGateStatus: AssistantTruthGateContractStatus
): string[] {
const reasons: string[] = [];
pushReason(reasons, `address_truth_gate_${truthGateStatus}`);
pushReason(reasons, input.routeExpectationReason);
pushReason(reasons, input.limitedReasonCategory ? `limited_category_${input.limitedReasonCategory}` : null);
(input.missingRequiredFilters ?? []).forEach((item) => pushReason(reasons, `missing_filter_${item}`));
(input.limitations ?? []).forEach((item) => pushReason(reasons, item));
(input.reasons ?? []).forEach((item) => pushReason(reasons, item));
return reasons.slice(0, 32);
}
export function resolveAddressTruthGate(input: ResolveAddressTruthGateInput): AddressTruthGateContract {
const truthGateStatus = truthGateStatusFrom(input);
return {
schema_version: ADDRESS_TRUTH_GATE_SCHEMA_VERSION,
policy_owner: "addressTruthGatePolicy",
truth_gate_status: truthGateStatus,
carryover_eligibility: carryoverEligibilityFor(input, truthGateStatus),
limited_reason_category: input.limitedReasonCategory ?? null,
runtime_readiness: input.runtimeReadiness ?? "UNKNOWN",
reason_codes: collectReasonCodes(input, truthGateStatus),
blocked_or_limited_explanation: explanationFor(truthGateStatus, input.limitedReasonCategory ?? null)
};
}
export function toAddressTruthGateContract(value: unknown): AddressTruthGateContract | null {
const record = toRecordObject(value);
if (!record) {
return null;
}
const truthGateStatus = toNonEmptyString(record.truth_gate_status);
const carryoverEligibility = toNonEmptyString(record.carryover_eligibility);
const limitedReasonCategory = toNonEmptyString(record.limited_reason_category);
const runtimeReadiness = toNonEmptyString(record.runtime_readiness);
if (!isTruthGateStatus(truthGateStatus) || !isCarryoverDepth(carryoverEligibility) || !isRuntimeReadiness(runtimeReadiness)) {
return null;
}
return {
schema_version: ADDRESS_TRUTH_GATE_SCHEMA_VERSION,
policy_owner: "addressTruthGatePolicy",
truth_gate_status: truthGateStatus,
carryover_eligibility: carryoverEligibility,
limited_reason_category: isLimitedReasonCategory(limitedReasonCategory) ? limitedReasonCategory : null,
runtime_readiness: runtimeReadiness,
reason_codes: toStringList(record.reason_codes).slice(0, 32),
blocked_or_limited_explanation: toNonEmptyString(record.blocked_or_limited_explanation)
};
}
export function buildAddressTruthGateDebugFields(input: ResolveAddressTruthGateInput): {
address_truth_gate_v1: AddressTruthGateContract;
} {
return {
address_truth_gate_v1: resolveAddressTruthGate(input)
};
}
export function attachAddressTruthGate<T extends Record<string, unknown>>(
debugPayload: T,
input: ResolveAddressTruthGateInput
): T & { address_truth_gate_v1: AddressTruthGateContract } {
return {
...debugPayload,
...buildAddressTruthGateDebugFields(input)
};
}

View File

@ -5,6 +5,7 @@ import {
type AssistantTransitionClassId
} from "../types/assistantRuntimeContracts";
import type { AddressIntent } from "../types/addressQuery";
import { toAddressTruthGateContract } from "./addressTruthGatePolicy";
import {
getAssistantCapabilityContract,
getAssistantCapabilityContractByIntent,
@ -190,7 +191,11 @@ function resolveTransitionId(input: {
function resolveTruthGateStatus(input: {
debug: Record<string, unknown>;
groundingStatus: string | null;
explicitTruthGateStatus?: AssistantRuntimeContractShadowDecision["truth_gate_contract_status"] | null;
}): AssistantRuntimeContractShadowDecision["truth_gate_contract_status"] {
if (input.explicitTruthGateStatus) {
return input.explicitTruthGateStatus;
}
if (input.groundingStatus === "route_mismatch_blocked" || input.debug.route_expectation_status === "mismatch") {
return "blocked_route_expectation_failure";
}
@ -219,11 +224,15 @@ function resolveTruthGateStatus(input: {
function carryoverEligibilityFor(
transitionId: AssistantTransitionClassId | null,
truthGateStatus: AssistantRuntimeContractShadowDecision["truth_gate_contract_status"]
truthGateStatus: AssistantRuntimeContractShadowDecision["truth_gate_contract_status"],
explicitCarryoverEligibility?: AssistantCarryoverDepth | null
): AssistantCarryoverDepth {
if (truthGateStatus.startsWith("blocked")) {
return "none";
}
if (explicitCarryoverEligibility) {
return explicitCarryoverEligibility;
}
if (transitionId === "T3" || transitionId === "T4" || transitionId === "T5") {
return "object_only";
}
@ -244,6 +253,7 @@ export function resolveAssistantRuntimeContractShadow(
): AssistantRuntimeContractShadowDecision {
const debug = toRecordObject(input.addressDebug) ?? {};
const meta = runtimeMetaFrom(input);
const addressTruthGate = toAddressTruthGateContract(debug.address_truth_gate_v1);
const groundingStatus =
toNonEmptyString(input.groundingStatus) ??
toNonEmptyString(toRecordObject(debug.answer_grounding_check)?.status);
@ -255,7 +265,11 @@ export function resolveAssistantRuntimeContractShadow(
groundingStatus
});
const transitionContract = transition.transitionId ? getAssistantTransitionContract(transition.transitionId) : null;
const truthGateStatus = resolveTruthGateStatus({ debug, groundingStatus });
const truthGateStatus = resolveTruthGateStatus({
debug,
groundingStatus,
explicitTruthGateStatus: addressTruthGate?.truth_gate_status ?? null
});
return {
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_contract_id: transition.transitionId,
@ -264,7 +278,11 @@ export function resolveAssistantRuntimeContractShadow(
capability_contract_id: capability.capabilityId,
capability_contract_reason: capability.reasons,
truth_gate_contract_status: truthGateStatus,
carryover_eligibility: carryoverEligibilityFor(transition.transitionId, truthGateStatus)
carryover_eligibility: carryoverEligibilityFor(
transition.transitionId,
truthGateStatus,
addressTruthGate?.carryover_eligibility ?? null
)
};
}

View File

@ -11,6 +11,7 @@ import {
type AssistantTruthGateContractStatus,
type AssistantTruthMode
} from "../types/assistantRuntimeContracts";
import { toAddressTruthGateContract } from "./addressTruthGatePolicy";
import { resolveAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
export interface ResolveAssistantTruthAnswerPolicyRuntimeInput {
@ -228,10 +229,12 @@ function collectReasonCodes(input: {
shadow: AssistantRuntimeContractShadowDecision;
truthMode: AssistantTruthMode;
truthGateStatus: AssistantTruthGateContractStatus;
explicitGateReasonCodes: string[];
}): string[] {
const reasons: string[] = [];
pushReason(reasons, `truth_gate_${input.truthGateStatus}`);
pushReason(reasons, `truth_mode_${input.truthMode}`);
input.explicitGateReasonCodes.forEach((item) => pushReason(reasons, item));
input.shadow.transition_contract_reason.forEach((item) => pushReason(reasons, item));
input.shadow.capability_contract_reason.forEach((item) => pushReason(reasons, item));
toStringList(input.debug.missing_required_filters).forEach((item) => pushReason(reasons, `missing_filter_${item}`));
@ -314,12 +317,13 @@ export function resolveAssistantTruthAnswerPolicyRuntime(
input: ResolveAssistantTruthAnswerPolicyRuntimeInput
): AssistantTruthAnswerPolicyRuntimeContract {
const debug = toRecordObject(input.addressDebug) ?? {};
const explicitAddressTruthGate = toAddressTruthGateContract(debug.address_truth_gate_v1);
const shadow = resolveAssistantRuntimeContractShadow({
addressDebug: debug,
addressRuntimeMeta: input.addressRuntimeMeta,
groundingStatus: input.groundingStatus
});
const truthGateStatus = shadow.truth_gate_contract_status;
const truthGateStatus = explicitAddressTruthGate?.truth_gate_status ?? shadow.truth_gate_contract_status;
const groundingStatus = groundingStatusFrom(debug, input, truthGateStatus);
const coverageStatus = coverageStatusFrom(debug, input, truthGateStatus, groundingStatus);
const truthMode = truthModeFrom({
@ -335,7 +339,8 @@ export function resolveAssistantTruthAnswerPolicyRuntime(
coverageReport,
shadow,
truthMode,
truthGateStatus
truthGateStatus,
explicitGateReasonCodes: explicitAddressTruthGate?.reason_codes ?? []
});
const shape = answerShapeFrom({
coverageStatus,
@ -343,7 +348,9 @@ export function resolveAssistantTruthAnswerPolicyRuntime(
truthGateStatus
});
const carryoverEligibility =
coverageStatus === "blocked" || truthMode === "unsupported" ? "none" : shadow.carryover_eligibility;
coverageStatus === "blocked" || truthMode === "unsupported"
? "none"
: explicitAddressTruthGate?.carryover_eligibility ?? shadow.carryover_eligibility;
const truthGate = {
schema_version: ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
@ -355,7 +362,8 @@ export function resolveAssistantTruthAnswerPolicyRuntime(
carryover_eligibility: carryoverEligibility,
reason_codes: reasonCodes,
source_truth_gate_status: truthGateStatus,
blocked_or_limited_explanation: explanationFor(truthGateStatus, truthMode, coverageStatus)
blocked_or_limited_explanation:
explicitAddressTruthGate?.blocked_or_limited_explanation ?? explanationFor(truthGateStatus, truthMode, coverageStatus)
} satisfies AssistantTruthAnswerPolicyRuntimeContract["truth_gate"];
const answerShape = {

View File

@ -1,5 +1,10 @@
export type AddressQuestionMode = "address_query" | "deep_analysis" | "unsupported";
import type {
AssistantCarryoverDepth,
AssistantTruthGateContractStatus
} from "./assistantRuntimeContracts";
export type AddressIntent =
| "period_coverage_profile"
| "document_type_and_account_section_profile"
@ -261,6 +266,16 @@ export interface AddressExecutionDebug {
| "rows_remaining_after_scope_filter";
runtime_readiness: AddressRuntimeReadiness;
limited_reason_category: AddressLimitedReasonCategory | null;
address_truth_gate_v1?: {
schema_version: "address_truth_gate_v1";
policy_owner: "addressTruthGatePolicy";
truth_gate_status: AssistantTruthGateContractStatus;
carryover_eligibility: AssistantCarryoverDepth;
limited_reason_category: AddressLimitedReasonCategory | null;
runtime_readiness: AddressRuntimeReadiness;
reason_codes: string[];
blocked_or_limited_explanation: string | null;
} | null;
organization_candidates?: string[];
semantic_frame?: AddressSemanticFrame | null;
response_type: AddressResponseType;

View File

@ -0,0 +1,75 @@
import { describe, expect, it } from "vitest";
import { resolveAddressTruthGate } from "../src/services/addressTruthGatePolicy";
describe("address truth gate policy", () => {
it("treats factual exact no-match as confirmed negative with reusable root scope", () => {
const gate = resolveAddressTruthGate({
intent: "open_items_by_counterparty_or_contract",
filters: {
account: "60",
period_from: "2022-08-01",
period_to: "2022-08-31"
},
selectedRecipe: "open_items_by_counterparty_or_contract",
rowsMatched: 0,
limitedReasonCategory: "empty_match",
runtimeReadiness: "LIVE_QUERYABLE_WITH_LIMITS",
reasons: ["open_items_account_exact_negative_response"],
routeExpectationStatus: "matched",
replyType: "factual"
});
expect(gate.truth_gate_status).toBe("full_confirmed");
expect(gate.carryover_eligibility).toBe("root_only");
expect(gate.blocked_or_limited_explanation).toBeNull();
expect(gate.reason_codes).toContain("limited_category_empty_match");
});
it("keeps selected-item limited answers object-scoped", () => {
const gate = resolveAddressTruthGate({
intent: "inventory_sale_trace_for_item",
filters: {
item: "Рабочая станция"
},
semanticFrame: {
scope_kind: "selected_object_scope",
anchor_kind: "selected_object",
anchor_value: "Рабочая станция",
date_scope_kind: "explicit",
date_basis_hint: "explicit_as_of_date",
self_scope_detected: false,
selected_object_scope_detected: true
},
selectedRecipe: "inventory_sale_trace_for_item",
rowsMatched: 0,
limitedReasonCategory: "empty_match",
runtimeReadiness: "LIVE_QUERYABLE_WITH_LIMITS",
limitations: ["no_rows_after_recipe_and_scope_filter"],
routeExpectationStatus: "matched",
replyType: "partial_coverage"
});
expect(gate.truth_gate_status).toBe("partial_supported");
expect(gate.carryover_eligibility).toBe("object_only");
expect(gate.blocked_or_limited_explanation).toBe("evidence_or_coverage_is_partial");
});
it("blocks missing-anchor clarifications instead of pretending they are reusable evidence", () => {
const gate = resolveAddressTruthGate({
intent: "inventory_purchase_provenance_for_item",
selectedRecipe: "inventory_purchase_provenance_for_item",
rowsMatched: 0,
limitedReasonCategory: "missing_anchor",
runtimeReadiness: "LIVE_QUERYABLE_WITH_LIMITS",
missingRequiredFilters: ["item"],
limitations: ["missing_required_filters"],
routeExpectationStatus: "matched",
replyType: "partial_coverage"
});
expect(gate.truth_gate_status).toBe("blocked_missing_anchor");
expect(gate.carryover_eligibility).toBe("none");
expect(gate.blocked_or_limited_explanation).toBe("required_anchor_missing");
expect(gate.reason_codes).toContain("missing_filter_item");
});
});

View File

@ -143,4 +143,27 @@ describe("assistant runtime contract registry", () => {
expect(decision.truth_gate_contract_status).toBe("limited_temporal_or_contextual");
});
it("reuses explicit exact-lane truth gate contracts for factual no-match carryover", () => {
const decision = resolveAssistantRuntimeContractShadow({
addressDebug: {
detected_intent: "open_items_by_counterparty_or_contract",
selected_recipe: "open_items_by_counterparty_or_contract",
rows_matched: 0,
address_truth_gate_v1: {
schema_version: "address_truth_gate_v1",
policy_owner: "addressTruthGatePolicy",
truth_gate_status: "full_confirmed",
carryover_eligibility: "root_only",
limited_reason_category: "empty_match",
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
reason_codes: ["open_items_account_exact_negative_response"],
blocked_or_limited_explanation: null
}
}
});
expect(decision.truth_gate_contract_status).toBe("full_confirmed");
expect(decision.carryover_eligibility).toBe("root_only");
});
});

View File

@ -89,6 +89,34 @@ describe("assistant truth answer policy runtime adapter", () => {
expect(policy.answer_shape.may_power_followup).toBe(false);
});
it("honors explicit exact-lane truth gate contracts for factual negative answers", () => {
const policy = resolveAssistantTruthAnswerPolicyRuntime({
addressDebug: {
capability_id: "inventory_counterparty_item_flow",
rows_matched: 0,
address_truth_gate_v1: {
schema_version: "address_truth_gate_v1",
policy_owner: "addressTruthGatePolicy",
truth_gate_status: "full_confirmed",
carryover_eligibility: "root_only",
limited_reason_category: "empty_match",
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
reason_codes: ["counterparty_item_flow_exact_negative_response"],
blocked_or_limited_explanation: null
}
},
replyType: "factual"
});
expect(policy.truth_gate.coverage_status).toBe("full");
expect(policy.truth_gate.grounding_status).toBe("grounded");
expect(policy.truth_gate.truth_mode).toBe("confirmed");
expect(policy.truth_gate.carryover_eligibility).toBe("root_only");
expect(policy.truth_gate.reason_codes).toContain("counterparty_item_flow_exact_negative_response");
expect(policy.answer_shape.answer_shape).toBe("confirmed_factual");
expect(policy.answer_shape.may_power_followup).toBe(true);
});
it("attaches top-level debug fields without hiding the nested contract", () => {
const debug = attachAssistantTruthAnswerPolicy(
{