@mmnto/cli 1.68.0 → 1.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,492 @@
1
+ /**
2
+ * ADR-111 miner slice 5b-i — the deterministic LLM record/replay SCAFFOLD.
3
+ *
4
+ * The certifying run (slice 5) introduces the miner's first NON-deterministic,
5
+ * LLM-backed components (the live `DraftExtractor` / `DraftClassifier` adapters,
6
+ * slice 5b-ii). The cohort panel's central answer (consolidated fold A, 4/4) is
7
+ * that those live outputs must be FROZEN into a recorded replay artifact so the
8
+ * scorer + falsification harness can re-run the certifying experiment ZERO-LLM,
9
+ * byte-deterministically, and auditably — "arguably contract-MANDATED" by §6
10
+ * (fail-loud, no degrade) + §8 (every decision ledgered) + Tenet-15.
11
+ *
12
+ * THIS file is the deterministic record/replay scaffold proven in isolation with
13
+ * a STUB orchestrator: NO live LLM, NO network, NO prompts (those are slice
14
+ * 5b-ii, which populates the provenance block from the real adapter). It defines:
15
+ *
16
+ * - the `llm-replay.v1` Zod artifact (two typed record sections + a provenance
17
+ * block), CLI-side — the replay artifact is a CLI-layer concern; core stays
18
+ * unaware of it;
19
+ * - the deterministic `inputKey` digests for the extractor + classifier ports
20
+ * (mirroring core's `deriveClaimId` — `sha256("<prefix>" + canonicalJson(...))`
21
+ * over IDENTITY fields only);
22
+ * - record/replay decorators (generic over the port) that wrap the core ports;
23
+ * - the EXTERNAL-expected-hash integrity gate (fold B — a self-hash the artifact
24
+ * validates against itself is circular; a content+hash co-rewrite would pass).
25
+ *
26
+ * Determinism discipline: this module is `new Date()` / `Math.random()` -free. A
27
+ * recorded `[]` (extractor) or `{behavioral, error-default}` (classifier) is a
28
+ * REAL row — present in the map, distinguishable from a missing key (a replay
29
+ * MISS is a corpus-integrity failure, never a safe-default).
30
+ *
31
+ * Reuse (Tenet-21): the canonical key-sorted serializer (`canonicalStringify`),
32
+ * the full-digest hash (`calculateDeterministicHash` shape), the `deriveClaimId`
33
+ * identity-fields-only pattern, and the wind-tunnel's external-expected-hash
34
+ * integrity discipline (`verifyControlIntegrity`) are all reused, not reinvented.
35
+ */
36
+ import { createHash } from 'node:crypto';
37
+ import { z } from 'zod';
38
+ // ─── Named constants ─────────────────────────────────
39
+ /**
40
+ * inputKey version prefixes (mirrors core's `CLAIM_ID_VERSION` discipline). The
41
+ * version is folded INTO the canonical identity payload (`keyVersion` field) AND
42
+ * prepended to the digest input, so the key space is partitioned by both port
43
+ * kind and schema version — a future identity-shape change re-keys cleanly
44
+ * instead of silently colliding with v1 keys.
45
+ */
46
+ const EXTRACTOR_KEY_VERSION = 'extractor:v1';
47
+ const CLASSIFIER_KEY_VERSION = 'classifier:v1';
48
+ // ─── Errors (CLI-layer; loud-by-construction) ────────
49
+ /**
50
+ * A replay query hit an `inputKey` absent from the frozen records. This is a
51
+ * CORPUS-INTEGRITY failure, never a recoverable per-PR condition: the frozen
52
+ * experiment's premise is that every input the certifying run will ask for was
53
+ * recorded. A miss means the corpus drifted out from under the replay (a new
54
+ * input appeared, or a recorded one was dropped). Falling back to `[]` /
55
+ * `{behavioral, error-default}` would absorb that drift silently and let the
56
+ * certifying verdict diverge from the frozen one undetected — so we throw.
57
+ */
58
+ export class ReplayMissError extends Error {
59
+ adapterKind;
60
+ inputKey;
61
+ constructor(adapterKind, inputKey) {
62
+ super(`${adapterKind} replay MISS: inputKey ${inputKey} is absent from the frozen records. ` +
63
+ `A miss is a corpus-integrity failure (the frozen-experiment premise is broken), never a safe-default — ` +
64
+ `re-record the replay fixture from the live adapter so it covers this input.`);
65
+ this.name = 'ReplayMissError';
66
+ this.adapterKind = adapterKind;
67
+ this.inputKey = inputKey;
68
+ }
69
+ }
70
+ /**
71
+ * The loaded replay fixture's content-hash does not match the EXTERNAL expected
72
+ * hash injected at construction (fold B). The expected hash is supplied by the
73
+ * caller (5c sources it from a committed lock), NOT embedded in the artifact —
74
+ * an embedded self-hash would be circular (a content+hash co-rewrite passes its
75
+ * own check). A mismatch means the fixture was tampered with or drifted; the
76
+ * replay must NOT proceed, so we throw AT CONSTRUCTION (before any query).
77
+ */
78
+ export class FixtureIntegrityError extends Error {
79
+ expectedHash;
80
+ actualHash;
81
+ constructor(expectedHash, actualHash) {
82
+ super(`replay fixture integrity check failed — expected ${expectedHash} got ${actualHash}. ` +
83
+ `The frozen LLM record/replay artifact was altered (tampered or drifted). Revert the change or ` +
84
+ `re-freeze the lock with the updated content-hash.`);
85
+ this.name = 'FixtureIntegrityError';
86
+ this.expectedHash = expectedHash;
87
+ this.actualHash = actualHash;
88
+ }
89
+ }
90
+ /**
91
+ * Recording the same `(adapterKind, inputKey)` twice with (potentially)
92
+ * different outputs. The record sink is APPEND-ONCE: a duplicate is never
93
+ * last-write-wins (that would silently launder a non-deterministic adapter's
94
+ * second answer over its first). The recording run must be deterministic, so a
95
+ * duplicate key is a producer bug → throw.
96
+ */
97
+ export class DuplicateRecordError extends Error {
98
+ adapterKind;
99
+ inputKey;
100
+ constructor(adapterKind, inputKey) {
101
+ super(`duplicate record for ${adapterKind} inputKey ${inputKey} — the record sink is append-once ` +
102
+ `(never last-write-wins). A second output for the same input is a non-determinism leak in the recording run.`);
103
+ this.name = 'DuplicateRecordError';
104
+ this.adapterKind = adapterKind;
105
+ this.inputKey = inputKey;
106
+ }
107
+ }
108
+ // ─── Canonical serialization (reused, not reinvented) ─
109
+ /**
110
+ * Recursively rebuild a value with object keys in SORTED order so the subsequent
111
+ * stringify is canonical (mirrors core's `canonicalize` in `artifacts/hash.ts`).
112
+ * Arrays keep element order (a reordered array is a different payload). Used for
113
+ * BOTH the inputKey digest payloads and the records-block content-hash, so the
114
+ * pure replay path has no git / IO dependency.
115
+ */
116
+ function canonicalize(value) {
117
+ if (Array.isArray(value))
118
+ return value.map(canonicalize);
119
+ if (typeof value === 'object' && value !== null) {
120
+ const sorted = {};
121
+ for (const key of Object.keys(value).sort()) {
122
+ sorted[key] = canonicalize(value[key]);
123
+ }
124
+ return sorted;
125
+ }
126
+ return value;
127
+ }
128
+ /** Canonical (recursively key-sorted) minified JSON serialization of `payload`. */
129
+ function canonicalJson(payload) {
130
+ return JSON.stringify(canonicalize(payload));
131
+ }
132
+ /** Full sha256 hex (64 chars) — the digest IS an identity, never a truncation. */
133
+ function sha256Hex(input) {
134
+ return createHash('sha256').update(input, 'utf-8').digest('hex');
135
+ }
136
+ // ─── inputKey derivation (fold D — the `deriveClaimId` pattern) ───────────────
137
+ /**
138
+ * Normalize a single review thread to its STABLE identity: sort comments by
139
+ * (body, author) and carry only the resolution flags + path. Provider/array
140
+ * order must not change the key, so comments are sorted by a stable tuple before
141
+ * hashing. Resolved/outdated flags ARE part of the identity (the eligible-thread
142
+ * set the extractor was actually handed depends on them).
143
+ */
144
+ function normalizeThread(thread) {
145
+ const comments = [...thread.comments].sort((a, b) => a.body !== b.body
146
+ ? a.body < b.body
147
+ ? -1
148
+ : 1
149
+ : a.author < b.author
150
+ ? -1
151
+ : a.author > b.author
152
+ ? 1
153
+ : 0);
154
+ return {
155
+ path: thread.path,
156
+ isResolved: thread.isResolved,
157
+ isOutdated: thread.isOutdated,
158
+ comments,
159
+ };
160
+ }
161
+ /**
162
+ * Normalize the eligible thread set: per-thread comment-normalize, then sort by the
163
+ * FULL canonical JSON of each normalized thread. Sorting by `(path, first-comment)`
164
+ * was NOT a total order — two threads on the SAME path sharing a first comment but
165
+ * differing in LATER comments compared equal, so their provider array order leaked
166
+ * into the canonical payload and the same logical input keyed differently (greptile
167
+ * P1 + CR, #2209; a PR commonly has multiple threads on one file). Sorting by the
168
+ * whole canonical thread IS a total order: two threads compare equal only when
169
+ * byte-identical, so neither thread nor comment order from the provider can shift
170
+ * the key. Stripped of any non-deterministic field — `ReviewThread` carries only
171
+ * `path`, resolution flags, and `{author, body}` comments, all stable.
172
+ */
173
+ function normalizeThreads(threads) {
174
+ return threads.map(normalizeThread).sort((a, b) => {
175
+ const ka = canonicalJson(a);
176
+ const kb = canonicalJson(b);
177
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
178
+ });
179
+ }
180
+ /**
181
+ * Filter to the EXACT eligible set the extractor is handed: non-resolved,
182
+ * non-outdated threads (mirrors core's `eligibleThreads`). The inputKey is a
183
+ * function of what the port ACTUALLY saw, so eligibility must be applied before
184
+ * normalizing — two contents that differ only in resolved/outdated threads
185
+ * (which the extractor never sees) still key identically.
186
+ */
187
+ function eligibleThreads(threads) {
188
+ return threads.filter((t) => !t.isResolved && !t.isOutdated);
189
+ }
190
+ /**
191
+ * Deterministic extractor inputKey (fold D). `sha256(canonicalJson({ keyVersion,
192
+ * pr, mergeCommitSha, threads: <normalized eligible threads> }))`. MUST include
193
+ * `mergeCommitSha` (provenance identity). The eligible set is normalized so
194
+ * provider thread/comment order can't change the key; resolved/outdated threads
195
+ * are excluded (the port never sees them). Mirrors `deriveClaimId`: the version
196
+ * is BOTH a payload field and a digest-input prefix.
197
+ */
198
+ export function extractorInputKey(content) {
199
+ const payload = {
200
+ keyVersion: EXTRACTOR_KEY_VERSION,
201
+ pr: content.pr,
202
+ mergeCommitSha: content.mergeCommitSha,
203
+ threads: normalizeThreads(eligibleThreads(content.threads)),
204
+ };
205
+ return sha256Hex(`${EXTRACTOR_KEY_VERSION}${canonicalJson(payload)}`);
206
+ }
207
+ /**
208
+ * Deterministic classifier inputKey (fold D). `sha256(canonicalJson({
209
+ * keyVersion, provenance, dslSource, draftRef }))`. The classifier does NOT
210
+ * dedupe drafts — TWO drafts from the SAME provenance must not collide to one
211
+ * key — so `draftRef` (a stable ordinal/ref the caller supplies, e.g. the slice-3
212
+ * per-(pr, ordinal) candidate ref) disambiguates them. Without it, N drafts with
213
+ * an identical body from one PR would map to one record and lose N-1 outputs.
214
+ */
215
+ export function classifierInputKey(draft, draftRef) {
216
+ const payload = {
217
+ keyVersion: CLASSIFIER_KEY_VERSION,
218
+ provenance: draft.provenance,
219
+ dslSource: draft.dslSource,
220
+ draftRef,
221
+ };
222
+ return sha256Hex(`${CLASSIFIER_KEY_VERSION}${canonicalJson(payload)}`);
223
+ }
224
+ // ─── The `llm-replay.v1` artifact (Zod, CLI-side) ─────
225
+ /**
226
+ * Run-level provenance block. In 5b-i these fields are populated by the (stub)
227
+ * caller — the SCHEMA and the integrity gate covering them are what this slice
228
+ * builds; 5b-ii populates them from the live adapter. They pin the exact frozen
229
+ * experiment (prompt + model + adapter + key version + tool version) so a replay
230
+ * is reproducible and a provenance drift is detectable. Provenance lives OUTSIDE
231
+ * the records map (the records block is strictly `inputKey → output`).
232
+ */
233
+ export const ReplayProvenanceSchema = z.object({
234
+ /** sha256 of the frozen draft/classify prompt TEMPLATE (the decaying-prompt pin). */
235
+ promptTemplateHash: z.string(),
236
+ /** sha256 of the frozen system prompt. */
237
+ systemPromptHash: z.string(),
238
+ /** LLM provider id (e.g. `anthropic` / `gemini` / `openai`). */
239
+ provider: z.string(),
240
+ /** Model id (e.g. a pinned model snapshot). */
241
+ model: z.string(),
242
+ /** Decode temperature the frozen outputs were produced at. */
243
+ temperature: z.number(),
244
+ /** The orchestrator build the live adapter ran under. */
245
+ orchestratorVersion: z.string(),
246
+ /** Which port adapter produced these records (`extractor` / `classifier` / a combined run). */
247
+ adapterKind: z.string(),
248
+ /** The inputKey schema version (so a key-shape change is recorded, not silent). */
249
+ keyVersion: z.string(),
250
+ /** The totem/CLI version that froze the artifact. */
251
+ totemVersion: z.string(),
252
+ });
253
+ /**
254
+ * The records block: STRICTLY `inputKey → the port's raw return value`. A
255
+ * recorded `[]` (extractor) / `{behavioral, error-default}` (classifier) is a
256
+ * REAL row, distinct from a missing key. NEVER write `durationMs` / `recordedAt`
257
+ * / local-user / run-id into a record — those are non-deterministic / identifying
258
+ * metadata that would corrupt the content-hash and break the frozen-experiment
259
+ * premise. The maps are plain `Record<inputKey, output>`; record keys are
260
+ * written SORTED by the canonical serializer (clean git diffs).
261
+ */
262
+ /**
263
+ * CLI-side validation of a recorded `ClassifierResult` (GCA #2209 + the cohort
264
+ * panel's "replay artifact schema is a CLI-layer concern", gemini): a LOCAL Zod
265
+ * schema rather than a static runtime import of core's `ClassifierResultSchema`
266
+ * (which would pull the heavy `@mmnto/totem` barrel onto the CLI-startup path).
267
+ * Mirrors core's shape AND its refinement (`error-default` ⟹ `behavioral`, the
268
+ * low-privilege safe-default) so a recorded `{structural, error-default}` is
269
+ * rejected. A test asserts parity with core, so this duplication can't silently
270
+ * drift if core's `ClassifierResult` changes.
271
+ */
272
+ export const ClassifierResultLocalSchema = z
273
+ .object({
274
+ disposition: z.enum(['structural', 'behavioral']),
275
+ dispositionSource: z.enum(['classified', 'error-default']),
276
+ })
277
+ .refine((v) => v.dispositionSource !== 'error-default' || v.disposition === 'behavioral', {
278
+ message: "dispositionSource 'error-default' requires disposition 'behavioral'",
279
+ });
280
+ export const ReplayRecordsSchema = z.object({
281
+ /** `inputKey → DraftExtractor.draft()` return (a `string[]`; `[]` is a real row). */
282
+ extractor: z.record(z.array(z.string())),
283
+ /** `inputKey → DraftClassifier.classify()` return (a `ClassifierResult`). */
284
+ classifier: z.record(ClassifierResultLocalSchema),
285
+ });
286
+ /** Stable artifact-format tag — bumped if the envelope shape changes. */
287
+ export const REPLAY_ARTIFACT_KIND = 'llm-replay.v1';
288
+ /** The full `llm-replay.v1` artifact: format tag + provenance + the two record sections. */
289
+ export const ReplayArtifactSchema = z.object({
290
+ kind: z.literal(REPLAY_ARTIFACT_KIND),
291
+ provenance: ReplayProvenanceSchema,
292
+ records: ReplayRecordsSchema,
293
+ });
294
+ /**
295
+ * Serialize an artifact to canonical, key-SORTED JSON, PRETTY-printed for a
296
+ * committable artifact + clean diffs. Record keys land sorted because the
297
+ * canonicalizer recursively sorts object keys — so re-freezing in a different
298
+ * insertion order is a no-op diff.
299
+ *
300
+ * NOTE (greptile/CR #2209): the content-hash (`computeArtifactHash`) is computed
301
+ * over the MINIFIED canonical form (`canonicalJson`), NOT these pretty-printed
302
+ * bytes. Both run through the same `canonicalize` (so they never drift on key
303
+ * order), but they are NOT byte-identical — to verify integrity always call
304
+ * `computeArtifactHash(loadedArtifact)`; never `sha256` the raw file bytes.
305
+ */
306
+ export function serializeReplayArtifact(artifact) {
307
+ return JSON.stringify(canonicalize(ReplayArtifactSchema.parse(artifact)), null, 2);
308
+ }
309
+ /**
310
+ * The artifact CONTENT-HASH (fold B + fold F): sha256 over the canonically-
311
+ * serialized WHOLE artifact (kind + provenance + records). Deterministic +
312
+ * git-independent (the pure replay path has no git dependency — contrast the
313
+ * wind-tunnel's `git hash-object`, which 5c may switch to for wind-tunnel
314
+ * consistency, out of scope here). Hashing the WHOLE artifact (not records-only)
315
+ * means the integrity gate ALSO COVERS the provenance block — so a prompt / model
316
+ * / key-version edit (e.g. `promptTemplateHash`) WITHOUT a re-record trips the
317
+ * gate (fold F: a prompt change must force a re-record, never silently serve
318
+ * stale outputs under a changed prompt). The expected hash is EXTERNAL (caller-
319
+ * injected; 5c sources it from a committed lock) — never embedded (a self-hash
320
+ * the artifact validates against itself is circular: a content+hash co-rewrite
321
+ * would pass its own check).
322
+ */
323
+ export function computeArtifactHash(artifact) {
324
+ return hashParsedArtifact(ReplayArtifactSchema.parse(artifact));
325
+ }
326
+ /**
327
+ * Hash an ALREADY-parsed artifact — no redundant re-parse (CR #2209). The Replay
328
+ * constructors parse the fixture once, then the integrity gate hashes that parsed
329
+ * value directly via this helper; `computeArtifactHash` (public) parses first for
330
+ * untrusted input. Both produce the same digest for a valid artifact.
331
+ */
332
+ function hashParsedArtifact(parsed) {
333
+ return sha256Hex(canonicalJson(parsed));
334
+ }
335
+ // ─── Record sink ─────────────────────────────────────
336
+ /**
337
+ * The append-once record sink the `Recording*` decorators write into. Holds the
338
+ * in-progress records map; `freeze()` produces the immutable artifact. A
339
+ * duplicate `(adapterKind, inputKey)` throws `DuplicateRecordError` — never
340
+ * last-write-wins (a second output for the same input is a non-determinism leak
341
+ * the recording run must surface, not absorb).
342
+ */
343
+ export class ReplayRecordSink {
344
+ extractor = new Map();
345
+ classifier = new Map();
346
+ recordExtractor(inputKey, output) {
347
+ if (this.extractor.has(inputKey))
348
+ throw new DuplicateRecordError('extractor', inputKey);
349
+ this.extractor.set(inputKey, output);
350
+ }
351
+ recordClassifier(inputKey, output) {
352
+ if (this.classifier.has(inputKey))
353
+ throw new DuplicateRecordError('classifier', inputKey);
354
+ this.classifier.set(inputKey, output);
355
+ }
356
+ /** Snapshot the records as a plain (Zod-validated) object — keys land sorted on serialize. */
357
+ records() {
358
+ return ReplayRecordsSchema.parse({
359
+ extractor: Object.fromEntries(this.extractor),
360
+ classifier: Object.fromEntries(this.classifier),
361
+ });
362
+ }
363
+ /** Assemble the full `llm-replay.v1` artifact from the recorded sink + a provenance block. */
364
+ freeze(provenance) {
365
+ return ReplayArtifactSchema.parse({
366
+ kind: REPLAY_ARTIFACT_KIND,
367
+ provenance,
368
+ records: this.records(),
369
+ });
370
+ }
371
+ }
372
+ // ─── Recording decorators (generic over the port) ─────
373
+ /**
374
+ * Records every `DraftExtractor.draft()` call's inputKey → raw `string[]` output
375
+ * into the sink, then passes the value THROUGH unchanged. The recorded value is
376
+ * the PORT's return (pre-core-funnel) — a recorded `[]` is a real row. A
377
+ * duplicate inputKey throws (via the sink).
378
+ */
379
+ export class RecordingDraftExtractor {
380
+ wrapped;
381
+ sink;
382
+ constructor(wrapped, sink) {
383
+ this.wrapped = wrapped;
384
+ this.sink = sink;
385
+ }
386
+ async draft(content) {
387
+ const output = await this.wrapped.draft(content);
388
+ this.sink.recordExtractor(extractorInputKey(content), output);
389
+ return output;
390
+ }
391
+ }
392
+ /**
393
+ * Records every `DraftClassifier.classify()` call's inputKey → raw
394
+ * `ClassifierResult` output into the sink, then passes it through unchanged. The
395
+ * caller supplies a `draftRef` (the disambiguator for multiple drafts from one
396
+ * provenance — fold D). A duplicate inputKey throws (via the sink).
397
+ *
398
+ * `classify` does not take a `draftRef` in the core port signature, so the
399
+ * decorator binds it via a per-draft ref RESOLVER the caller supplies (e.g. the
400
+ * slice-3 per-(pr, ordinal) candidate ref). The resolver is pure + deterministic.
401
+ */
402
+ export class RecordingDraftClassifier {
403
+ wrapped;
404
+ sink;
405
+ draftRef;
406
+ constructor(wrapped, sink, draftRef) {
407
+ this.wrapped = wrapped;
408
+ this.sink = sink;
409
+ this.draftRef = draftRef;
410
+ }
411
+ async classify(draft) {
412
+ const output = await this.wrapped.classify(draft);
413
+ this.sink.recordClassifier(classifierInputKey(draft, this.draftRef(draft)), output);
414
+ return output;
415
+ }
416
+ }
417
+ // ─── Replay decorators (PURE — zero live calls) ───────
418
+ /**
419
+ * Validate the loaded fixture's whole-artifact content-hash against the EXTERNAL
420
+ * expected hash (fold B + fold F — provenance is covered, so a prompt-hash edit
421
+ * trips the gate). Throws `FixtureIntegrityError` on mismatch. Shared by both
422
+ * replay decorators so the gate runs exactly once per construction with one
423
+ * implementation.
424
+ */
425
+ function assertFixtureIntegrity(artifact, expectedHash) {
426
+ // `artifact` is already Zod-parsed by the Replay constructor — hash it directly,
427
+ // no redundant re-parse (CR #2209; matters for high-volume 5c runs).
428
+ const actualHash = hashParsedArtifact(artifact);
429
+ if (actualHash !== expectedHash)
430
+ throw new FixtureIntegrityError(expectedHash, actualHash);
431
+ }
432
+ /**
433
+ * PURE replay of `DraftExtractor` — zero live LLM / network calls. Computes the
434
+ * inputKey for the requested content, looks it up in the frozen records:
435
+ * - HIT → return the recorded `string[]` (including a recorded `[]` — a real row);
436
+ * - MISS → throw `ReplayMissError` (NEVER fall back to `[]`).
437
+ *
438
+ * Integrity (fold B + F): the constructor takes the EXTERNAL expected content-
439
+ * hash, computes the actual WHOLE-ARTIFACT hash (records + provenance), and throws
440
+ * `FixtureIntegrityError` AT CONSTRUCTION on mismatch — so a tampered/drifted
441
+ * fixture (records OR a prompt-provenance edit) can never serve a single replay.
442
+ */
443
+ export class ReplayDraftExtractor {
444
+ records;
445
+ constructor(fixture, expectedHash) {
446
+ const parsed = ReplayArtifactSchema.parse(fixture);
447
+ assertFixtureIntegrity(parsed, expectedHash);
448
+ this.records = parsed.records.extractor;
449
+ }
450
+ // `async` so a MISS surfaces as a REJECTED promise (the port contract is
451
+ // `Promise<string[]>`; a consumer awaits it). A synchronous `throw` would
452
+ // escape an `await`-less caller's `.catch`, so the rejection path is uniform.
453
+ async draft(content) {
454
+ const inputKey = extractorInputKey(content);
455
+ // `Object.prototype.hasOwnProperty`-style presence check, NOT truthiness: a
456
+ // recorded `[]` is a real HIT (falsy-but-present), distinct from an absent
457
+ // key (a MISS).
458
+ if (!Object.prototype.hasOwnProperty.call(this.records, inputKey)) {
459
+ throw new ReplayMissError('extractor', inputKey);
460
+ }
461
+ return this.records[inputKey];
462
+ }
463
+ }
464
+ /**
465
+ * PURE replay of `DraftClassifier` — zero live calls. Computes the inputKey
466
+ * (using the caller's `draftRef` resolver), looks it up:
467
+ * - HIT → return the recorded `ClassifierResult` (including a recorded
468
+ * `{behavioral, error-default}` — a real row);
469
+ * - MISS → throw `ReplayMissError` (NEVER fall back to the safe-default).
470
+ *
471
+ * Same external-expected-hash integrity gate at construction (fold B).
472
+ */
473
+ export class ReplayDraftClassifier {
474
+ records;
475
+ draftRef;
476
+ constructor(fixture, expectedHash, draftRef) {
477
+ const parsed = ReplayArtifactSchema.parse(fixture);
478
+ assertFixtureIntegrity(parsed, expectedHash);
479
+ this.records = parsed.records.classifier;
480
+ this.draftRef = draftRef;
481
+ }
482
+ // `async` so a MISS surfaces as a REJECTED promise (uniform with the port
483
+ // contract `Promise<ClassifierResult>`), as in `ReplayDraftExtractor.draft`.
484
+ async classify(draft) {
485
+ const inputKey = classifierInputKey(draft, this.draftRef(draft));
486
+ if (!Object.prototype.hasOwnProperty.call(this.records, inputKey)) {
487
+ throw new ReplayMissError('classifier', inputKey);
488
+ }
489
+ return this.records[inputKey];
490
+ }
491
+ }
492
+ //# sourceMappingURL=spine-llm-replay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spine-llm-replay.js","sourceRoot":"","sources":["../../src/commands/spine-llm-replay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=spine-llm-replay.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spine-llm-replay.test.d.ts","sourceRoot":"","sources":["../../src/commands/spine-llm-replay.test.ts"],"names":[],"mappings":""}