/** * Inquiry mapping — ADR 0003 step 5. * * This module implements the "memoize the mapping, never the answer" principle. * A MappingProposal is a reviewable record produced by a pluggable MappingProposer. * Proposals flow through Survey's existing candidate → review machinery. * The durable artifact after review is an InquiryMapping. * Answers always recompute live from the TrustBundle. * * Nothing here silently decides. Exact canonical-form matching is the only thing * that resolves without review. A proposer may suggest that a question maps to a * registered claim or rule — but every suggestion lands as a MappingProposal with * provenance before it counts. (ADR 0003 §4) * * Integration point: resolveQuestion is the entry point for consumers checking * whether a cached mapping already covers a question. Flow-agent hook wiring * (connecting this to an agent's output pipeline) is out of scope for this module * and lives in the flow-agents repo. */ import type { DerivationRule, InquiryRecord, TrustBundle } from "@kontourai/surface"; import type { CanonicalClaimTarget } from "@kontourai/surface"; import type { Candidate, CandidateSet, ReviewOutcome } from "./types.js"; import type { AutoAcceptWarning } from "./producer-profile.js"; import type { ReviewItem } from "./review-resource.js"; /** * A single machine- or human-generated suggestion that a natural-language * question maps to a canonical claim target or a named derivation rule. * * Exactly one of proposedTarget / proposedRuleId must be set. */ export interface MappingProposal { id: string; question: string; /** The canonical claim target this question is proposed to map to. */ proposedTarget?: CanonicalClaimTarget; /** The derivation rule id this question is proposed to map to. */ proposedRuleId?: string; /** Proposer confidence in the mapping (0–1). */ confidence: number; /** Human-readable rationale for the proposal. */ rationale: string; /** Optional verbatim excerpt from the question that drove the suggestion. */ excerpt?: string; /** Who or what generated this proposal (name of the MappingProposer). */ proposedBy: string; /** ISO 8601 timestamp. */ proposedAt: string; } /** * The durable reviewed artifact: a natural-language question has been mapped * to a canonical claim target or derivation rule and the mapping has been * given a status through review (or auto-accept policy). * * Per ADR 0003 §6: memoize the mapping, never the answer. Answers always * recompute from live claim status; this record is never updated to carry * a cached answer. */ export interface InquiryMapping { id: string; /** Deterministic normalized form of the question (see normalizeQuestion). */ normalizedQuestion: string; /** The canonical claim target this mapping resolves to. */ target?: CanonicalClaimTarget; /** The derivation rule id this mapping resolves to. */ ruleId?: string; /** Whether the mapping was accepted by a human reviewer or auto-accept policy. */ status: "verified" | "assumed" | "rejected"; /** Actor who performed the review (reviewer id or "auto-accept-policy"). */ reviewedBy: string; /** ISO 8601 timestamp of review. */ reviewedAt: string; /** Optional rationale for the decision. */ rationale?: string; /** * Whether the reviewer was within their declared comfort zone. * Mirrors Survey's withinComfortZone semantics: false means a different * authority should confirm; true (or absent) means the reviewer was * comfortable making this decision. */ withinComfortZone?: boolean; /** The id of the MappingProposal that was accepted or rejected. */ proposalId: string; } /** * Pluggable interface for proposing a canonical mapping for a question. * * Implementations may be deterministic (like the reference proposer below), * embedding-based, or LLM-backed — but they are always proposers: their output * goes through review before it counts (ADR 0003 §4). * * The interface accepts both synchronous return values and Promises, matching * Survey's async-optional style. */ export interface MappingProposer { name: string; propose(question: string, context: { bundle?: TrustBundle; rules?: DerivationRule[]; }): MappingProposal[] | Promise; } /** * Deterministic normalization for question strings. * * Rules: * - Lowercase * - Collapse internal whitespace runs to a single space * - Trim leading/trailing whitespace * - Strip terminal punctuation (. ? ! , ;) from the end * * This is exact normalized-text memoization, not semantic matching. * Two questions that differ only in case, whitespace, or trailing punctuation * are considered the same question. Questions with different wording but the * same intent are NOT matched here; a MappingProposer handles that. */ export declare function normalizeQuestion(question: string): string; /** * Project an array of proposals for a single question into Survey's existing * Candidate / CandidateSet shapes so they flow through the existing review * machinery rather than a parallel system. * * Status rules: * - All proposals agree on the same target/ruleId → "needs-review" * - Proposals disagree (more than one distinct resolved target/rule) → "conflict" * - Empty proposals → "needs-review" with empty candidates * * The CandidateSet target is the normalized question; each Candidate carries * the proposal id as extractionId and the proposal metadata. */ export declare function proposalsToCandidateSet(question: string, proposals: MappingProposal[]): { candidateSet: CandidateSet; candidates: Candidate[]; }; /** * Turn a Survey ReviewOutcome on a mapping candidate set into a durable * InquiryMapping record. * * The candidateSet must have been built by proposalsToCandidateSet. * The reviewOutcome's candidateId must match one of the candidates. */ export declare function applyMappingReview(candidateSet: CandidateSet, reviewOutcome: ReviewOutcome): InquiryMapping; export interface AutoAcceptPolicy { minConfidence: number; } /** Optional hooks for {@link applyAutoAcceptPolicy}. */ export interface AutoAcceptPolicyOptions { /** * Called once for each proposal the policy refused because its confidence * is not a finite number in [0, 1]. The proposal gets no mapping and stays * in human review. */ onWarning?: (warning: AutoAcceptWarning) => void; } /** * Apply an auto-accept policy to a list of proposals, returning InquiryMappings. * * Proposals at or above minConfidence → status "assumed", withinComfortZone: true * Proposals below minConfidence → return a "needs-review" mapping (not yet durable) * Proposals whose confidence is not a finite number in [0, 1] are never * auto-accepted. Throws `RangeError` unless `policy.minConfidence` is a finite * number in (0, 1]. Refused out-of-range proposals are reported through * `options.onWarning`. * * Only non-conflicting proposals are auto-accepted. If proposals disagree, they * need human review regardless of confidence. * * Returns an array of InquiryMappings (only for accepted proposals). */ export declare function applyAutoAcceptPolicy(proposals: MappingProposal[], policy: AutoAcceptPolicy, options?: AutoAcceptPolicyOptions): InquiryMapping[]; /** * Look up an InquiryMapping for a question by exact normalized-text match. * * Rejected mappings are remembered but never resolve: this function returns * undefined for rejected mappings so callers treat the question as a miss. * To check whether a question was previously rejected (and should not be * re-proposed), call lookupRejectedMapping. */ export declare function lookupMapping(mappings: InquiryMapping[], question: string): InquiryMapping | undefined; /** * Check whether a question was previously rejected. * Rejected mappings prevent re-proposing: if this returns a mapping, the * question should not be sent to a proposer again without human escalation. */ export declare function lookupRejectedMapping(mappings: InquiryMapping[], question: string): InquiryMapping | undefined; /** * Resolve a natural-language question against a TrustBundle. * * On mapping hit (verified or assumed): constructs a Surface Inquiry from the * mapped target/rule and returns resolveInquiry(...) — answers always recompute * live from the current bundle state; the mapping is memoized, not the answer. * * On miss (no mapping, or rejected mapping): returns an InquiryRecord with * outcome "unsupported" so the gap is honest and recordable. * * This function is the clean integration point for consumers. Flow-agent hook * wiring (connecting this to an agent's output pipeline) lives in the * flow-agents repo. */ export declare function resolveQuestion(bundle: TrustBundle, question: string, options: { mappings: InquiryMapping[]; rules?: DerivationRule[]; now?: Date; askedBy: string; }): InquiryRecord; /** * Build ReviewItem records for the existing review workbench from a list of * mapping candidate sets. * * Follow the existing ReviewItem contract exactly. This helper produces * ReviewItem payloads so mapping proposals can be reviewed through the same * workbench as other Survey candidates. */ export declare function buildMappingReviewItems(candidateSets: Array<{ candidateSet: CandidateSet; candidates: Candidate[]; }>): ReviewItem[]; /** * Reference MappingProposer for tests. * * REFERENCE IMPLEMENTATION ONLY — not suitable for production matching. * * Matching strategy: a question maps to a claim if it contains both the * claim's subjectId and fieldOrBehavior as token substrings (case-insensitive, * space-delimited token match). This is intentionally simple and transparent * so tests can be deterministic. */ export declare const referenceMappingProposer: MappingProposer;