import { type InstitutionalValidationIssue, type InstitutionalWrite } from "./institutional.js"; import type { InstitutionalMemory, MemoryContext, MemoryTrace, MemoryWrite } from "./types.js"; export type CorrectionRootCause = "knowledge_gap" | "procedure_fault" | "duplicate_conflict" | "stale_position" | "ambiguous"; export type CandidateMutation = { kind: "create"; proposed: MemoryWrite; } | { kind: "update" | "supersede"; targetMemoryId: string; proposed: MemoryWrite; } | { kind: "retire"; targetMemoryId: string; note: string; } | { kind: "route_adjustment"; targetMemoryId?: string; note: string; }; export type CandidateMutationKind = CandidateMutation["kind"]; export type CandidateLifecycleState = "pending_validation" | "validated" | "needs_changes" | "rejected" /** Transient: an approve() call has atomically claimed the candidate and is applying its mutation. Not itself terminal -- on apply failure it reverts to "validated" so the candidate stays retryable. */ | "applying" | "applied"; /** Every `CandidateLifecycleState`, for callers (e.g. CLI flag validation) that need to enumerate them without duplicating the union. */ export declare const CANDIDATE_LIFECYCLE_STATES: readonly CandidateLifecycleState[]; export interface CorrectionInput { id?: string; sessionId: string; prompt: string; correctionText: string; expectedOutcome: string; /** * An unauthenticated provenance hint, not a verified identity. This * library has no concept of authentication -- callers that expose * `submit`/`list`/`get` to multiple tenants or untrusted agents are * responsible for scoping access and for supplying a trustworthy `actor` * to `approve`/`reject`/`requestChanges` from their own session context, * not from this field. */ actor: string; context: MemoryContext; trace: MemoryTrace; /** Memory IDs the correction disputes as wrong, missing, or stale. Empty means the correction reports a pure knowledge gap. */ disputedMemoryIds?: string[]; evidence?: string[]; } export interface StructuralValidationSummary { valid: boolean; issues: InstitutionalValidationIssue[]; } export interface ReplayGateResult { passed: boolean; caseIds: string[]; failures: string[]; } export interface ReplayGate { run(candidate: CorrectionCandidate): Promise; } export type CandidateAuditEvent = "submitted" | "diagnosed" | "validated" | "validation_failed" | "replay_passed" | "replay_failed" | "approved" | "rejected" | "changes_requested" | "applied"; export interface CandidateAuditEntry { at: string; actor: string; event: CandidateAuditEvent; detail?: string; } export interface CandidateReviewerDecision { actor: string; decision: "approved" | "rejected" | "changes_requested"; reason?: string; at: string; } export interface CorrectionCandidate { readonly id: string; state: CandidateLifecycleState; correction: CorrectionInput; rootCause?: CorrectionRootCause; rootCauseReason?: string; affectedMemoryIds: string[]; mutation?: CandidateMutation; /** Institutional records that reference the mutation's target via #24's dependency fields (`dependsOnPositionIds`/`positionIds`/`procedureIds`) and would be impacted by applying it. */ impactedMemoryIds?: string[]; structuralValidation?: StructuralValidationSummary; replay?: ReplayGateResult; reviewerDecision?: CandidateReviewerDecision; appliedMemoryId?: string; audit: CandidateAuditEntry[]; readonly createdAt: string; updatedAt: string; /** * Monotonically incremented by every `touch()`. Exists solely for * optimistic-concurrency checks (`assertNotModifiedConcurrently`): * `updatedAt` alone is not reliable for that, since two writes within the * same millisecond produce an identical ISO timestamp and would silently * defeat a string-equality staleness check. */ revision: number; } export interface CorrectionDiagnosis { rootCause: CorrectionRootCause; reason: string; affectedMemoryIds: string[]; } /** * Classifies why a correction happened by checking whether the disputed memory * was present -- and applicable -- in the retrieval manifest recorded at the * time of the original response. This is the deterministic check the issue * requires to distinguish a knowledge gap (nothing existed) from a * procedure/routing fault (the right material existed but was not surfaced). */ export declare function diagnoseCorrection(correction: CorrectionInput, existing: InstitutionalMemory[]): CorrectionDiagnosis; /** * Produces the smallest mutation that addresses the diagnosed root cause. * Ambiguous corrections never produce a mutation -- they stay queued for a * human decision, per the issue's explicit requirement. */ export declare function proposeCandidateMutation(correction: CorrectionInput, diagnosis: CorrectionDiagnosis, existing: InstitutionalMemory[]): CandidateMutation | undefined; /** * Computes the #24 dependency-graph closure of records that reference the * mutation's target, so a reviewer can see blast radius before approving a * supersede/retire. `validateInstitutionalMemories` already rejects a * mutation that would leave a dangling reference; this exists for review * visibility into which records those references belong to, not safety. */ export declare function computeImpactedMemoryIds(mutation: CandidateMutation, existing: InstitutionalMemory[]): string[]; /** * Applies a candidate's mutation to an institutional corpus in memory, * without persisting anything -- used both by structural validation (check * the resulting set) and by a replay gate (run scenarios against the * resulting set before approval). */ export declare function applyMutationToInstitutionalSet(mutation: CandidateMutation, existing: InstitutionalWrite[]): InstitutionalWrite[]; export declare function validateCandidateStructure(mutation: CandidateMutation, correction: CorrectionInput, existingInstitutional: InstitutionalWrite[]): StructuralValidationSummary; export interface ApplyMutationResult { memoryId: string; } /** * Applies an approved mutation to the owning provider and returns the * resulting memory id. Implementations are responsible for refreshing that * provider's retrieval state (e.g. calling `MemoryProvider.refresh()`) as * part of applying the mutation, so the approved change is immediately * visible to subsequent retrieval -- `CorrectionReviewQueue.approve` treats * this call as the single atomic apply step and does not refresh anything * itself. `context` is the correction's own context, so the implementation * can scope provider lookups the same way the original correction was. */ export type ApplyMutation = (mutation: CandidateMutation, context: MemoryContext) => Promise; /** * A loader for the current institutional corpus, scoped to `context`. May * be synchronous (an in-memory fixture) or asynchronous (a live provider * query) -- `CorrectionReviewQueue` awaits it either way. */ export type InstitutionalLoader = (context: MemoryContext) => T | Promise; /** * Durable storage for correction candidates. `update` is the sole mutation * entry point and must be atomic per id: it loads the current row, applies * `mutate` to it, and persists the result as one unit, so a concurrent * `update` on the same id either fully precedes or fully follows this one -- * never interleaves with it. A Postgres-backed implementation gets this for * free via `SELECT ... FOR UPDATE`; an in-memory implementation gets it for * free by keeping `mutate` synchronous, since JS never interleaves within a * single synchronous callback. `mutate` must therefore never itself await * anything -- all async work (loading the institutional corpus, running the * replay gate) happens before calling `update`, and `mutate` only re-checks * the freshest state and applies an already-computed transition. */ export interface CorrectionCandidateStore { insert(candidate: CorrectionCandidate): Promise; get(candidateId: string): Promise; list(filter?: { state?: CandidateLifecycleState; }): Promise; update(candidateId: string, mutate: (candidate: CorrectionCandidate) => CorrectionCandidate): Promise; } /** * Caps on `CorrectionInput` free-text/array fields, enforced in `submit()`. * A tool schema (e.g. the OpenCode `memory_submit_correction` input schema) * is the first line of defense but not the only one: any caller that * bypasses or misconfigures that schema would otherwise be able to enqueue * unbounded strings/arrays into durable JSONB storage, where they are later * read back by diagnosis, replay, and every `list()`/`get()` caller. */ export declare const CORRECTION_INPUT_LIMITS: { readonly maxTextLength: 8000; readonly maxActorLength: 255; readonly maxDisputedMemoryIds: 100; readonly maxMemoryIdLength: 512; readonly maxPromptLength: 8000; readonly maxEvidenceEntries: 100; readonly maxEvidenceEntryLength: 2000; }; /** * In-process, non-durable `CorrectionCandidateStore`. Suitable for tests and * for hosts that have not configured persistent storage; state is lost on * process exit and is not visible to other processes. */ export declare class InMemoryCorrectionCandidateStore implements CorrectionCandidateStore { private readonly maxCandidates; private readonly candidates; constructor(maxCandidates?: number); insert(candidate: CorrectionCandidate): Promise; get(candidateId: string): Promise; list(filter?: { state?: CandidateLifecycleState; }): Promise; update(candidateId: string, mutate: (candidate: CorrectionCandidate) => CorrectionCandidate): Promise; /** Only removes a resolved (terminal) candidate, never one that is still under active review. */ private evictOldestTerminal; } export declare class CorrectionReviewQueue { private readonly store; private readonly loadInstitutional; private readonly loadInstitutionalWrites; private readonly applyMutation; private readonly replayGate; constructor(store: CorrectionCandidateStore, loadInstitutional: InstitutionalLoader, loadInstitutionalWrites: InstitutionalLoader, applyMutation: ApplyMutation, replayGate: ReplayGate); submit(correction: CorrectionInput): Promise; get(candidateId: string): Promise; list(filter?: { state?: CandidateLifecycleState; }): Promise; /** * Runs diagnosis, mutation proposal, structural validation, and the * replay gate. All of that work happens against a point-in-time read and * is not itself atomic with the write -- the final `store.update` re-runs * the terminal/locked-state check against the freshest row, so a * concurrent reject/approve that completed while this was computing * causes this call to abort instead of clobbering that decision. That * check alone is not enough for `needs_changes`/`requestChanges`, though: * revalidation is legitimately allowed to start FROM `needs_changes`, so a * `requestChanges()` call that lands while this computation is still in * flight would otherwise be silently overwritten by a finalize based on a * stale snapshot. `assertNotModifiedConcurrently` closes that gap by * requiring the row's `revision` to still match what it was when this * call started. */ runValidation(candidateId: string): Promise; reject(candidateId: string, actor: string, reason: string): Promise; requestChanges(candidateId: string, actor: string, reason: string): Promise; /** * Claims the candidate for approval (atomic; fails immediately if it is * not "validated"), applies its mutation outside the store lock since a * provider write may be slow, then finalizes to "applied". * * If `applyMutation` itself throws, the mutation never took effect, so the * claim is safely rolled back to "validated" and the candidate stays * retryable. If `applyMutation` *succeeds* but the finalize write fails * (e.g. a transient store error), the candidate is deliberately left in * "applying" rather than reverted -- reverting would make it retryable, * and retrying would call `applyMutation` a second time for a mutation * that already landed (this library has no idempotency key for * create/update/supersede). A candidate stuck this way must be resolved * with `recoverStuckApplying`, using the memory id reported in the thrown * error. */ approve(candidateId: string, actor: string): Promise; /** * Manually resolves a candidate stuck in "applying" -- the only way that * can happen is a crash or a finalize failure between `approve()` calling * `applyMutation` and recording the result (see `approve`'s doc comment). * `outcome: "validated"` means a human confirmed the mutation never took * effect, so the candidate is retryable via `approve()` again. * `outcome: "applied"` means a human confirmed (e.g. from provider state * or logs) that it did land, so this records that outcome without * re-applying it. */ recoverStuckApplying(candidateId: string, actor: string, outcome: "validated" | "applied", reason: string, appliedMemoryId?: string): Promise; }