Files
Jyotisha/frontend/src/lib/rectification-agentic/v9/evidence-model.ts
T
Jesse_Chen b1b4f5fac9
Independent Staging Quality Gate / validate (push) Successful in 10m19s
Independent Staging Quality Gate / publish (push) Successful in 8m7s
fix(rectification): score family events and allow appearance follow-up
Dated family evidence now moves candidates via D12 and kin houses, career receipts expose both D1-10 and D10, and appearance/marks may be asked as auxiliary first-house scores. New cases bind Skill 10.0.4.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 08:35:46 +08:00

207 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* V9 evidence model contracts: event kinds, domains, date precision and the
* append-only revision/status machine. IDs are always generated by the server.
*/
export const EVIDENCE_KINDS = [
"education_start",
"education_completion",
"education_interruption",
"education_change",
"education_milestone",
"career_entry",
"career_change",
"promotion",
"career_pressure",
"career_exit",
"business_start",
"relationship_start",
"relationship_commitment",
"relationship_separation",
"relationship_end",
"relationship_change",
"relocation",
"foreign_move",
"return",
"home_change",
"finance_gain",
"finance_loss",
"income_change",
"asset_change",
"finance_change",
"self_health_event",
"pressure_period",
"family_event",
"appearance_note",
"birthmark_or_scar",
"other",
] as const;
export type EvidenceKind = (typeof EVIDENCE_KINDS)[number];
export const EVIDENCE_DOMAINS = [
"education",
"career",
"relationship",
"relocation",
"finance",
"health",
"health_pressure",
"family",
"appearance",
"marks",
"other",
] as const;
export type EvidenceDomain = (typeof EVIDENCE_DOMAINS)[number];
export const DATE_PRECISIONS = [
"year",
"month",
"quarter",
"day",
"range",
"unknown",
] as const;
export type DatePrecision = (typeof DATE_PRECISIONS)[number];
export const EVIDENCE_STATUSES = [
"draft",
"pending_confirmation",
"confirmed",
"superseded",
"rejected",
] as const;
export type EvidenceStatus = (typeof EVIDENCE_STATUSES)[number];
const KIND_SET = new Set<string>(EVIDENCE_KINDS);
const DOMAIN_SET = new Set<string>(EVIDENCE_DOMAINS);
const PRECISION_SET = new Set<string>(DATE_PRECISIONS);
const STATUS_SET = new Set<string>(EVIDENCE_STATUSES);
export function isEvidenceKind(value: unknown): value is EvidenceKind {
return typeof value === "string" && KIND_SET.has(value);
}
export function isEvidenceDomain(value: unknown): value is EvidenceDomain {
return typeof value === "string" && DOMAIN_SET.has(value);
}
export function isDatePrecision(value: unknown): value is DatePrecision {
return typeof value === "string" && PRECISION_SET.has(value);
}
export function isEvidenceStatus(value: unknown): value is EvidenceStatus {
return typeof value === "string" && STATUS_SET.has(value);
}
/**
* Kinds that are semantically distinct and must never be folded together.
* The scoring path keys on event_kind, so collapsing these would corrupt
* both the ledger and the candidate contrast.
*/
export const DISTINCT_KIND_GROUPS: readonly (readonly EvidenceKind[])[] = [
["career_entry", "career_pressure", "career_exit"],
["relationship_start", "relationship_commitment", "relationship_separation"],
];
/**
* Legal evidence status transitions. Only the server confirmation path may
* produce `confirmed`; a grounded draft may use that server path in the same run.
*/
export const EVIDENCE_STATUS_TRANSITIONS: Readonly<
Record<EvidenceStatus, readonly EvidenceStatus[]>
> = {
draft: ["pending_confirmation", "confirmed", "rejected", "superseded"],
pending_confirmation: ["confirmed", "rejected", "superseded"],
confirmed: ["superseded"],
superseded: [],
rejected: [],
};
export function canTransitEvidenceStatus(
from: EvidenceStatus,
to: EvidenceStatus,
): boolean {
return EVIDENCE_STATUS_TRANSITIONS[from].includes(to);
}
/**
* Normalized quote grounding contract: a user_quote is accepted only when it
* is a substring match after whitespace/punctuation normalization of the
* source turn's user message.
*/
const QUOTE_PUNCTUATION = /[\s\u3000,。!?、;:“”‘’()《》·—…,!.;:?]/g;
export function normalizeQuote(value: string): string {
return value.replace(QUOTE_PUNCTUATION, "").toLowerCase();
}
const PRECISION_RANK: Readonly<Record<string, number>> = {
day: 4,
month: 3,
quarter: 2,
range: 2,
year: 1,
unknown: 0,
};
export function datePrecisionRank(precision: string): number {
return PRECISION_RANK[precision] ?? -1;
}
/**
* Agent-facing date label. Day precision must never collapse to a year.
*/
export function displayDateLabel(
precision: string,
occurredFrom: string | null,
occurredTo: string | null,
): string {
const from = occurredFrom?.slice(0, 10) ?? "";
const to = occurredTo?.slice(0, 10) ?? "";
if (precision === "day" && from) return from;
if (precision === "month" && from) return from.slice(0, 7);
if (precision === "year" && from) return `${from.slice(0, 4)}`;
if (precision === "quarter" && from) return from.slice(0, 7);
if (precision === "range") {
if (from && to) return `${from}${to}`;
return from || to || "日期范围";
}
if (precision === "unknown") return "日期不明";
return from || "日期不明";
}
export function quoteIsGroundedInMessage(
userMessage: string,
quote: string,
): boolean {
const normalizedMessage = normalizeQuote(userMessage);
const normalizedQuote = normalizeQuote(quote);
return (
normalizedQuote.length > 0 &&
normalizedMessage.includes(normalizedQuote)
);
}
/** Background kinds that never advance scoring coverage counts. */
export const BACKGROUND_ONLY_KINDS: ReadonlySet<EvidenceKind> = new Set([
"other",
]);
export function isBackgroundEvidenceKind(kind: EvidenceKind): boolean {
return BACKGROUND_ONLY_KINDS.has(kind);
}
/** Ledger/engine subject follows the domain. Family events must not stay on the tool default `self`. */
export function evidenceSubjectForDomain(
domain: string,
_subject?: string | null,
): "self" | "family" | "other" {
if (domain === "family") return "family";
if (domain === "other") return "other";
return "self";
}