326 lines
11 KiB
TypeScript
326 lines
11 KiB
TypeScript
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;
|
|
truthGateStatusHint?: AssistantTruthGateContractStatus | 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 {
|
|
if (input.truthGateStatusHint) {
|
|
return input.truthGateStatusHint;
|
|
}
|
|
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)
|
|
};
|
|
}
|