АРЧ АП11 - Вынести exact-lane truth gate для factual-negative и limited ответов в отдельный policy-owner
This commit is contained in:
parent
6b14946f7e
commit
dc8dfcf237
|
|
@ -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,6 +2510,76 @@ function buildLimitedExecutionResult(input) {
|
|||
requestedResultMode: requestedResultMode,
|
||||
resultMode: resultSemantics.result_mode
|
||||
});
|
||||
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,
|
||||
query_shape_confidence: input.shape.confidence,
|
||||
detected_intent: input.intent.intent,
|
||||
detected_intent_confidence: input.intent.confidence,
|
||||
extracted_filters: input.filters,
|
||||
missing_required_filters: input.missingRequiredFilters,
|
||||
selected_recipe: input.selectedRecipe,
|
||||
mcp_call_status_legacy: toLegacyMcpStatus(input.mcpCallStatus),
|
||||
account_scope_mode: input.accountScopeMode ?? "strict",
|
||||
account_scope_fallback_applied: input.accountScopeFallbackApplied ?? false,
|
||||
anchor_type: input.anchor?.anchor_type ?? null,
|
||||
anchor_value_raw: input.anchor?.anchor_value_raw ?? null,
|
||||
anchor_value_resolved: input.anchor?.anchor_value_resolved ?? null,
|
||||
resolver_confidence: input.anchor?.resolver_confidence ?? null,
|
||||
ambiguity_count: input.anchor?.ambiguity_count ?? 0,
|
||||
match_failure_stage: input.matchFailureStage ?? "none",
|
||||
match_failure_reason: input.matchFailureReason ?? null,
|
||||
mcp_call_status: input.mcpCallStatus,
|
||||
rows_fetched: input.rowsFetched,
|
||||
raw_rows_received: input.rawRowsReceived ?? input.rowsFetched,
|
||||
rows_after_account_scope: input.rowsAfterAccountScope ?? 0,
|
||||
rows_after_recipe_filter: input.rowsAfterRecipeFilter ?? 0,
|
||||
rows_materialized: input.rowsMaterialized ?? 0,
|
||||
rows_matched: input.rowsMatched,
|
||||
raw_row_keys_sample: input.rawRowKeysSample ?? [],
|
||||
materialization_drop_reason: input.materializationDropReason ?? "none",
|
||||
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||||
account_token_normalized: accountScopeAudit.accountTokenNormalized,
|
||||
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
|
||||
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
|
||||
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
|
||||
runtime_readiness: runtimeReadiness,
|
||||
limited_reason_category: input.category,
|
||||
semantic_frame: input.semanticFrame ?? null,
|
||||
response_type: "LIMITED_WITH_REASON",
|
||||
capability_id: input.capabilityAudit?.capabilityId ?? null,
|
||||
capability_layer: input.capabilityAudit?.layer ?? null,
|
||||
capability_route_mode: input.capabilityAudit?.routeMode ?? null,
|
||||
capability_route_enabled: input.capabilityAudit?.enabled ?? true,
|
||||
capability_route_reason: input.capabilityAudit?.reason ?? null,
|
||||
shadow_route_intent: input.shadowRouteAudit?.intent ?? null,
|
||||
shadow_route_selected_recipe: input.shadowRouteAudit?.selectedRecipe ?? null,
|
||||
shadow_route_status: input.shadowRouteAudit?.status ?? "skipped",
|
||||
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,
|
||||
...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({
|
||||
|
|
@ -2522,61 +2593,7 @@ function buildLimitedExecutionResult(input) {
|
|||
}),
|
||||
reply_type: "partial_coverage",
|
||||
response_type: "LIMITED_WITH_REASON",
|
||||
debug: {
|
||||
detected_mode: input.mode.mode,
|
||||
detected_mode_confidence: input.mode.confidence,
|
||||
query_shape: input.shape.shape,
|
||||
query_shape_confidence: input.shape.confidence,
|
||||
detected_intent: input.intent.intent,
|
||||
detected_intent_confidence: input.intent.confidence,
|
||||
extracted_filters: input.filters,
|
||||
missing_required_filters: input.missingRequiredFilters,
|
||||
selected_recipe: input.selectedRecipe,
|
||||
mcp_call_status_legacy: toLegacyMcpStatus(input.mcpCallStatus),
|
||||
account_scope_mode: input.accountScopeMode ?? "strict",
|
||||
account_scope_fallback_applied: input.accountScopeFallbackApplied ?? false,
|
||||
anchor_type: input.anchor?.anchor_type ?? null,
|
||||
anchor_value_raw: input.anchor?.anchor_value_raw ?? null,
|
||||
anchor_value_resolved: input.anchor?.anchor_value_resolved ?? null,
|
||||
resolver_confidence: input.anchor?.resolver_confidence ?? null,
|
||||
ambiguity_count: input.anchor?.ambiguity_count ?? 0,
|
||||
match_failure_stage: input.matchFailureStage ?? "none",
|
||||
match_failure_reason: input.matchFailureReason ?? null,
|
||||
mcp_call_status: input.mcpCallStatus,
|
||||
rows_fetched: input.rowsFetched,
|
||||
raw_rows_received: input.rawRowsReceived ?? input.rowsFetched,
|
||||
rows_after_account_scope: input.rowsAfterAccountScope ?? 0,
|
||||
rows_after_recipe_filter: input.rowsAfterRecipeFilter ?? 0,
|
||||
rows_materialized: input.rowsMaterialized ?? 0,
|
||||
rows_matched: input.rowsMatched,
|
||||
raw_row_keys_sample: input.rawRowKeysSample ?? [],
|
||||
materialization_drop_reason: input.materializationDropReason ?? "none",
|
||||
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||||
account_token_normalized: accountScopeAudit.accountTokenNormalized,
|
||||
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
|
||||
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
|
||||
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
|
||||
runtime_readiness: runtimeReadinessForLimitedCategory(input.category),
|
||||
limited_reason_category: input.category,
|
||||
semantic_frame: input.semanticFrame ?? null,
|
||||
response_type: "LIMITED_WITH_REASON",
|
||||
capability_id: input.capabilityAudit?.capabilityId ?? null,
|
||||
capability_layer: input.capabilityAudit?.layer ?? null,
|
||||
capability_route_mode: input.capabilityAudit?.routeMode ?? null,
|
||||
capability_route_enabled: input.capabilityAudit?.enabled ?? true,
|
||||
capability_route_reason: input.capabilityAudit?.reason ?? null,
|
||||
shadow_route_intent: input.shadowRouteAudit?.intent ?? null,
|
||||
shadow_route_selected_recipe: input.shadowRouteAudit?.selectedRecipe ?? null,
|
||||
shadow_route_status: input.shadowRouteAudit?.status ?? "skipped",
|
||||
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,
|
||||
...resultSemantics,
|
||||
limitations: input.limitations,
|
||||
reasons
|
||||
}
|
||||
debug: debugPayload
|
||||
};
|
||||
}
|
||||
function composeOrganizationClarificationReply(organizations) {
|
||||
|
|
@ -3322,66 +3339,95 @@ class AddressQueryService {
|
|||
responseType,
|
||||
rowsMatched: 0
|
||||
}), undefined);
|
||||
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,
|
||||
query_shape_confidence: shape.confidence,
|
||||
detected_intent: intent.intent,
|
||||
detected_intent_confidence: intent.confidence,
|
||||
extracted_filters: filters.extracted_filters,
|
||||
missing_required_filters: [],
|
||||
selected_recipe: effectiveRecipeId,
|
||||
mcp_call_status_legacy: toLegacyMcpStatus(stageStatus),
|
||||
account_scope_mode: plan.account_scope_mode,
|
||||
account_scope_fallback_applied: accountScopeFallbackApplied,
|
||||
anchor_type: anchor.anchor_type,
|
||||
anchor_value_raw: anchor.anchor_value_raw,
|
||||
anchor_value_resolved: anchor.anchor_value_resolved,
|
||||
resolver_confidence: anchor.resolver_confidence,
|
||||
ambiguity_count: anchor.ambiguity_count,
|
||||
match_failure_stage: matchFailureStage,
|
||||
match_failure_reason: matchFailureReason,
|
||||
mcp_call_status: stageStatus,
|
||||
rows_fetched: mcp.fetched_rows,
|
||||
raw_rows_received: mcp.raw_rows.length,
|
||||
rows_after_account_scope: normalizedRows.length,
|
||||
rows_after_recipe_filter: filterByAnchors.length,
|
||||
rows_materialized: normalizedRows.length,
|
||||
rows_matched: 0,
|
||||
raw_row_keys_sample: rowDiagnostics.rawRowKeysSample,
|
||||
materialization_drop_reason: rowDiagnostics.materializationDropReason,
|
||||
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||||
account_token_normalized: accountScopeAudit.accountTokenNormalized,
|
||||
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
|
||||
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
|
||||
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
|
||||
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: factualNoRowsLimitations,
|
||||
reasons: factualNoRowsReasons,
|
||||
...(capabilityAudit
|
||||
? {
|
||||
capability_id: capabilityAudit.capabilityId,
|
||||
capability_layer: capabilityAudit.layer,
|
||||
capability_route_mode: capabilityAudit.routeMode,
|
||||
capability_route_enabled: capabilityAudit.enabled,
|
||||
capability_route_reason: capabilityAudit.reason
|
||||
}
|
||||
: {}),
|
||||
...(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: {
|
||||
detected_mode: mode.mode,
|
||||
detected_mode_confidence: mode.confidence,
|
||||
query_shape: shape.shape,
|
||||
query_shape_confidence: shape.confidence,
|
||||
detected_intent: intent.intent,
|
||||
detected_intent_confidence: intent.confidence,
|
||||
extracted_filters: filters.extracted_filters,
|
||||
missing_required_filters: [],
|
||||
selected_recipe: effectiveRecipeId,
|
||||
mcp_call_status_legacy: toLegacyMcpStatus(stageStatus),
|
||||
account_scope_mode: plan.account_scope_mode,
|
||||
account_scope_fallback_applied: accountScopeFallbackApplied,
|
||||
anchor_type: anchor.anchor_type,
|
||||
anchor_value_raw: anchor.anchor_value_raw,
|
||||
anchor_value_resolved: anchor.anchor_value_resolved,
|
||||
resolver_confidence: anchor.resolver_confidence,
|
||||
ambiguity_count: anchor.ambiguity_count,
|
||||
match_failure_stage: matchFailureStage,
|
||||
match_failure_reason: matchFailureReason,
|
||||
mcp_call_status: stageStatus,
|
||||
rows_fetched: mcp.fetched_rows,
|
||||
raw_rows_received: mcp.raw_rows.length,
|
||||
rows_after_account_scope: normalizedRows.length,
|
||||
rows_after_recipe_filter: filterByAnchors.length,
|
||||
rows_materialized: normalizedRows.length,
|
||||
rows_matched: 0,
|
||||
raw_row_keys_sample: rowDiagnostics.rawRowKeysSample,
|
||||
materialization_drop_reason: rowDiagnostics.materializationDropReason,
|
||||
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||||
account_token_normalized: accountScopeAudit.accountTokenNormalized,
|
||||
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
|
||||
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
|
||||
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
|
||||
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||||
limited_reason_category: null,
|
||||
response_type: responseType,
|
||||
...semantics,
|
||||
limitations: [...filters.warnings, ...extraLimitations],
|
||||
reasons: withConfirmedBalanceFallbackReason([...baseReasons, noRowsReason], requestedResultMode, undefined, semantics.result_mode),
|
||||
...(capabilityAudit
|
||||
? {
|
||||
capability_id: capabilityAudit.capabilityId,
|
||||
capability_layer: capabilityAudit.layer,
|
||||
capability_route_mode: capabilityAudit.routeMode,
|
||||
capability_route_enabled: capabilityAudit.enabled,
|
||||
capability_route_reason: capabilityAudit.reason
|
||||
}
|
||||
: {}),
|
||||
...(shadowRouteAudit
|
||||
? {
|
||||
shadow_route_status: shadowRouteAudit.status
|
||||
}
|
||||
: {})
|
||||
}
|
||||
debug: debugPayload
|
||||
};
|
||||
};
|
||||
if (organizationWarehouseRecoveryApplied) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue