АРЧ АП11 - Вынести exact-lane truth gate для factual-negative и limited ответов в отдельный policy-owner
This commit is contained in:
@@ -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)
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user