399 lines
13 KiB
TypeScript
399 lines
13 KiB
TypeScript
import type { AssistantCoverageStatus } from "../types/assistantRuntimeContracts";
|
|
import type {
|
|
AddressAsOfDateBasis,
|
|
AddressCoverageEvidenceBasis,
|
|
AddressEvidenceStrength,
|
|
AddressFilterSet,
|
|
AddressIntent,
|
|
AddressResponseType,
|
|
AddressResultMode,
|
|
AddressSemanticFrame
|
|
} from "../types/addressQuery";
|
|
|
|
export const ADDRESS_COVERAGE_EVIDENCE_SCHEMA_VERSION = "address_coverage_evidence_v1" as const;
|
|
|
|
export interface AddressCoverageEvidenceContract {
|
|
schema_version: typeof ADDRESS_COVERAGE_EVIDENCE_SCHEMA_VERSION;
|
|
policy_owner: "addressCoverageEvidencePolicy";
|
|
requested_result_mode: AddressResultMode | null;
|
|
result_mode: AddressResultMode | null;
|
|
evidence_strength: AddressEvidenceStrength | null;
|
|
balance_confirmed: boolean | null;
|
|
as_of_date_basis: AddressAsOfDateBasis | null;
|
|
coverage_status: AssistantCoverageStatus;
|
|
evidence_basis: AddressCoverageEvidenceBasis;
|
|
reason_codes: string[];
|
|
}
|
|
|
|
export interface ResolveAddressCoverageEvidenceInput {
|
|
intent: AddressIntent;
|
|
selectedRecipe: string | null;
|
|
filters: AddressFilterSet;
|
|
semanticFrame?: AddressSemanticFrame | null;
|
|
responseType: AddressResponseType;
|
|
rowsMatched: number;
|
|
overrideResultMode?: AddressResultMode | null;
|
|
overrideEvidenceStrength?: AddressEvidenceStrength | null;
|
|
overrideBalanceConfirmed?: boolean | 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 normalizeIsoDateHint(value: unknown): string | null {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})(?:T.*)?$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
const year = Number(match[1]);
|
|
const month = Number(match[2]);
|
|
const day = Number(match[3]);
|
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
|
return null;
|
|
}
|
|
const candidate = new Date(Date.UTC(year, month - 1, day));
|
|
if (
|
|
candidate.getUTCFullYear() !== year ||
|
|
candidate.getUTCMonth() + 1 !== month ||
|
|
candidate.getUTCDate() !== day
|
|
) {
|
|
return null;
|
|
}
|
|
return `${match[1]}-${match[2]}-${match[3]}`;
|
|
}
|
|
|
|
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 isResultMode(value: string | null): value is AddressResultMode {
|
|
return value === "heuristic_candidates" || value === "confirmed_balance";
|
|
}
|
|
|
|
function isEvidenceStrength(value: string | null): value is AddressEvidenceStrength {
|
|
return value === "weak" || value === "medium" || value === "strong";
|
|
}
|
|
|
|
function isCoverageStatus(value: string | null): value is AssistantCoverageStatus {
|
|
return value === "full" || value === "partial" || value === "blocked";
|
|
}
|
|
|
|
function isAsOfDateBasis(value: string | null): value is AddressAsOfDateBasis {
|
|
return (
|
|
value === "period_end" ||
|
|
value === "explicit_as_of_date" ||
|
|
value === "period_range" ||
|
|
value === "implicit_current_snapshot"
|
|
);
|
|
}
|
|
|
|
function isEvidenceBasis(value: string | null): value is AddressCoverageEvidenceBasis {
|
|
return (
|
|
value === "matched_rows" ||
|
|
value === "exact_negative" ||
|
|
value === "limited_response" ||
|
|
value === "heuristic_candidates" ||
|
|
value === "unknown"
|
|
);
|
|
}
|
|
|
|
export function isHeuristicCandidatesIntent(intent: AddressIntent): boolean {
|
|
return (
|
|
intent === "list_receivables_counterparties" ||
|
|
intent === "list_payables_counterparties" ||
|
|
intent === "list_open_contracts" ||
|
|
intent === "open_items_by_counterparty_or_contract"
|
|
);
|
|
}
|
|
|
|
export function isConfirmedBalanceIntent(intent: AddressIntent): boolean {
|
|
return (
|
|
intent === "account_balance_snapshot" ||
|
|
intent === "documents_forming_balance" ||
|
|
intent === "inventory_on_hand_as_of_date" ||
|
|
intent === "inventory_purchase_provenance_for_item" ||
|
|
intent === "inventory_purchase_documents_for_item" ||
|
|
intent === "inventory_sale_trace_for_item" ||
|
|
intent === "inventory_profitability_for_item" ||
|
|
intent === "inventory_purchase_to_sale_chain" ||
|
|
intent === "open_contracts_confirmed_as_of_date" ||
|
|
intent === "payables_confirmed_as_of_date" ||
|
|
intent === "receivables_confirmed_as_of_date" ||
|
|
intent === "vat_payable_confirmed_as_of_date" ||
|
|
intent === "vat_liability_confirmed_for_tax_period"
|
|
);
|
|
}
|
|
|
|
export function resolveAddressAsOfDateBasis(
|
|
filters: AddressFilterSet,
|
|
semanticFrame?: AddressSemanticFrame | null
|
|
): AddressAsOfDateBasis | null {
|
|
if (
|
|
semanticFrame?.date_scope_kind === "implicit_current" &&
|
|
semanticFrame.date_basis_hint === "implicit_current_snapshot"
|
|
) {
|
|
return "implicit_current_snapshot";
|
|
}
|
|
const asOfDate = normalizeIsoDateHint(filters.as_of_date);
|
|
if (asOfDate) {
|
|
return "explicit_as_of_date";
|
|
}
|
|
if (semanticFrame?.date_basis_hint) {
|
|
return semanticFrame.date_basis_hint;
|
|
}
|
|
const periodFrom = normalizeIsoDateHint(filters.period_from);
|
|
const periodTo = normalizeIsoDateHint(filters.period_to);
|
|
if (periodFrom && periodTo) {
|
|
return "period_range";
|
|
}
|
|
if (!periodFrom && periodTo) {
|
|
return "period_end";
|
|
}
|
|
if (periodFrom) {
|
|
return "period_range";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function deriveAddressEvidenceStrength(input: {
|
|
intent: AddressIntent;
|
|
selectedRecipe: string | null;
|
|
responseType: AddressResponseType;
|
|
rowsMatched: number;
|
|
}): AddressEvidenceStrength | null {
|
|
if (isHeuristicCandidatesIntent(input.intent)) {
|
|
if (input.rowsMatched <= 0 || input.responseType === "LIMITED_WITH_REASON") {
|
|
return "weak";
|
|
}
|
|
if (input.selectedRecipe === "address_open_items_by_party_or_contract_v1") {
|
|
return "medium";
|
|
}
|
|
return "weak";
|
|
}
|
|
if (isConfirmedBalanceIntent(input.intent)) {
|
|
if (input.rowsMatched > 0) {
|
|
return "strong";
|
|
}
|
|
return input.responseType === "LIMITED_WITH_REASON" ? "weak" : "medium";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function resolveAddressRequestedResultMode(
|
|
intent: AddressIntent,
|
|
filters: AddressFilterSet,
|
|
semanticFrame?: AddressSemanticFrame | null
|
|
): AddressResultMode | null {
|
|
if (isConfirmedBalanceIntent(intent)) {
|
|
return "confirmed_balance";
|
|
}
|
|
if (intent === "list_open_contracts") {
|
|
return "heuristic_candidates";
|
|
}
|
|
if (isHeuristicCandidatesIntent(intent)) {
|
|
const asOfDateBasis = resolveAddressAsOfDateBasis(filters, semanticFrame);
|
|
if (
|
|
asOfDateBasis === "explicit_as_of_date" ||
|
|
asOfDateBasis === "period_end" ||
|
|
asOfDateBasis === "period_range" ||
|
|
asOfDateBasis === "implicit_current_snapshot"
|
|
) {
|
|
return "confirmed_balance";
|
|
}
|
|
return "heuristic_candidates";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function balanceConfirmedFrom(input: {
|
|
intent: AddressIntent;
|
|
responseType: AddressResponseType;
|
|
}): boolean | null {
|
|
if (isHeuristicCandidatesIntent(input.intent)) {
|
|
return false;
|
|
}
|
|
if (isConfirmedBalanceIntent(input.intent)) {
|
|
return input.responseType !== "LIMITED_WITH_REASON";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function coverageStatusFrom(input: {
|
|
resultMode: AddressResultMode | null;
|
|
balanceConfirmed: boolean | null;
|
|
responseType: AddressResponseType;
|
|
rowsMatched: number;
|
|
}): AssistantCoverageStatus {
|
|
if (input.responseType === "LIMITED_WITH_REASON") {
|
|
return input.resultMode === "heuristic_candidates" ? "partial" : "blocked";
|
|
}
|
|
if (input.balanceConfirmed === false) {
|
|
return "partial";
|
|
}
|
|
if (input.rowsMatched > 0) {
|
|
return "full";
|
|
}
|
|
if (input.resultMode === "heuristic_candidates") {
|
|
return "partial";
|
|
}
|
|
if (input.resultMode === "confirmed_balance" && input.balanceConfirmed === true) {
|
|
return "full";
|
|
}
|
|
return "blocked";
|
|
}
|
|
|
|
function evidenceBasisFrom(input: {
|
|
resultMode: AddressResultMode | null;
|
|
responseType: AddressResponseType;
|
|
rowsMatched: number;
|
|
balanceConfirmed: boolean | null;
|
|
}): AddressCoverageEvidenceBasis {
|
|
if (input.responseType === "LIMITED_WITH_REASON") {
|
|
return "limited_response";
|
|
}
|
|
if (input.resultMode === "heuristic_candidates" || input.balanceConfirmed === false) {
|
|
return "heuristic_candidates";
|
|
}
|
|
if (input.rowsMatched > 0) {
|
|
return "matched_rows";
|
|
}
|
|
if (input.resultMode === "confirmed_balance") {
|
|
return "exact_negative";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
export function resolveAddressCoverageEvidence(
|
|
input: ResolveAddressCoverageEvidenceInput
|
|
): AddressCoverageEvidenceContract {
|
|
const requestedResultMode = resolveAddressRequestedResultMode(input.intent, input.filters, input.semanticFrame);
|
|
const resultMode = input.overrideResultMode ?? requestedResultMode;
|
|
const evidenceStrength =
|
|
input.overrideEvidenceStrength ?? deriveAddressEvidenceStrength(input);
|
|
const balanceConfirmed =
|
|
input.overrideBalanceConfirmed ?? balanceConfirmedFrom(input);
|
|
const asOfDateBasis = resolveAddressAsOfDateBasis(input.filters, input.semanticFrame);
|
|
const coverageStatus = coverageStatusFrom({
|
|
resultMode,
|
|
balanceConfirmed,
|
|
responseType: input.responseType,
|
|
rowsMatched: input.rowsMatched
|
|
});
|
|
const evidenceBasis = evidenceBasisFrom({
|
|
resultMode,
|
|
responseType: input.responseType,
|
|
rowsMatched: input.rowsMatched,
|
|
balanceConfirmed
|
|
});
|
|
const reasonCodes: string[] = [];
|
|
pushReason(reasonCodes, `coverage_status_${coverageStatus}`);
|
|
pushReason(reasonCodes, resultMode ? `result_mode_${resultMode}` : "result_mode_unknown");
|
|
pushReason(reasonCodes, evidenceStrength ? `evidence_strength_${evidenceStrength}` : "evidence_strength_none");
|
|
pushReason(reasonCodes, `evidence_basis_${evidenceBasis}`);
|
|
pushReason(reasonCodes, balanceConfirmed === true ? "balance_confirmed_true" : balanceConfirmed === false ? "balance_confirmed_false" : "balance_confirmed_unknown");
|
|
pushReason(reasonCodes, asOfDateBasis ? `as_of_date_basis_${asOfDateBasis}` : "as_of_date_basis_none");
|
|
return {
|
|
schema_version: ADDRESS_COVERAGE_EVIDENCE_SCHEMA_VERSION,
|
|
policy_owner: "addressCoverageEvidencePolicy",
|
|
requested_result_mode: requestedResultMode,
|
|
result_mode: resultMode,
|
|
evidence_strength: evidenceStrength,
|
|
balance_confirmed: balanceConfirmed,
|
|
as_of_date_basis: asOfDateBasis,
|
|
coverage_status: coverageStatus,
|
|
evidence_basis: evidenceBasis,
|
|
reason_codes: reasonCodes.slice(0, 24)
|
|
};
|
|
}
|
|
|
|
export function attachAddressCoverageEvidence<T extends Record<string, unknown>>(
|
|
debugPayload: T,
|
|
input: ResolveAddressCoverageEvidenceInput
|
|
): T & { address_coverage_evidence_v1: AddressCoverageEvidenceContract } {
|
|
return {
|
|
...debugPayload,
|
|
address_coverage_evidence_v1: resolveAddressCoverageEvidence(input)
|
|
};
|
|
}
|
|
|
|
export function toAddressCoverageEvidenceContract(value: unknown): AddressCoverageEvidenceContract | null {
|
|
const record = toRecordObject(value);
|
|
if (!record) {
|
|
return null;
|
|
}
|
|
const requestedResultMode = toNonEmptyString(record.requested_result_mode);
|
|
const resultMode = toNonEmptyString(record.result_mode);
|
|
const evidenceStrength = toNonEmptyString(record.evidence_strength);
|
|
const asOfDateBasis = toNonEmptyString(record.as_of_date_basis);
|
|
const coverageStatus = toNonEmptyString(record.coverage_status);
|
|
const evidenceBasis = toNonEmptyString(record.evidence_basis);
|
|
const balanceConfirmed = typeof record.balance_confirmed === "boolean" ? record.balance_confirmed : null;
|
|
if (!isCoverageStatus(coverageStatus) || !isEvidenceBasis(evidenceBasis)) {
|
|
return null;
|
|
}
|
|
if (requestedResultMode !== null && !isResultMode(requestedResultMode)) {
|
|
return null;
|
|
}
|
|
if (resultMode !== null && !isResultMode(resultMode)) {
|
|
return null;
|
|
}
|
|
if (evidenceStrength !== null && !isEvidenceStrength(evidenceStrength)) {
|
|
return null;
|
|
}
|
|
if (asOfDateBasis !== null && !isAsOfDateBasis(asOfDateBasis)) {
|
|
return null;
|
|
}
|
|
return {
|
|
schema_version: ADDRESS_COVERAGE_EVIDENCE_SCHEMA_VERSION,
|
|
policy_owner: "addressCoverageEvidencePolicy",
|
|
requested_result_mode: requestedResultMode,
|
|
result_mode: resultMode,
|
|
evidence_strength: evidenceStrength,
|
|
balance_confirmed: balanceConfirmed,
|
|
as_of_date_basis: asOfDateBasis,
|
|
coverage_status: coverageStatus,
|
|
evidence_basis: evidenceBasis,
|
|
reason_codes: Array.isArray(record.reason_codes)
|
|
? record.reason_codes
|
|
.map((item) => toNonEmptyString(item))
|
|
.filter((item): item is string => Boolean(item))
|
|
.slice(0, 24)
|
|
: []
|
|
};
|
|
}
|