/** * Pure, dependency-free foundation for deep-interview state shape. * * Ownership boundary (per the approved consensus plan): this leaf module owns the * canonical persisted shape (interview data nested under `state`), durable round * identity/hashing, lossless legacy normalization, and the deep-interview-specific * envelope/round merge used by every writer (`deep-interview-recorder`, * `state-runtime` write/reconcile, seed, and handoff). It MUST NOT import the * active-state, state-writer, CLI runtime, or filesystem so it stays cycle-free and * trivially testable. */ export type DeepInterviewRoundLifecycle = "answered" | "pending_scoring" | "scored"; export type DeepInterviewTriggerKind = "A" | "B" | "C" | "D"; /** `active` triggers must satisfy the bidirectional invariant; disputed/unresolved are exempt with rationale. */ export type DeepInterviewTriggerStatus = "active" | "disputed" | "unresolved"; export interface DeepInterviewEstablishedFact { id: string; statement: string; round: number; component?: string; dimension?: string; evidence?: string; disputed: boolean; /** * Resolution pointer for a disputed fact: the id of the fact that replaced it * after the user confirmed a pivot. A disputed fact without `superseded_by` * keeps the deterministic ambiguity floor elevated; setting it releases the * pressure while preserving the contradicted fact for audit. */ superseded_by?: string; } export interface DeepInterviewTriggerMetadata { kind: DeepInterviewTriggerKind; name: string; status: DeepInterviewTriggerStatus; component: string; dimension: string; priorDimensionScore?: number; newDimensionScore?: number; priorAmbiguity?: number; newAmbiguity?: number; evidence?: string; contradictedFactId?: string; /** Required when status is `disputed` or `unresolved` to exempt the invariant. */ rationale?: string; } export interface DeepInterviewRoundRecord { round_key: string; round_id?: string; round: number; question_id?: string; question_text?: string; question_hash: string; answer_hash: string; selected_options?: string[]; custom_input?: string; component?: string; dimension?: string; ambiguity_at_ask?: number; lifecycle: DeepInterviewRoundLifecycle; answered_at: string; scored_at?: string; scores?: Record; /** Effective ambiguity after the deterministic floor clamp (`max(reported, floor)`). */ ambiguity?: number; /** Original LLM-reported ambiguity, preserved for audit when the floor clamped it. */ reported_ambiguity?: number; /** Deterministic floor in effect when this round was scored, when it clamped. */ ambiguity_floor?: number; triggers?: DeepInterviewTriggerMetadata[]; } export interface DeepInterviewStateEnvelope { threshold?: number; threshold_source?: string; state?: Record; [key: string]: unknown; } export declare function hashContent(value: string): string; /** * Canonicalize interview prose to NFC. * * Hangul, unlike most Latin prose, routinely reaches the interview in both * composed and decomposed form (macOS-sourced pastes and some IME/clipboard * paths emit NFD), and the two forms are the same text for the user while * being different JavaScript strings. Canonicalizing at the identity and * length boundaries keeps one round per answer and keeps the character caps * script-fair: decomposed Hangul otherwise costs two to three code points per * syllable against the same budget. */ export declare function canonicalizeDeepInterviewText(value: string): string; export declare function questionHash(questionText: string): string; export declare function answerHash(selectedOptions: string[] | undefined, customInput: string | undefined): string; /** * Durable round identity. Prefer `interview_id + round_id`; fall back to * `interview_id + round + question.id` when no caller-supplied `round_id` exists. */ export declare function deriveRoundKey(interviewId: string | undefined, input: { round_id?: string; round: number; questionId?: string; }): string; /** * Canonicalize a deep-interview envelope: interview data nested under `state`, * legacy flattened fields hoisted in losslessly, transcript duplicates removed * from the top level, and `rounds`/`established_facts` guaranteed to be arrays. * * Idempotent: a canonical envelope is returned unchanged in shape. Preserves all * unknown envelope and nested fields except the envelope-reserved keys that leak * into `state` (see `ENVELOPE_RESERVED_STATE_KEYS`), which are stripped so a * malformed envelope-in-state write cannot permanently nest state. Never mutates * the input. */ export declare function normalizeDeepInterviewEnvelope(value: unknown): DeepInterviewStateEnvelope; /** * Lossless, idempotent merge of two round arrays. * * - Records sharing a durable key (`round_key`, or synthesized from * `round_id`/`question_id`) merge into one, preferring scored over answered. * - Records without any durable identity are preserved verbatim; an exact * duplicate is skipped so repeated writes stay idempotent, but distinct records * are never collapsed. * * Deliberate refinement of the approved plan: rather than mutating opaque legacy * records with synthetic `legacy:` keys, they are preserved verbatim with * exact-duplicate dedupe. This satisfies the plan's intent (lossless, idempotent, * never collapse distinct rounds) without rewriting user-supplied round objects, * and keeps free-form extension preservation intact. Recorder-produced records * always carry a `round_key`, so the synthetic path is unnecessary in practice. */ export declare function mergeDeepInterviewRounds(existing: readonly Record[], incoming: readonly Record[]): Record[]; /** * Deep-interview-specific envelope merge. Unlike the generic shallow null-delete * merge, this keeps interview data nested under `state`, never deletes `state`, * and merges `rounds` losslessly by durable key so a partial write (e.g. a * scoring update) cannot drop recorder-written transcript history. */ export declare function mergeDeepInterviewEnvelope(existing: unknown, incoming: unknown, options?: { replace?: boolean; }): DeepInterviewStateEnvelope; export declare const DEEP_INTERVIEW_INTENT_CATEGORIES: readonly ["artifact", "surface", "integration", "constraint"]; export type DeepInterviewIntentCategory = (typeof DEEP_INTERVIEW_INTENT_CATEGORIES)[number]; /** * User-input fields that legitimately carry prose (goals, prompts, descriptions, * answers). Shell metacharacters (`;`, `|`, `&`, backticks, `$()`) are valid prose * here and must NOT be rejected as structural injection. Structural fields (ids, * categories, hashes) stay strictly validated by their own guards. */ export declare const DEEP_INTERVIEW_FREETEXT_FIELDS: ReadonlySet; /** DoS-prevention character-count caps, matching ask-schema string-length semantics. */ export declare const MAX_INITIAL_CONTEXT_LENGTH = 50000; export declare const MAX_USER_RESPONSE_LENGTH = 10000; /** Maximum serialized JavaScript-string length for one LLM-produced structured response. */ export declare const MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH = 100000; /** Count Unicode code points without allocating an intermediate character array. */ export declare function deepInterviewCharacterCount(value: string): number; export declare function isDeepInterviewFreeTextField(name: string): boolean; /** * Assert that one structured deep-interview response is JSON-serializable and * bounded by JavaScript string length. This deliberately does not measure or * cap accumulated persisted interview state. */ export declare function assertDeepInterviewStructuredResponseWithinLimit(value: unknown): void; /** * Assert a free-text input is within its size cap. Never inspects content for shell * metacharacters — free-text fields accept prose verbatim; this only bounds length. * The cap is measured on the NFC form so decomposed Hangul is charged the same * budget as the identical composed text. */ export declare function assertDeepInterviewInputWithinLimit(value: string, max: number, fieldName?: string): void; /** Validate user-supplied deep-interview prose before an envelope is persisted. */ export declare function assertDeepInterviewEnvelopeInputLimits(envelope: Record): void; export interface DeepInterviewIntentItem { id: string; category: DeepInterviewIntentCategory; statement: string; } export interface DeepInterviewIntentManifest { version: 1; items: DeepInterviewIntentItem[]; digest: string; confirmation_round: 0; confirmation_answer_hash: string; confirmation_evidence: string; } export interface DeepInterviewIntentSubstitution { removed_id: string; replacement_ids: string[]; rationale: string; } export interface DeepInterviewIntentReview { version: 1; status: "not_required" | "pending" | "approved"; locked_digest: string; observed_digest: string; removed_locked_ids: string[]; supporting_substitutions: DeepInterviewIntentSubstitution[]; approval_round?: number; answer_hash?: string; user_answer_evidence?: string; } export declare function isCanonicalDeepInterviewAnswerHash(value: unknown): value is string; export declare function deepInterviewIntentManifestDigest(items: readonly DeepInterviewIntentItem[]): string; export declare function deepInterviewObservedIntentDigest(ids: readonly string[]): string; export declare function createDeepInterviewIntentManifest(items: readonly DeepInterviewIntentItem[], confirmation: { round: 0; answer_hash: string; }): DeepInterviewIntentManifest; export declare function assertDeepInterviewIntentManifest(value: unknown): asserts value is DeepInterviewIntentManifest; export declare function reviewDeepInterviewIntent(locked: DeepInterviewIntentManifest, observedItems: readonly DeepInterviewIntentItem[], input: Omit): DeepInterviewIntentReview; export declare function assertDeepInterviewIntentReview(value: unknown, locked: DeepInterviewIntentManifest, observedIds: readonly string[], recordedAnswers: readonly { round: unknown; answer_hash: unknown; }[]): asserts value is DeepInterviewIntentReview;