/** * ADR-111 miner slice 5b-i — the deterministic LLM record/replay SCAFFOLD. * * The certifying run (slice 5) introduces the miner's first NON-deterministic, * LLM-backed components (the live `DraftExtractor` / `DraftClassifier` adapters, * slice 5b-ii). The cohort panel's central answer (consolidated fold A, 4/4) is * that those live outputs must be FROZEN into a recorded replay artifact so the * scorer + falsification harness can re-run the certifying experiment ZERO-LLM, * byte-deterministically, and auditably — "arguably contract-MANDATED" by §6 * (fail-loud, no degrade) + §8 (every decision ledgered) + Tenet-15. * * THIS file is the deterministic record/replay scaffold proven in isolation with * a STUB orchestrator: NO live LLM, NO network, NO prompts (those are slice * 5b-ii, which populates the provenance block from the real adapter). It defines: * * - the `llm-replay.v1` Zod artifact (two typed record sections + a provenance * block), CLI-side — the replay artifact is a CLI-layer concern; core stays * unaware of it; * - the deterministic `inputKey` digests for the extractor + classifier ports * (mirroring core's `deriveClaimId` — `sha256("" + canonicalJson(...))` * over IDENTITY fields only); * - record/replay decorators (generic over the port) that wrap the core ports; * - the EXTERNAL-expected-hash integrity gate (fold B — a self-hash the artifact * validates against itself is circular; a content+hash co-rewrite would pass). * * Determinism discipline: this module is `new Date()` / `Math.random()` -free. A * recorded `[]` (extractor) or `{behavioral, error-default}` (classifier) is a * REAL row — present in the map, distinguishable from a missing key (a replay * MISS is a corpus-integrity failure, never a safe-default). * * Reuse (Tenet-21): the canonical key-sorted serializer (`canonicalStringify`), * the full-digest hash (`calculateDeterministicHash` shape), the `deriveClaimId` * identity-fields-only pattern, and the wind-tunnel's external-expected-hash * integrity discipline (`verifyControlIntegrity`) are all reused, not reinvented. */ import { z } from 'zod'; import type { ClassifierResult, DraftResult, ExtractStageResult, ReviewThreadContent } from '@mmnto/totem'; type DraftCandidate = ExtractStageResult['drafts'][number]; /** Discriminates the two record sections for the duplicate-key guard. */ export type AdapterKind = 'extractor' | 'classifier'; /** * A replay query hit an `inputKey` absent from the frozen records. This is a * CORPUS-INTEGRITY failure, never a recoverable per-PR condition: the frozen * experiment's premise is that every input the certifying run will ask for was * recorded. A miss means the corpus drifted out from under the replay (a new * input appeared, or a recorded one was dropped). Falling back to `[]` / * `{behavioral, error-default}` would absorb that drift silently and let the * certifying verdict diverge from the frozen one undetected — so we throw. */ export declare class ReplayMissError extends Error { readonly adapterKind: AdapterKind; readonly inputKey: string; constructor(adapterKind: AdapterKind, inputKey: string); } /** * The loaded replay fixture's content-hash does not match the EXTERNAL expected * hash injected at construction (fold B). The expected hash is supplied by the * caller (5c sources it from a committed lock), NOT embedded in the artifact — * an embedded self-hash would be circular (a content+hash co-rewrite passes its * own check). A mismatch means the fixture was tampered with or drifted; the * replay must NOT proceed, so we throw AT CONSTRUCTION (before any query). */ export declare class FixtureIntegrityError extends Error { readonly expectedHash: string; readonly actualHash: string; constructor(expectedHash: string, actualHash: string); } /** * Recording the same `(adapterKind, inputKey)` twice with (potentially) * different outputs. The record sink is APPEND-ONCE: a duplicate is never * last-write-wins (that would silently launder a non-deterministic adapter's * second answer over its first). The recording run must be deterministic, so a * duplicate key is a producer bug → throw. */ export declare class DuplicateRecordError extends Error { readonly adapterKind: AdapterKind; readonly inputKey: string; constructor(adapterKind: AdapterKind, inputKey: string); } /** * Deterministic extractor inputKey (fold D). `sha256(canonicalJson({ keyVersion, * pr, mergeCommitSha, threads: }))`. MUST include * `mergeCommitSha` (provenance identity). The eligible set is normalized so * provider thread/comment order can't change the key; resolved/outdated threads * are excluded (the port never sees them). Mirrors `deriveClaimId`: the version * is BOTH a payload field and a digest-input prefix. */ export declare function extractorInputKey(content: ReviewThreadContent): string; /** * Deterministic classifier inputKey (fold D). `sha256(canonicalJson({ * keyVersion, provenance, dslSource, draftRef }))`. The classifier does NOT * dedupe drafts — TWO drafts from the SAME provenance must not collide to one * key — so `draftRef` (a stable ordinal/ref the caller supplies, e.g. the slice-3 * per-(pr, ordinal) candidate ref) disambiguates them. Without it, N drafts with * an identical body from one PR would map to one record and lose N-1 outputs. */ export declare function classifierInputKey(draft: DraftCandidate, draftRef: string): string; /** * Run-level provenance block. In 5b-i these fields are populated by the (stub) * caller — the SCHEMA and the integrity gate covering them are what this slice * builds; 5b-ii populates them from the live adapter. They pin the exact frozen * experiment (prompt + model + adapter + key version + tool version) so a replay * is reproducible and a provenance drift is detectable. Provenance lives OUTSIDE * the records map (the records block is strictly `inputKey → output`). */ export declare const ReplayProvenanceSchema: z.ZodObject<{ /** sha256 of the frozen draft/classify prompt TEMPLATE (the decaying-prompt pin). */ promptTemplateHash: z.ZodString; /** sha256 of the frozen system prompt. */ systemPromptHash: z.ZodString; /** LLM provider id (e.g. `anthropic` / `gemini` / `openai`). */ provider: z.ZodString; /** Model id (e.g. a pinned model snapshot). */ model: z.ZodString; /** Decode temperature the frozen outputs were produced at. */ temperature: z.ZodNumber; /** The orchestrator build the live adapter ran under. */ orchestratorVersion: z.ZodString; /** Which port adapter produced these records (`extractor` / `classifier` / a combined run). */ adapterKind: z.ZodString; /** The inputKey schema version (so a key-shape change is recorded, not silent). */ keyVersion: z.ZodString; /** The totem/CLI version that froze the artifact. */ totemVersion: z.ZodString; }, "strip", z.ZodTypeAny, { model: string; temperature: number; provider: string; promptTemplateHash: string; systemPromptHash: string; orchestratorVersion: string; adapterKind: string; keyVersion: string; totemVersion: string; }, { model: string; temperature: number; provider: string; promptTemplateHash: string; systemPromptHash: string; orchestratorVersion: string; adapterKind: string; keyVersion: string; totemVersion: string; }>; export type ReplayProvenance = z.infer; /** * The records block: STRICTLY `inputKey → the port's raw return value`. A * recorded `[]` (extractor) / `{behavioral, error-default}` (classifier) is a * REAL row, distinct from a missing key. NEVER write `durationMs` / `recordedAt` * / local-user / run-id into a record — those are non-deterministic / identifying * metadata that would corrupt the content-hash and break the frozen-experiment * premise. The maps are plain `Record`; record keys are * written SORTED by the canonical serializer (clean git diffs). */ /** * CLI-side validation of a recorded `ClassifierResult` (GCA #2209 + the cohort * panel's "replay artifact schema is a CLI-layer concern", gemini): a LOCAL Zod * schema rather than a static runtime import of core's `ClassifierResultSchema` * (which would pull the heavy `@mmnto/totem` barrel onto the CLI-startup path). * Mirrors core's shape AND its refinement (`error-default` ⟹ `behavioral`, the * low-privilege safe-default) so a recorded `{structural, error-default}` is * rejected. A test asserts parity with core, so this duplication can't silently * drift if core's `ClassifierResult` changes. */ export declare const ClassifierResultLocalSchema: z.ZodEffects; dispositionSource: z.ZodEnum<["classified", "error-default"]>; }, "strip", z.ZodTypeAny, { disposition: "structural" | "behavioral"; dispositionSource: "classified" | "error-default"; }, { disposition: "structural" | "behavioral"; dispositionSource: "classified" | "error-default"; }>, { disposition: "structural" | "behavioral"; dispositionSource: "classified" | "error-default"; }, { disposition: "structural" | "behavioral"; dispositionSource: "classified" | "error-default"; }>; /** * CLI-side validation of a recorded `DraftResult` (GCA #2209 + the panel's "replay * artifact schema is a CLI-layer concern"): a LOCAL Zod schema rather than a static * runtime import of core's `DraftResultSchema` (which would pull the heavy * `@mmnto/totem` barrel onto the CLI-startup path). Mirrors core's shape AND its * "cause iff empty" refinement LOGIC (the `NoDraftCauseSchema` enum, including the * replay-migration-only `legacy-unknown`). Parity is accept/reject EQUIVALENCE, * locked by a test — the local ZodError `message`/`path` stay intentionally concise * (matching the `ClassifierResultLocalSchema` sibling), not byte-identical to core. */ export declare const DraftResultLocalSchema: z.ZodEffects; noDraftCause: z.ZodOptional>; }, "strip", z.ZodTypeAny, { drafts: string[]; noDraftCause?: "legacy-unknown" | "invoke-error" | "empty-output" | "none-sentinel" | "unparseable-shape" | "non-array" | "all-filtered" | undefined; }, { drafts: string[]; noDraftCause?: "legacy-unknown" | "invoke-error" | "empty-output" | "none-sentinel" | "unparseable-shape" | "non-array" | "all-filtered" | undefined; }>, { drafts: string[]; noDraftCause?: "legacy-unknown" | "invoke-error" | "empty-output" | "none-sentinel" | "unparseable-shape" | "non-array" | "all-filtered" | undefined; }, { drafts: string[]; noDraftCause?: "legacy-unknown" | "invoke-error" | "empty-output" | "none-sentinel" | "unparseable-shape" | "non-array" | "all-filtered" | undefined; }>; export declare const ReplayRecordsSchema: z.ZodObject<{ /** * `inputKey → DraftExtractor.draft()` return — a `DraftResult` (`{drafts, noDraftCause?}`); * a recorded `{drafts:[], noDraftCause}` is a real row. BACKWARD-COMPAT (the α * cause-tag migration): a legacy fixture stored a bare `string[]` (pre-cause-tag), * accepted via the union so the committed cert-#1 fixture still parses + hashes * IDENTICALLY (the bare-array branch returns it unchanged). `ReplayDraftExtractor` * normalizes a legacy row to a `DraftResult` on read (empty → `legacy-unknown`); * fresh recordings always write the object form. */ extractor: z.ZodRecord, z.ZodEffects; noDraftCause: z.ZodOptional>; }, "strip", z.ZodTypeAny, { drafts: string[]; noDraftCause?: "legacy-unknown" | "invoke-error" | "empty-output" | "none-sentinel" | "unparseable-shape" | "non-array" | "all-filtered" | undefined; }, { drafts: string[]; noDraftCause?: "legacy-unknown" | "invoke-error" | "empty-output" | "none-sentinel" | "unparseable-shape" | "non-array" | "all-filtered" | undefined; }>, { drafts: string[]; noDraftCause?: "legacy-unknown" | "invoke-error" | "empty-output" | "none-sentinel" | "unparseable-shape" | "non-array" | "all-filtered" | undefined; }, { drafts: string[]; noDraftCause?: "legacy-unknown" | "invoke-error" | "empty-output" | "none-sentinel" | "unparseable-shape" | "non-array" | "all-filtered" | undefined; }>]>>; /** `inputKey → DraftClassifier.classify()` return (a `ClassifierResult`). */ classifier: z.ZodRecord; dispositionSource: z.ZodEnum<["classified", "error-default"]>; }, "strip", z.ZodTypeAny, { disposition: "structural" | "behavioral"; dispositionSource: "classified" | "error-default"; }, { disposition: "structural" | "behavioral"; dispositionSource: "classified" | "error-default"; }>, { disposition: "structural" | "behavioral"; dispositionSource: "classified" | "error-default"; }, { disposition: "structural" | "behavioral"; dispositionSource: "classified" | "error-default"; }>>; }, "strip", z.ZodTypeAny, { extractor: Record; classifier: Record; }, { extractor: Record; classifier: Record; }>; export type ReplayRecords = z.infer; /** Stable artifact-format tag — bumped if the envelope shape changes. */ export declare const REPLAY_ARTIFACT_KIND = "llm-replay.v1"; /** The full `llm-replay.v1` artifact: format tag + provenance + the two record sections. */ export declare const ReplayArtifactSchema: z.ZodObject<{ kind: z.ZodLiteral<"llm-replay.v1">; provenance: z.ZodObject<{ /** sha256 of the frozen draft/classify prompt TEMPLATE (the decaying-prompt pin). */ promptTemplateHash: z.ZodString; /** sha256 of the frozen system prompt. */ systemPromptHash: z.ZodString; /** LLM provider id (e.g. `anthropic` / `gemini` / `openai`). */ provider: z.ZodString; /** Model id (e.g. a pinned model snapshot). */ model: z.ZodString; /** Decode temperature the frozen outputs were produced at. */ temperature: z.ZodNumber; /** The orchestrator build the live adapter ran under. */ orchestratorVersion: z.ZodString; /** Which port adapter produced these records (`extractor` / `classifier` / a combined run). */ adapterKind: z.ZodString; /** The inputKey schema version (so a key-shape change is recorded, not silent). */ keyVersion: z.ZodString; /** The totem/CLI version that froze the artifact. */ totemVersion: z.ZodString; }, "strip", z.ZodTypeAny, { model: string; temperature: number; provider: string; promptTemplateHash: string; systemPromptHash: string; orchestratorVersion: string; adapterKind: string; keyVersion: string; totemVersion: string; }, { model: string; temperature: number; provider: string; promptTemplateHash: string; systemPromptHash: string; orchestratorVersion: string; adapterKind: string; keyVersion: string; totemVersion: string; }>; records: z.ZodObject<{ /** * `inputKey → DraftExtractor.draft()` return — a `DraftResult` (`{drafts, noDraftCause?}`); * a recorded `{drafts:[], noDraftCause}` is a real row. BACKWARD-COMPAT (the α * cause-tag migration): a legacy fixture stored a bare `string[]` (pre-cause-tag), * accepted via the union so the committed cert-#1 fixture still parses + hashes * IDENTICALLY (the bare-array branch returns it unchanged). `ReplayDraftExtractor` * normalizes a legacy row to a `DraftResult` on read (empty → `legacy-unknown`); * fresh recordings always write the object form. */ extractor: z.ZodRecord, z.ZodEffects; noDraftCause: z.ZodOptional>; }, "strip", z.ZodTypeAny, { drafts: string[]; noDraftCause?: "legacy-unknown" | "invoke-error" | "empty-output" | "none-sentinel" | "unparseable-shape" | "non-array" | "all-filtered" | undefined; }, { drafts: string[]; noDraftCause?: "legacy-unknown" | "invoke-error" | "empty-output" | "none-sentinel" | "unparseable-shape" | "non-array" | "all-filtered" | undefined; }>, { drafts: string[]; noDraftCause?: "legacy-unknown" | "invoke-error" | "empty-output" | "none-sentinel" | "unparseable-shape" | "non-array" | "all-filtered" | undefined; }, { drafts: string[]; noDraftCause?: "legacy-unknown" | "invoke-error" | "empty-output" | "none-sentinel" | "unparseable-shape" | "non-array" | "all-filtered" | undefined; }>]>>; /** `inputKey → DraftClassifier.classify()` return (a `ClassifierResult`). */ classifier: z.ZodRecord; dispositionSource: z.ZodEnum<["classified", "error-default"]>; }, "strip", z.ZodTypeAny, { disposition: "structural" | "behavioral"; dispositionSource: "classified" | "error-default"; }, { disposition: "structural" | "behavioral"; dispositionSource: "classified" | "error-default"; }>, { disposition: "structural" | "behavioral"; dispositionSource: "classified" | "error-default"; }, { disposition: "structural" | "behavioral"; dispositionSource: "classified" | "error-default"; }>>; }, "strip", z.ZodTypeAny, { extractor: Record; classifier: Record; }, { extractor: Record; classifier: Record; }>; }, "strip", z.ZodTypeAny, { kind: "llm-replay.v1"; provenance: { model: string; temperature: number; provider: string; promptTemplateHash: string; systemPromptHash: string; orchestratorVersion: string; adapterKind: string; keyVersion: string; totemVersion: string; }; records: { extractor: Record; classifier: Record; }; }, { kind: "llm-replay.v1"; provenance: { model: string; temperature: number; provider: string; promptTemplateHash: string; systemPromptHash: string; orchestratorVersion: string; adapterKind: string; keyVersion: string; totemVersion: string; }; records: { extractor: Record; classifier: Record; }; }>; export type ReplayArtifact = z.infer; /** * Serialize an artifact to canonical, key-SORTED JSON, PRETTY-printed for a * committable artifact + clean diffs. Record keys land sorted because the * canonicalizer recursively sorts object keys — so re-freezing in a different * insertion order is a no-op diff. * * NOTE (greptile/CR #2209): the content-hash (`computeArtifactHash`) is computed * over the MINIFIED canonical form (`canonicalJson`), NOT these pretty-printed * bytes. Both run through the same `canonicalize` (so they never drift on key * order), but they are NOT byte-identical — to verify integrity always call * `computeArtifactHash(loadedArtifact)`; never `sha256` the raw file bytes. */ export declare function serializeReplayArtifact(artifact: ReplayArtifact): string; /** * The artifact CONTENT-HASH (fold B + fold F): sha256 over the canonically- * serialized WHOLE artifact (kind + provenance + records). Deterministic + * git-independent (the pure replay path has no git dependency — contrast the * wind-tunnel's `git hash-object`, which 5c may switch to for wind-tunnel * consistency, out of scope here). Hashing the WHOLE artifact (not records-only) * means the integrity gate ALSO COVERS the provenance block — so a prompt / model * / key-version edit (e.g. `promptTemplateHash`) WITHOUT a re-record trips the * gate (fold F: a prompt change must force a re-record, never silently serve * stale outputs under a changed prompt). The expected hash is EXTERNAL (caller- * injected; 5c sources it from a committed lock) — never embedded (a self-hash * the artifact validates against itself is circular: a content+hash co-rewrite * would pass its own check). */ export declare function computeArtifactHash(artifact: ReplayArtifact): string; /** * The append-once record sink the `Recording*` decorators write into. Holds the * in-progress records map; `freeze()` produces the immutable artifact. A * duplicate `(adapterKind, inputKey)` throws `DuplicateRecordError` — never * last-write-wins (a second output for the same input is a non-determinism leak * the recording run must surface, not absorb). */ export declare class ReplayRecordSink { private readonly extractor; private readonly classifier; recordExtractor(inputKey: string, output: DraftResult): void; recordClassifier(inputKey: string, output: ClassifierResult): void; /** Snapshot the records as a plain (Zod-validated) object — keys land sorted on serialize. */ records(): ReplayRecords; /** Assemble the full `llm-replay.v1` artifact from the recorded sink + a provenance block. */ freeze(provenance: ReplayProvenance): ReplayArtifact; } /** * Records every `DraftExtractor.draft()` call's inputKey → raw `DraftResult` output * into the sink, then passes the value THROUGH unchanged. The recorded value is the * PORT's return (pre-core-funnel) — a recorded `{drafts:[], noDraftCause}` is a real * row that freezes the NO-DRAFT cause for the replay. A duplicate inputKey throws * (via the sink). */ export declare class RecordingDraftExtractor { private readonly wrapped; private readonly sink; constructor(wrapped: { draft(content: ReviewThreadContent): Promise; }, sink: ReplayRecordSink); draft(content: ReviewThreadContent): Promise; } /** * Records every `DraftClassifier.classify()` call's inputKey → raw * `ClassifierResult` output into the sink, then passes it through unchanged. The * caller supplies a `draftRef` (the disambiguator for multiple drafts from one * provenance — fold D). A duplicate inputKey throws (via the sink). * * `classify` does not take a `draftRef` in the core port signature, so the * decorator binds it via a per-draft ref RESOLVER the caller supplies (e.g. the * slice-3 per-(pr, ordinal) candidate ref). The resolver is pure + deterministic. */ export declare class RecordingDraftClassifier { private readonly wrapped; private readonly sink; private readonly draftRef; constructor(wrapped: { classify(draft: DraftCandidate): Promise; }, sink: ReplayRecordSink, draftRef: (draft: DraftCandidate) => string); classify(draft: DraftCandidate): Promise; } /** * PURE replay of `DraftExtractor` — zero live LLM / network calls. Computes the * inputKey for the requested content, looks it up in the frozen records: * - HIT → return the recorded `DraftResult` (a recorded empty result is a real * row; a legacy bare-`string[]` row is normalized to a `DraftResult`); * - MISS → throw `ReplayMissError` (NEVER fall back to an empty result). * * Integrity (fold B + F): the constructor takes the EXTERNAL expected content- * hash, computes the actual WHOLE-ARTIFACT hash (records + provenance), and throws * `FixtureIntegrityError` AT CONSTRUCTION on mismatch — so a tampered/drifted * fixture (records OR a prompt-provenance edit) can never serve a single replay. */ export declare class ReplayDraftExtractor { private readonly records; constructor(fixture: ReplayArtifact, expectedHash: string); draft(content: ReviewThreadContent): Promise; } /** * PURE replay of `DraftClassifier` — zero live calls. Computes the inputKey * (using the caller's `draftRef` resolver), looks it up: * - HIT → return the recorded `ClassifierResult` (including a recorded * `{behavioral, error-default}` — a real row); * - MISS → throw `ReplayMissError` (NEVER fall back to the safe-default). * * Same external-expected-hash integrity gate at construction (fold B). */ export declare class ReplayDraftClassifier { private readonly records; private readonly draftRef; constructor(fixture: ReplayArtifact, expectedHash: string, draftRef: (draft: DraftCandidate) => string); classify(draft: DraftCandidate): Promise; } export {}; //# sourceMappingURL=spine-llm-replay.d.ts.map