/** Reproducible synthetic cases. Runtime core/fixtures are injected unchanged by UI and tests. */ import type { Claim, Decision, Input, Product, Route, Source } from "./grounded-answer"; import type { Scenario } from "./grounded-answer-fixtures"; export const caseSchemaVersion = 1; export const coreVersion = "sha256:0b8772443c20faedf8d7c5bd8f855ce3ebb80d3a5602a65a3fe1b7a949206530"; export const fixtureVersion = "sha256:352aa9142125ff46bbdbb8b42db4a5a4d30f3f6c4667383d13569b476895d074"; export type Simulation = "none" | "valid" | "forged" | "wrong-product" | "unsupported-value"; export type CaseState = { scenarioId: string; input: Input; sourceIds: readonly string[]; simulation: Simulation; expected: Route }; export type CaseEngine = { catalog: readonly Product[]; evidence: readonly Source[]; scenarios: readonly Scenario[]; evaluate: (input: Input, products: readonly Product[], sources: readonly Source[]) => Decision }; const routes = ["deterministic-answer", "model-request", "safe-fallback"]; const simulations = ["none", "valid", "forged", "wrong-product", "unsupported-value"]; const caseAnchors = ["#grounded-scenarios-title", "#grounded-evidence", "#grounded-source"]; const record = (v: unknown): v is Record => v !== null && typeof v === "object" && !Array.isArray(v); const clone = (v: T): T => JSON.parse(JSON.stringify(v)); const same = (a: unknown, b: unknown): boolean => { if (Object.is(a, b)) return true; if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => same(v, b[i])); if (!record(a) || !record(b)) return false; const keys = Object.keys(a); return keys.length === Object.keys(b).length && keys.every(key => Object.hasOwn(b, key) && same(a[key], b[key])); }; export function presetCase(id: string, engine: CaseEngine): CaseState { const scenario = engine.scenarios.find(item => item.id === id); if (!scenario) throw new Error("Unknown synthetic preset"); const { simulatedResponse, ...input } = scenario.input; return clone({ scenarioId: id, input, sourceIds: scenario.sourceIds, simulation: simulatedResponse ? "forged" : "none", expected: scenario.expected }); } /** Only known preset IDs enter URLs; visitor text, hashes and unknown query values never do. */ export function presetHref(id: string, engine: CaseEngine) { if (!engine.scenarios.some(item => item.id === id)) throw new Error("Unknown synthetic preset"); return `/lab/grounded-answer?scenario=${encodeURIComponent(id)}`; } export function readPresetLocation(search: string, engine: CaseEngine, hash = "") { const params = new URLSearchParams(search); const values = params.getAll("scenario"); const valid = search.length <= 200 && values.length === 1 && Array.from(params.keys()).every(key => key === "scenario") && engine.scenarios.some(item => item.id === values[0]); const id = valid ? values[0] : engine.scenarios[0].id; const anchor = caseAnchors.includes(hash) ? hash : ""; return { id, invalid: search !== "" && !valid, href: presetHref(id, engine) + anchor }; } /** Structural validation, not a claim that the input has eligible evidence or a valid clock. */ export function validateCase(value: unknown, engine: CaseEngine): CaseState { if (!record(value) || !record(value.input)) throw new Error("Invalid case structure"); const v = value.input; const ids = (items: unknown, allowed: readonly string[], limit: number): items is string[] => Array.isArray(items) && items.length <= limit && items.every(id => typeof id === "string" && allowed.includes(id)) && new Set(items).size === items.length; if (!engine.scenarios.some(item => item.id === value.scenarioId) || !routes.includes(value.expected as string) || !simulations.includes(value.simulation as string) || typeof v.message !== "string" || v.message.length > 500 || typeof v.now !== "string" || v.now.length > 40 || !["fact", "compare", "explain"].includes(v.intent as string) || !["price", "connection", "mounting"].includes(v.field as string) || !["us", "cn"].includes(v.region as string) || typeof v.providerAvailable !== "boolean" || !ids(v.previousProductIds, engine.catalog.map(p => p.id), 2) || !ids(value.sourceIds, engine.evidence.map(s => s.id), engine.evidence.length) || Object.hasOwn(v, "simulatedResponse")) throw new Error("Unsupported case input"); return clone({ scenarioId: value.scenarioId, input: { message: v.message, intent: v.intent, field: v.field, region: v.region, now: v.now, previousProductIds: v.previousProductIds, providerAvailable: v.providerAvailable }, sourceIds: value.sourceIds, simulation: value.simulation, expected: value.expected }) as CaseState; } export function isModifiedCase(state: CaseState, engine: CaseEngine) { const ordered = (value: CaseState) => ({ ...value, sourceIds: engine.evidence.filter(source => value.sourceIds.includes(source.id)).map(source => source.id) }); return !same(ordered(state), ordered(presetCase(state.scenarioId, engine))); } /** The UI and local report use this one request builder and the same accepted decision core. */ export function evaluateCase(state: CaseState, engine: CaseEngine) { const included = engine.evidence.filter(source => state.sourceIds.includes(source.id)); const submitted: Input = { ...state.input, simulatedResponse: undefined }; const preliminary = engine.evaluate(submitted, engine.catalog, included); if (state.input.intent === "explain" && state.simulation !== "none") { const claims: Claim[] = preliminary.products.flatMap(product => { const source = preliminary.reviews.find(review => review.accepted && review.source.productId === product.id)?.source; return source ? [{ productId: product.id, field: state.input.field, value: source.facts[state.input.field]!, citationId: source.id }] : []; }); if (claims[0]) { if (state.simulation === "forged") claims[0] = { ...claims[0], citationId: "forged-source" }; if (state.simulation === "wrong-product") claims[0] = { ...claims[0], productId: engine.catalog.find(product => product.id !== claims[0].productId)!.id }; if (state.simulation === "unsupported-value") claims[0] = { ...claims[0], value: "Invented value, absent from the evidence" }; } submitted.simulatedResponse = { claims }; } return { included, submitted, result: engine.evaluate(submitted, engine.catalog, included) }; } export function exportCase(value: unknown, engine: CaseEngine) { const state = validateCase(value, engine); const { submitted, result } = evaluateCase(state, engine); return clone({ schemaVersion: caseSchemaVersion, coreVersion, fixtureVersion, kind: "independent-synthetic-reference", synthetic: true, liveModel: false, presetId: state.scenarioId, modified: isModifiedCase(state, engine), state, input: submitted, decision: result, evidence: result.reviews.map(review => ({ id: review.source.id, accepted: review.accepted, reasons: review.reasons })) }); } /** Recompute rather than trust saved decisions. Only this schema and exact core/fixtures are supported. */ export function replayCaseExport(value: unknown, engine: CaseEngine) { if (!record(value) || value.schemaVersion !== caseSchemaVersion || value.coreVersion !== coreVersion || value.fixtureVersion !== fixtureVersion) throw new Error("Unsupported case version"); const actual = exportCase(value.state, engine); if (!same(value, actual)) throw new Error("Saved case does not match its deterministic replay"); return actual; } /** Retain the host router's opaque metadata when a native fragment entry has null state. */ export function caseHistoryState(previous: unknown, current: unknown, key: string) { return { ...(record(previous) ? previous : {}), ...(record(current) ? current : {}), groundedAnswerCase: key }; } /** Drafts live only in this controller's tab memory. History receives opaque keys, never input text. */ export function createCaseTimeline(engine: CaseEngine, session: string) { let serial = 0; let currentKey = ""; let currentHref = ""; let currentDraft: CaseState | null = null; const drafts = new Map(); const freshKey = () => `${session}:${++serial}`; return { restore(search: string, key: unknown, hash = "", pathname = "/lab/grounded-answer") { if (pathname !== "/lab/grounded-answer" && pathname !== "/lab/grounded-answer/") return null; const location = readPresetLocation(search, engine, hash); // Some fragment navigations copy the old key instead of exposing null. // A changed trusted hash with the same key still needs a distinct entry. const copiedAnchorKey = key === currentKey && location.href !== currentHref && caseAnchors.includes(hash); const remembered = !location.invalid && !copiedAnchorKey && typeof key === "string" ? drafts.get(key) : undefined; currentKey = remembered?.scenarioId === location.id ? key as string : freshKey(); currentHref = location.href; // Native fragment entries may have null history.state. Carry the current // same-preset draft into that new entry before giving it its own key. const anchorDraft = !remembered && !location.invalid && caseAnchors.includes(hash) && currentDraft?.scenarioId === location.id ? currentDraft : null; const state = remembered?.scenarioId === location.id ? clone(remembered) : anchorDraft ? clone(anchorDraft) : presetCase(location.id, engine); currentDraft = clone(state); drafts.set(currentKey, clone(state)); return { ...location, state, key: currentKey, mode: "replace" as const }; }, select(id: string, current: CaseState) { drafts.set(currentKey, clone(current)); const state = presetCase(id, engine); const mode = current.scenarioId === id ? "replace" as const : "push" as const; if (mode === "push") currentKey = freshKey(); currentDraft = clone(state); currentHref = presetHref(id, engine); drafts.set(currentKey, clone(state)); return { state, key: currentKey, href: currentHref, mode, invalid: false }; }, remember(state: CaseState) { currentDraft = clone(state); if (currentKey) drafts.set(currentKey, clone(state)); } }; }