@dzhechkov/harness-core 0.3.142 → 0.3.144
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.
- package/.dz-manifest.json +49 -25
- package/README.md +2 -1
- package/dist/backlog.d.ts +97 -1
- package/dist/backlog.d.ts.map +1 -1
- package/dist/backlog.js +214 -31
- package/dist/backlog.js.map +1 -1
- package/dist/compounding.d.ts +22 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +29 -10
- package/dist/compounding.js.map +1 -1
- package/dist/epoch-replay.d.ts +399 -0
- package/dist/epoch-replay.d.ts.map +1 -0
- package/dist/epoch-replay.js +695 -0
- package/dist/epoch-replay.js.map +1 -0
- package/dist/guard.d.ts +44 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +144 -1
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/sbom.json +84 -24
- package/src/backlog.ts +282 -27
- package/src/compounding.ts +45 -8
- package/src/epoch-replay.ts +955 -0
- package/src/guard.ts +151 -1
- package/src/index.ts +52 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dz epoch-replay` — the executable cold-vs-warm EPOCH RUNNER (feature epoch-replay, scout idea #4).
|
|
3
|
+
*
|
|
4
|
+
* `dz compounding` answers READINESS ("N unique prompt events recorded — a replay can now be RUN").
|
|
5
|
+
* This module answers the RESULT: Epoch-0 (cold, no injected lessons) vs Epoch-1 (warm, the SAME
|
|
6
|
+
* instances plus exactly the lessons the apply leg injected), scored into a three-valued verdict
|
|
7
|
+
* whose positive branch requires two DISJOINT Wilson confidence intervals.
|
|
8
|
+
*
|
|
9
|
+
* ── The honesty boundary (ADR-002) ─────────────────────────────────────────────────────────────
|
|
10
|
+
* This runner ORCHESTRATES and SCORES. It NEVER calls a model. Real mode is a three-stage protocol
|
|
11
|
+
* over files:
|
|
12
|
+
* 1. `buildWorkOrder` — emits the instances + per-arm generation instructions + the
|
|
13
|
+
* PRE-REGISTERED blind A/B assignment (seeded, decided before any plan
|
|
14
|
+
* text exists) + an integrity `digest` over that pre-registered core.
|
|
15
|
+
* 2. `buildJudgePrompts`— renders the blind judge prompts from the filled plans. The judge-facing
|
|
16
|
+
* payload is `{id, prompt}` and NOTHING else.
|
|
17
|
+
* 3. `verifyWorkOrder` + `unblindJudgments` + `scoreEpochReplay` — check the order really is the
|
|
18
|
+
* pre-registered one (digest + seed-derived assignment), un-blind against
|
|
19
|
+
* it (never against a field the judge wrote), and compute the verdict.
|
|
20
|
+
* Every stage is pure and deterministic, so the protocol is testable with zero LLM dependency.
|
|
21
|
+
*
|
|
22
|
+
* ── What "blind" has to mean ───────────────────────────────────────────────────────────────────
|
|
23
|
+
* The first version shipped `warmIsA` INSIDE the judge artifact: the judge could read the answer
|
|
24
|
+
* key, so the blinding was theatre (Codex QE CRITICAL-1). `warmIsA` now exists only in the work
|
|
25
|
+
* order, which `--score` consumes and the judge never sees — and the artifact is byte-identical
|
|
26
|
+
* whichever way the assignment fell.
|
|
27
|
+
*
|
|
28
|
+
* ── The conformance firewall ───────────────────────────────────────────────────────────────────
|
|
29
|
+
* The warm arm's only delta is the lessons the apply leg ALREADY injects for that prompt. Gold
|
|
30
|
+
* answers, judge verdicts, and outcome labels never enter the warm context — feedback flows from
|
|
31
|
+
* SOLVE OUTCOMES ONLY. `buildWorkOrder` therefore reads instances, not results, and there is no
|
|
32
|
+
* code path from an `EpochOutcome` back into a work order.
|
|
33
|
+
*
|
|
34
|
+
* ── `--mock` ───────────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
* A seeded synthetic outcome generator (reusing `mulberry32` — no second RNG in this repo) with a
|
|
36
|
+
* configurable TRUE effect, so the verdict math is exercised at $0 before any real data exists.
|
|
37
|
+
*
|
|
38
|
+
* Everything here is PURE: callers read/write files; this module only computes.
|
|
39
|
+
*/
|
|
40
|
+
// One-way dependency: epoch-replay → compounding. `mulberry32` (the repo's only PRNG), the darwin
|
|
41
|
+
// min-n and the single `replayableInstances` definition all live there; importing them keeps this
|
|
42
|
+
// module free of a second RNG and of a second definition of "a replayable pair".
|
|
43
|
+
import { createHash } from 'node:crypto';
|
|
44
|
+
import { mulberry32, MIN_SAMPLES_PER_ARM, replayableInstances, } from './compounding.js';
|
|
45
|
+
export { replayableInstances };
|
|
46
|
+
// ── Wilson score interval (ADR-003) ────────────────────────────────────────────────────────────
|
|
47
|
+
/** 95% two-sided normal quantile. Named so a future 90%/99% run is a parameter, not a fork. */
|
|
48
|
+
export const WILSON_Z = 1.96;
|
|
49
|
+
/** Per-arm minimum. Shared with the darwin FDR discipline already pinned in compounding.ts. */
|
|
50
|
+
export const MIN_INSTANCES = MIN_SAMPLES_PER_ARM;
|
|
51
|
+
/**
|
|
52
|
+
* Floor of DECISIVE pairs for the no-lift branch. Necessary, NOT sufficient: reaching it only makes
|
|
53
|
+
* the non-superiority test eligible — the test itself must still pass (see {@link NO_LIFT_MARGIN}).
|
|
54
|
+
*
|
|
55
|
+
* The first draft FALSIFIED on `warmWins <= coldWins` at this n, which made 6/12 vs 6/12 read as
|
|
56
|
+
* "refuted". That is indefensible: a tie at n=12 is UNDER-POWERED, not evidence of no effect
|
|
57
|
+
* (Codex QE HIGH-3).
|
|
58
|
+
*/
|
|
59
|
+
export const FALSIFY_NO_LIFT_MIN_N = 2 * MIN_SAMPLES_PER_ARM;
|
|
60
|
+
/**
|
|
61
|
+
* Pre-registered NON-SUPERIORITY margin, on the LIFT scale (see {@link liftInterval}). "No lift" is
|
|
62
|
+
* claimed only when the UPPER bound of the lift interval sits below this — i.e. the data EXCLUDE
|
|
63
|
+
* any lift worth having, rather than merely failing to show one.
|
|
64
|
+
*
|
|
65
|
+
* Consequence, stated plainly: at this margin the branch needs ~1200 decisive pairs. That is the
|
|
66
|
+
* honest price of an equivalence-style claim, and it is exactly why a 6/6 tie at n=12 is
|
|
67
|
+
* INCONCLUSIVE rather than FALSIFIED.
|
|
68
|
+
*/
|
|
69
|
+
export const NO_LIFT_MARGIN = 0.05;
|
|
70
|
+
/** A margin outside this range is REFUSED, never clamped: `--margin 99` must not buy FALSIFIED. */
|
|
71
|
+
export const MARGIN_MIN_EXCLUSIVE = 0;
|
|
72
|
+
export const MARGIN_MAX = 0.5;
|
|
73
|
+
/** True for a margin that may be pre-registered — finite and in `(0, 0.5]`. */
|
|
74
|
+
export function isValidMargin(value) {
|
|
75
|
+
return typeof value === 'number' && Number.isFinite(value) && value > MARGIN_MIN_EXCLUSIVE && value <= MARGIN_MAX;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Wilson score interval for a binomial proportion. Returns `null` — never a fabricated interval —
|
|
79
|
+
* for any input that is not a real (k, n) pair: n <= 0, non-integers, k out of [0, n], non-finite
|
|
80
|
+
* numbers, or a non-finite/non-positive z. A `null` interval can only ever produce INCONCLUSIVE.
|
|
81
|
+
*/
|
|
82
|
+
export function wilsonInterval(k, n, z = WILSON_Z) {
|
|
83
|
+
if (!Number.isFinite(k) || !Number.isFinite(n) || !Number.isFinite(z))
|
|
84
|
+
return null;
|
|
85
|
+
if (!Number.isInteger(k) || !Number.isInteger(n))
|
|
86
|
+
return null;
|
|
87
|
+
if (n <= 0 || k < 0 || k > n)
|
|
88
|
+
return null;
|
|
89
|
+
if (z <= 0)
|
|
90
|
+
return null;
|
|
91
|
+
const p = k / n;
|
|
92
|
+
const z2 = z * z;
|
|
93
|
+
const denom = 1 + z2 / n;
|
|
94
|
+
const centre = (p + z2 / (2 * n)) / denom;
|
|
95
|
+
const half = (z / denom) * Math.sqrt((p * (1 - p)) / n + z2 / (4 * n * n));
|
|
96
|
+
if (!Number.isFinite(centre) || !Number.isFinite(half))
|
|
97
|
+
return null;
|
|
98
|
+
return {
|
|
99
|
+
k,
|
|
100
|
+
n,
|
|
101
|
+
p,
|
|
102
|
+
lower: Math.max(0, Math.min(1, centre - half)),
|
|
103
|
+
upper: Math.max(0, Math.min(1, centre + half)),
|
|
104
|
+
z,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Map the Wilson interval for `p̂ = P(warm wins | decisive)` onto the LIFT scale, `2p − 1`.
|
|
109
|
+
*
|
|
110
|
+
* WHY THE SCALE MATTERS (and why it is not cosmetic): `margin` is stated as "a lift worth having",
|
|
111
|
+
* which is what a reader reasons about, and it kept exactly the meaning it had under the previous
|
|
112
|
+
* (wrong) two-proportion model. On the raw `p̂` scale the equivalent threshold is `0.5 + margin/2`,
|
|
113
|
+
* NOT `0.5 + margin` — reading the margin on the `p̂` scale would silently DOUBLE the strictness of
|
|
114
|
+
* the non-superiority branch, i.e. make FALSIFIED easier. That is the anti-conservative direction,
|
|
115
|
+
* which is precisely the class of error the paired rewrite exists to remove.
|
|
116
|
+
*
|
|
117
|
+
* Worked check (the case that drove the rewrite): 500 warm / 500 cold over 1000 decisive pairs gives
|
|
118
|
+
* `p̂` CI `[0.4691, 0.5309]` → lift CI `[-0.0619, +0.0619]`. Upper `0.0619` exceeds the default
|
|
119
|
+
* margin `0.05`, so it reads INCONCLUSIVE. The discarded two-proportion Newcombe interval put the
|
|
120
|
+
* upper bound at `0.0437` and called the same data FALSIFIED.
|
|
121
|
+
*/
|
|
122
|
+
export function liftInterval(pWarm) {
|
|
123
|
+
if (pWarm === null)
|
|
124
|
+
return null;
|
|
125
|
+
const d = 2 * pWarm.p - 1;
|
|
126
|
+
const lower = 2 * pWarm.lower - 1;
|
|
127
|
+
const upper = 2 * pWarm.upper - 1;
|
|
128
|
+
if (!Number.isFinite(d) || !Number.isFinite(lower) || !Number.isFinite(upper))
|
|
129
|
+
return null;
|
|
130
|
+
return { d, lower, upper };
|
|
131
|
+
}
|
|
132
|
+
// ── Stage 1: the work order (ADR-002) ──────────────────────────────────────────────────────────
|
|
133
|
+
export const WORK_ORDER_KIND = 'dz-epoch-replay-work-order';
|
|
134
|
+
/**
|
|
135
|
+
* v3: the integrity `digest` (v2) plus the PRE-REGISTERED `margin` and `corpusFingerprint`, and an
|
|
136
|
+
* unambiguous JSON digest input. An older order cannot be verified under these rules, so it is
|
|
137
|
+
* refused rather than half-trusted.
|
|
138
|
+
*/
|
|
139
|
+
export const WORK_ORDER_VERSION = 3;
|
|
140
|
+
function clampInt(value, fallback, min, max) {
|
|
141
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
142
|
+
return fallback;
|
|
143
|
+
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
144
|
+
}
|
|
145
|
+
function clampNumber(value, fallback, min, max) {
|
|
146
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
147
|
+
return fallback;
|
|
148
|
+
return Math.max(min, Math.min(max, value));
|
|
149
|
+
}
|
|
150
|
+
export const DEFAULT_WORD_MIN = 80;
|
|
151
|
+
export const DEFAULT_WORD_MAX = 150;
|
|
152
|
+
/**
|
|
153
|
+
* The digest input is the PRE-REGISTERED core and nothing else: version, seed, margin, the corpus
|
|
154
|
+
* fingerprint, and every `[id, warmIsA]` pair in order. Deliberately EXCLUDED are the fields the
|
|
155
|
+
* protocol requires a human to fill after emit — `coldPlan`, `warmPlan` and `class` — so filling
|
|
156
|
+
* them keeps the order verifiable. (Limitation stated in ADR-002: `class` is NOT covered.)
|
|
157
|
+
*
|
|
158
|
+
* Serialization is `JSON.stringify` over an array of TUPLES, not a delimiter-joined string. The
|
|
159
|
+
* first version used `${id}:${flag}` joined by `,`, so an id containing `:` or `,` could make two
|
|
160
|
+
* DIFFERENT orders hash identically — e.g. `[['a:1,b', false]]` and `[['a', true], ['b', false]]`
|
|
161
|
+
* both flattened to `a:1,b:0` (Codex QE MED-D). JSON escaping removes the ambiguity.
|
|
162
|
+
*/
|
|
163
|
+
function workOrderDigestInput(core) {
|
|
164
|
+
return JSON.stringify({
|
|
165
|
+
v: core.version,
|
|
166
|
+
seed: core.seed,
|
|
167
|
+
margin: core.margin,
|
|
168
|
+
corpus: core.corpusFingerprint,
|
|
169
|
+
items: core.items.map((i) => [i.id, i.warmIsA]),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/** sha256 over the ordered instance identity — lets a reviewer see two orders share a corpus. */
|
|
173
|
+
export function corpusFingerprint(instances) {
|
|
174
|
+
return createHash('sha256')
|
|
175
|
+
.update(JSON.stringify(instances.map((i) => [i.id, i.query])))
|
|
176
|
+
.digest('hex');
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* sha256 over {@link workOrderDigestInput}. Pure computation — no IO, no key material.
|
|
180
|
+
*
|
|
181
|
+
* HONEST SCOPE — read this before describing what it proves. This is an integrity check against
|
|
182
|
+
* ACCIDENTAL corruption and mismatch; it is NOT a cryptographic commitment. The digest is
|
|
183
|
+
* self-contained, so a determined operator can re-forge it (at n=12 a seed search finds a matching
|
|
184
|
+
* assignment in a few thousand tries). The threat model is US making mistakes — a hand-edited file,
|
|
185
|
+
* a stale order paired with fresh judgments — exactly the corruption-detection scoping the
|
|
186
|
+
* hash-chain backlog idea already carries. The honest-use contract is procedural: emit once, then
|
|
187
|
+
* judge, and keep the emitted file.
|
|
188
|
+
*/
|
|
189
|
+
export function workOrderDigest(order) {
|
|
190
|
+
return createHash('sha256')
|
|
191
|
+
.update(workOrderDigestInput({
|
|
192
|
+
version: order.version,
|
|
193
|
+
seed: order.seed,
|
|
194
|
+
margin: order.margin,
|
|
195
|
+
corpusFingerprint: order.corpusFingerprint,
|
|
196
|
+
items: order.items,
|
|
197
|
+
}))
|
|
198
|
+
.digest('hex');
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The one sentence that states what the digest is and is not. Held in a constant so the CLI error
|
|
202
|
+
* text, the module documentation and the honest-scope regression test all read the SAME words —
|
|
203
|
+
* this promise must not quietly regrow into "commitment" language (Codex QE HIGH-C).
|
|
204
|
+
*/
|
|
205
|
+
export const DIGEST_HONEST_SCOPE = 'integrity check against accidental corruption/mismatch — not a cryptographic commitment; ' +
|
|
206
|
+
'a determined operator can re-forge it, and the honest-use contract is emit-once-then-judge';
|
|
207
|
+
/**
|
|
208
|
+
* Integrity-check a work order before ANY verdict may depend on it (Codex QE HIGH-2).
|
|
209
|
+
*
|
|
210
|
+
* Checking `kind` and `Array.isArray(items)` was vacuous: a hand-written file with the right two
|
|
211
|
+
* fields and an invented `warmIsA` un-blinded into whatever verdict its author wanted. Four checks
|
|
212
|
+
* now have to agree:
|
|
213
|
+
* 1. the `digest` recomputes over (version, seed, margin, corpus fingerprint, `[id, warmIsA]`…);
|
|
214
|
+
* 2. every `warmIsA` is REDERIVABLE from the stated `seed` — the same `mulberry32` stream that
|
|
215
|
+
* emitted it;
|
|
216
|
+
* 3. the pre-registered `margin` is in range;
|
|
217
|
+
* 4. structural sanity — unique non-empty ids, boolean assignments, integer seed.
|
|
218
|
+
*
|
|
219
|
+
* WHAT THIS IS NOT: see {@link DIGEST_HONEST_SCOPE}. Re-deriving from the seed raises the bar from
|
|
220
|
+
* "edit one field" to "search for a seed", which at n=12 is a few thousand tries — a deterrent
|
|
221
|
+
* against slips, not a defence against intent. Nothing here is a cryptographic commitment, and no
|
|
222
|
+
* amount of hashing inside the file itself could make it one.
|
|
223
|
+
*/
|
|
224
|
+
export function verifyWorkOrder(value) {
|
|
225
|
+
const problems = [];
|
|
226
|
+
if (typeof value !== 'object' || value === null)
|
|
227
|
+
return { ok: false, problems: ['not an object'] };
|
|
228
|
+
const o = value;
|
|
229
|
+
if (o.kind !== WORK_ORDER_KIND)
|
|
230
|
+
problems.push(`kind is not ${WORK_ORDER_KIND}`);
|
|
231
|
+
if (o.version !== WORK_ORDER_VERSION)
|
|
232
|
+
problems.push(`version is not ${WORK_ORDER_VERSION} (older orders cannot be verified under these rules)`);
|
|
233
|
+
if (!Number.isInteger(o.seed) || o.seed < 0)
|
|
234
|
+
problems.push('seed is not a non-negative integer');
|
|
235
|
+
if (!isValidMargin(o.margin)) {
|
|
236
|
+
problems.push(`pre-registered margin ${JSON.stringify(o.margin)} is not a number in (0, ${MARGIN_MAX}]`);
|
|
237
|
+
}
|
|
238
|
+
if (typeof o.corpusFingerprint !== 'string' || o.corpusFingerprint === '')
|
|
239
|
+
problems.push('missing corpusFingerprint');
|
|
240
|
+
if (!Array.isArray(o.items)) {
|
|
241
|
+
problems.push('items is not an array');
|
|
242
|
+
return { ok: false, problems };
|
|
243
|
+
}
|
|
244
|
+
const items = o.items;
|
|
245
|
+
const seen = new Set();
|
|
246
|
+
const core = [];
|
|
247
|
+
for (let i = 0; i < items.length; i++) {
|
|
248
|
+
const it = (typeof items[i] === 'object' && items[i] !== null ? items[i] : {});
|
|
249
|
+
const id = typeof it.id === 'string' ? it.id : '';
|
|
250
|
+
if (id.trim() === '')
|
|
251
|
+
problems.push(`item ${i}: missing id`);
|
|
252
|
+
else if (seen.has(id))
|
|
253
|
+
problems.push(`item ${i}: duplicate id ${JSON.stringify(id)}`);
|
|
254
|
+
seen.add(id);
|
|
255
|
+
if (typeof it.warmIsA !== 'boolean')
|
|
256
|
+
problems.push(`item ${i}: warmIsA is not a boolean`);
|
|
257
|
+
core.push({ id, warmIsA: it.warmIsA === true });
|
|
258
|
+
}
|
|
259
|
+
if (problems.length > 0)
|
|
260
|
+
return { ok: false, problems };
|
|
261
|
+
const expected = workOrderDigest({
|
|
262
|
+
seed: o.seed,
|
|
263
|
+
version: o.version,
|
|
264
|
+
margin: o.margin,
|
|
265
|
+
corpusFingerprint: o.corpusFingerprint,
|
|
266
|
+
items: core,
|
|
267
|
+
});
|
|
268
|
+
if (typeof o.digest !== 'string' || o.digest !== expected) {
|
|
269
|
+
problems.push(`digest mismatch — the pre-registered core (seed / margin / corpus / ids / assignments) was altered after emit. ` +
|
|
270
|
+
`NOTE: ${DIGEST_HONEST_SCOPE}`);
|
|
271
|
+
}
|
|
272
|
+
// Re-derive the assignment from the stated seed: the same one call per item, in item order.
|
|
273
|
+
const rand = mulberry32(o.seed);
|
|
274
|
+
for (let i = 0; i < core.length; i++) {
|
|
275
|
+
const derived = rand() < 0.5;
|
|
276
|
+
if (derived !== core[i].warmIsA) {
|
|
277
|
+
problems.push(`item ${i} (${core[i].id}): warmIsA does not derive from seed ${o.seed} — the assignment was not pre-registered`);
|
|
278
|
+
break; // one is enough; listing all would just be noise
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { ok: problems.length === 0, problems };
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Emit the generation work order. Deterministic in (instances, seed): the same corpus and seed
|
|
285
|
+
* produce the same blind assignment, which is what makes "pre-registered" checkable after the fact.
|
|
286
|
+
*/
|
|
287
|
+
export function buildWorkOrder(instances, options = {}) {
|
|
288
|
+
// A margin is a PRE-REGISTRATION, so a bad one is an error, not something to quietly round into
|
|
289
|
+
// range: `margin: 99` clamped to 0.5 would silently pre-register a bar nothing can fail.
|
|
290
|
+
if (options.margin !== undefined && !isValidMargin(options.margin)) {
|
|
291
|
+
throw new RangeError(`margin ${JSON.stringify(options.margin)} must be a number in (${MARGIN_MIN_EXCLUSIVE}, ${MARGIN_MAX}]`);
|
|
292
|
+
}
|
|
293
|
+
const margin = options.margin ?? NO_LIFT_MARGIN;
|
|
294
|
+
const seed = clampInt(options.seed, 20260729, 0, 2 ** 31 - 1);
|
|
295
|
+
const wordMin = clampInt(options.wordMin, DEFAULT_WORD_MIN, 10, 5000);
|
|
296
|
+
const wordMax = Math.max(wordMin, clampInt(options.wordMax, DEFAULT_WORD_MAX, 10, 5000));
|
|
297
|
+
const limit = clampInt(options.limit, 0, 0, 100000);
|
|
298
|
+
const picked = limit > 0 ? instances.slice(0, limit) : instances.slice();
|
|
299
|
+
const rand = mulberry32(seed);
|
|
300
|
+
const items = picked.map((inst) => {
|
|
301
|
+
const warmIsA = rand() < 0.5;
|
|
302
|
+
const shared = `Answer the user prompt below as a first-response PLAN of ${wordMin}-${wordMax} words. ` +
|
|
303
|
+
'Plain prose, no headings, no lists. Say what you would DO first and why.';
|
|
304
|
+
return {
|
|
305
|
+
id: inst.id,
|
|
306
|
+
query: inst.query,
|
|
307
|
+
class: inst.class,
|
|
308
|
+
warmIsA,
|
|
309
|
+
cold: { instruction: shared, lessons: [] },
|
|
310
|
+
warm: {
|
|
311
|
+
instruction: `${shared} The following learned lessons were surfaced for this prompt; use them if relevant.`,
|
|
312
|
+
lessons: inst.lessons.slice(),
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
const emittedAt = typeof options.nowTs === 'string' ? options.nowTs : new Date().toISOString();
|
|
317
|
+
const fingerprint = corpusFingerprint(picked.map((i) => ({ id: i.id, query: i.query })));
|
|
318
|
+
return {
|
|
319
|
+
kind: WORK_ORDER_KIND,
|
|
320
|
+
version: WORK_ORDER_VERSION,
|
|
321
|
+
seed,
|
|
322
|
+
generatedAt: emittedAt,
|
|
323
|
+
emittedAt,
|
|
324
|
+
margin,
|
|
325
|
+
corpusFingerprint: fingerprint,
|
|
326
|
+
wordMin,
|
|
327
|
+
wordMax,
|
|
328
|
+
protocol: [
|
|
329
|
+
'PRE-REGISTERED before any plan text exists. Do not edit `warmIsA`, `seed`, `margin`, or `id`.',
|
|
330
|
+
'COLD arm = the prompt alone. WARM arm = the same prompt plus exactly the lessons the apply leg injected.',
|
|
331
|
+
'CONFORMANCE FIREWALL: never place a gold answer, a judge verdict, or an outcome label in either arm.',
|
|
332
|
+
`Fill coldPlan/warmPlan for each item (${wordMin}-${wordMax} words, symmetric length).`,
|
|
333
|
+
'Assign `class` NOW if you intend to slice — a class chosen after outcomes are known is not a pre-registration.',
|
|
334
|
+
'Then: `dz epoch-replay --judge <this file>` → judge each prompt with an EXTERNAL model → `dz epoch-replay --score <judgments> --work-order <this file>`.',
|
|
335
|
+
`The non-superiority margin (${margin}) is PRE-REGISTERED HERE. --score reads it from this file and rejects a --margin flag.`,
|
|
336
|
+
'PRIVACY: this file contains raw local prompt texts. Keep it out of version control.',
|
|
337
|
+
`INTEGRITY: \`digest\` covers version + seed + margin + corpus + every [id, warmIsA]. Editing any of them makes --judge/--score refuse. It is an ${DIGEST_HONEST_SCOPE}.`,
|
|
338
|
+
],
|
|
339
|
+
items,
|
|
340
|
+
digest: workOrderDigest({ seed, version: WORK_ORDER_VERSION, margin, corpusFingerprint: fingerprint, items }),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Render blind A/B judge prompts from a FILLED work order. An item missing either plan is skipped
|
|
345
|
+
* with a reason: half a pair is not a comparison, and substituting an empty string would hand the
|
|
346
|
+
* judge a rigged contest.
|
|
347
|
+
*/
|
|
348
|
+
export function buildJudgePrompts(order) {
|
|
349
|
+
const prompts = [];
|
|
350
|
+
const skipped = [];
|
|
351
|
+
for (const item of order.items ?? []) {
|
|
352
|
+
const cold = typeof item.coldPlan === 'string' ? item.coldPlan.trim() : '';
|
|
353
|
+
const warm = typeof item.warmPlan === 'string' ? item.warmPlan.trim() : '';
|
|
354
|
+
if (cold === '' || warm === '') {
|
|
355
|
+
skipped.push({
|
|
356
|
+
id: item.id,
|
|
357
|
+
reason: cold === '' && warm === '' ? 'both plans missing' : cold === '' ? 'coldPlan missing' : 'warmPlan missing',
|
|
358
|
+
});
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const a = item.warmIsA ? warm : cold;
|
|
362
|
+
const b = item.warmIsA ? cold : warm;
|
|
363
|
+
prompts.push({
|
|
364
|
+
id: item.id,
|
|
365
|
+
prompt: [
|
|
366
|
+
'You are a blind judge. An assistant received this user prompt:',
|
|
367
|
+
`PROMPT: ${item.query}`,
|
|
368
|
+
'',
|
|
369
|
+
'Two candidate first-response plans. Judge which better serves the user: concreteness,',
|
|
370
|
+
'correct first actions, avoiding known failure modes. Ignore verbosity and style.',
|
|
371
|
+
'',
|
|
372
|
+
`PLAN A: ${a}`,
|
|
373
|
+
'',
|
|
374
|
+
`PLAN B: ${b}`,
|
|
375
|
+
'',
|
|
376
|
+
'Reply with EXACTLY 3 lines:',
|
|
377
|
+
'WINNER: A|B|TIE',
|
|
378
|
+
'DECISIVE: <the single concrete element that made the winner better, one line>',
|
|
379
|
+
'CONFIDENCE: high|medium|low',
|
|
380
|
+
].join('\n'),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return { prompts, skipped };
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Map blind judgments back to arms using the work order's PRE-REGISTERED `warmIsA`. The judgments
|
|
387
|
+
* file deliberately carries no arm labels: if un-blinding read a label the judge (or a later hand
|
|
388
|
+
* edit) supplied, the blinding would be decorative.
|
|
389
|
+
*
|
|
390
|
+
* Unknown ids and unparseable winners are SKIPPED with a reason, never guessed. DUPLICATE ids are
|
|
391
|
+
* different: they are a CORRUPT input, not a skippable row, so the whole call is REFUSED. Skipping
|
|
392
|
+
* the second copy silently accepted a file in which one judgment had been pasted five times — which
|
|
393
|
+
* scored as n=5 and reached SUPPORTED off a single opinion (Codex QE MED-4).
|
|
394
|
+
*/
|
|
395
|
+
export function unblindJudgments(order, judgments) {
|
|
396
|
+
const byId = new Map();
|
|
397
|
+
for (const item of order.items ?? [])
|
|
398
|
+
byId.set(item.id, item);
|
|
399
|
+
const outcomes = [];
|
|
400
|
+
const skipped = [];
|
|
401
|
+
const seen = new Set();
|
|
402
|
+
// Refuse BEFORE interpreting anything: a duplicated id makes the whole file untrustworthy.
|
|
403
|
+
const dupes = new Set();
|
|
404
|
+
const walked = new Set();
|
|
405
|
+
for (const j of judgments ?? []) {
|
|
406
|
+
const id = typeof j?.id === 'string' ? j.id : '';
|
|
407
|
+
if (id === '')
|
|
408
|
+
continue;
|
|
409
|
+
if (walked.has(id))
|
|
410
|
+
dupes.add(id);
|
|
411
|
+
walked.add(id);
|
|
412
|
+
}
|
|
413
|
+
if (dupes.size > 0) {
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
error: `corrupt judgments: duplicate id(s) ${[...dupes].sort().map((d) => JSON.stringify(d)).join(', ')} — one instance may be judged exactly once`,
|
|
417
|
+
outcomes: [],
|
|
418
|
+
skipped: [],
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
for (const j of judgments ?? []) {
|
|
422
|
+
const id = typeof j?.id === 'string' ? j.id : '';
|
|
423
|
+
if (id === '') {
|
|
424
|
+
skipped.push({ id: String(j?.id ?? ''), reason: 'missing id' });
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const item = byId.get(id);
|
|
428
|
+
if (!item) {
|
|
429
|
+
skipped.push({ id, reason: 'id not in the work order (cannot un-blind)' });
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const raw = typeof j.winner === 'string' ? j.winner.trim().toUpperCase() : '';
|
|
433
|
+
let winner;
|
|
434
|
+
if (raw === 'TIE')
|
|
435
|
+
winner = 'tie';
|
|
436
|
+
else if (raw === 'A')
|
|
437
|
+
winner = item.warmIsA ? 'warm' : 'cold';
|
|
438
|
+
else if (raw === 'B')
|
|
439
|
+
winner = item.warmIsA ? 'cold' : 'warm';
|
|
440
|
+
else {
|
|
441
|
+
skipped.push({ id, reason: `unparseable winner ${JSON.stringify(j.winner)} (expected A|B|TIE)` });
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
seen.add(id);
|
|
445
|
+
outcomes.push({ id, class: item.class ?? null, winner });
|
|
446
|
+
}
|
|
447
|
+
return { ok: true, error: null, outcomes, skipped };
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* The three-valued verdict (ADR-003, as amended).
|
|
451
|
+
*
|
|
452
|
+
* THE MODEL — a SINGLE binomial over DECISIVE pairs. Each instance yields ONE judgment about ONE
|
|
453
|
+
* prompt, so the arms are PAIRED, not two independent samples. Let `W` = warm wins, `C` = cold
|
|
454
|
+
* wins, `D = W + C` (ties are excluded from the test and reported separately). The statistic is
|
|
455
|
+
* `p̂ = W/D` with a Wilson interval, mapped to the lift scale by {@link liftInterval}:
|
|
456
|
+
*
|
|
457
|
+
* SUPPORTED lift lower bound > 0 (equivalently: Wilson lower on p̂ > 0.5)
|
|
458
|
+
* FALSIFIED harm — lift upper bound < 0 (Wilson upper on p̂ < 0.5); OR non-superiority —
|
|
459
|
+
* lift upper bound < `margin` with `D >= FALSIFY_NO_LIFT_MIN_N`
|
|
460
|
+
* INCONCLUSIVE everything else, including `D < MIN_INSTANCES`. A first-class honest outcome.
|
|
461
|
+
*
|
|
462
|
+
* This is exactly the statistic the manual 2026-07-29 experiment used, and it is the correction the
|
|
463
|
+
* re-QE demanded: the previous two-proportion (Newcombe) framing treated the paired judgments as
|
|
464
|
+
* independent samples and was ANTI-CONSERVATIVE — 500/500 over 1000 decisive pairs produced an
|
|
465
|
+
* upper bound of 0.0437 and a FALSIFIED verdict where the paired form gives 0.0619 and INCONCLUSIVE
|
|
466
|
+
* (Codex QE HIGH-A).
|
|
467
|
+
*
|
|
468
|
+
* REFUSALS (verdict is a placeholder, `refusal` is set): duplicate instance ids, an invalid `z`, or
|
|
469
|
+
* an out-of-range `margin`.
|
|
470
|
+
*/
|
|
471
|
+
export function scoreEpochReplay(outcomes, options = {}) {
|
|
472
|
+
const slice = typeof options.slice === 'string' && options.slice.trim() !== '' ? options.slice.trim() : 'all';
|
|
473
|
+
// HIGH-B: a margin is a PRE-REGISTRATION. Out of range is an error, not something to round into
|
|
474
|
+
// range — clamping `--margin 99` down to a legal value would buy FALSIFIED for free.
|
|
475
|
+
const marginGiven = options.margin !== undefined;
|
|
476
|
+
const marginValid = !marginGiven || isValidMargin(options.margin);
|
|
477
|
+
const margin = marginGiven && marginValid ? options.margin : NO_LIFT_MARGIN;
|
|
478
|
+
// MED-5: `z` used to be CLAMPED into [0.0001, 10]. A caller passing 0 therefore got a near-zero
|
|
479
|
+
// -width interval, which is trivially disjoint from anything — SUPPORTED fabricated out of a bad
|
|
480
|
+
// argument. An invalid z is refused, consistent with `wilsonInterval`'s null discipline.
|
|
481
|
+
const zGiven = options.z !== undefined;
|
|
482
|
+
const zValid = !zGiven || (typeof options.z === 'number' && Number.isFinite(options.z) && options.z > 0 && options.z <= 10);
|
|
483
|
+
const z = zGiven && zValid ? options.z : WILSON_Z;
|
|
484
|
+
const pool = (outcomes ?? []).filter((o) => {
|
|
485
|
+
if (!o || typeof o.id !== 'string')
|
|
486
|
+
return false;
|
|
487
|
+
if (o.winner !== 'cold' && o.winner !== 'warm' && o.winner !== 'tie')
|
|
488
|
+
return false;
|
|
489
|
+
return slice === 'all' ? true : (o.class ?? null) === slice;
|
|
490
|
+
});
|
|
491
|
+
// MED-4: five copies of one judgement are not five observations. Refuse — a duplicated instance
|
|
492
|
+
// id means the input is corrupt, and D must never be inflated by a paste.
|
|
493
|
+
const dupes = new Set();
|
|
494
|
+
const walkedIds = new Set();
|
|
495
|
+
for (const o of pool) {
|
|
496
|
+
if (walkedIds.has(o.id))
|
|
497
|
+
dupes.add(o.id);
|
|
498
|
+
walkedIds.add(o.id);
|
|
499
|
+
}
|
|
500
|
+
const n = pool.length;
|
|
501
|
+
const warmWins = pool.filter((o) => o.winner === 'warm').length;
|
|
502
|
+
const coldWins = pool.filter((o) => o.winner === 'cold').length;
|
|
503
|
+
const ties = pool.filter((o) => o.winner === 'tie').length;
|
|
504
|
+
// HIGH-A: ONE binomial over DECISIVE pairs. Ties carry no directional information, so they are
|
|
505
|
+
// excluded from the denominator (and reported); `p̂ = W/D` is the paired statistic.
|
|
506
|
+
const decisive = warmWins + coldWins;
|
|
507
|
+
const pWarm = wilsonInterval(warmWins, decisive, z);
|
|
508
|
+
const lift = liftInterval(pWarm);
|
|
509
|
+
// Cold's interval is the exact complement of warm's — reported for readability, never a second test.
|
|
510
|
+
const coldCi = wilsonInterval(coldWins, decisive, z);
|
|
511
|
+
const warm = { arm: 'warm', wins: warmWins, n: decisive, ci: pWarm };
|
|
512
|
+
const cold = { arm: 'cold', wins: coldWins, n: decisive, ci: coldCi };
|
|
513
|
+
const base = {
|
|
514
|
+
slice,
|
|
515
|
+
n,
|
|
516
|
+
ties,
|
|
517
|
+
decisive,
|
|
518
|
+
cold,
|
|
519
|
+
warm,
|
|
520
|
+
lift,
|
|
521
|
+
z,
|
|
522
|
+
minN: MIN_INSTANCES,
|
|
523
|
+
falsifyNoLiftMinN: FALSIFY_NO_LIFT_MIN_N,
|
|
524
|
+
noLiftMargin: margin,
|
|
525
|
+
};
|
|
526
|
+
// ── refusals: a corrupt input yields NO measurement ──
|
|
527
|
+
if (!zValid) {
|
|
528
|
+
return {
|
|
529
|
+
...base,
|
|
530
|
+
z: WILSON_Z,
|
|
531
|
+
verdict: 'INCONCLUSIVE',
|
|
532
|
+
refusal: `invalid z ${JSON.stringify(options.z)} — must be a finite number in (0, 10]; refused rather than clamped, because a tiny z manufactures a significant interval`,
|
|
533
|
+
reason: 'REFUSED: invalid confidence parameter — no verdict was computed',
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
if (!marginValid) {
|
|
537
|
+
return {
|
|
538
|
+
...base,
|
|
539
|
+
noLiftMargin: NO_LIFT_MARGIN,
|
|
540
|
+
verdict: 'INCONCLUSIVE',
|
|
541
|
+
refusal: `invalid margin ${JSON.stringify(options.margin)} — must be a number in (${MARGIN_MIN_EXCLUSIVE}, ${MARGIN_MAX}]; refused rather than clamped, because an oversized margin buys FALSIFIED`,
|
|
542
|
+
reason: 'REFUSED: invalid non-superiority margin — no verdict was computed',
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
if (dupes.size > 0) {
|
|
546
|
+
return {
|
|
547
|
+
...base,
|
|
548
|
+
verdict: 'INCONCLUSIVE',
|
|
549
|
+
refusal: `corrupt outcomes: duplicate instance id(s) ${[...dupes].sort().map((d) => JSON.stringify(d)).join(', ')} — one instance counts exactly once`,
|
|
550
|
+
reason: 'REFUSED: duplicate instance ids would inflate the denominator — no verdict was computed',
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
if (decisive < MIN_INSTANCES) {
|
|
554
|
+
return {
|
|
555
|
+
...base,
|
|
556
|
+
verdict: 'INCONCLUSIVE',
|
|
557
|
+
refusal: null,
|
|
558
|
+
reason: `insufficient data: ${decisive} DECISIVE pair(s) in slice "${slice}"` +
|
|
559
|
+
`${ties > 0 ? ` (${ties} tie(s) carry no direction and are excluded)` : ''}, ` +
|
|
560
|
+
`${MIN_INSTANCES} needed (darwin FDR discipline — n=3 gave a 33% false-discovery rate)`,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
if (pWarm === null || lift === null) {
|
|
564
|
+
return {
|
|
565
|
+
...base,
|
|
566
|
+
verdict: 'INCONCLUSIVE',
|
|
567
|
+
refusal: null,
|
|
568
|
+
reason: `no valid confidence interval could be computed for slice "${slice}" (degenerate counts)`,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const shape = `warm ${warmWins}/${decisive} decisive (p=${pWarm.p.toFixed(3)}, CI [${pWarm.lower.toFixed(3)}, ${pWarm.upper.toFixed(3)}]); ` +
|
|
572
|
+
`lift [${lift.lower.toFixed(3)}, ${lift.upper.toFixed(3)}]`;
|
|
573
|
+
// Belt-and-braces: `lift.lower > 0` already implies warm won more, but asserting it makes a
|
|
574
|
+
// SUPPORTED unreachable through a single edited comparison.
|
|
575
|
+
if (lift.lower > 0 && warmWins > coldWins) {
|
|
576
|
+
return {
|
|
577
|
+
...base,
|
|
578
|
+
verdict: 'SUPPORTED',
|
|
579
|
+
refusal: null,
|
|
580
|
+
reason: `${shape} — the lift interval lies ENTIRELY above zero (warm wins more than half of decisive pairs)`,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (lift.upper < 0 && coldWins > warmWins) {
|
|
584
|
+
return {
|
|
585
|
+
...base,
|
|
586
|
+
verdict: 'FALSIFIED',
|
|
587
|
+
refusal: null,
|
|
588
|
+
reason: `harm: ${shape} — the lift interval lies ENTIRELY below zero (cold wins more than half of decisive pairs)`,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
// NON-SUPERIORITY, not "warm didn't win". The data must EXCLUDE a lift as large as the margin;
|
|
592
|
+
// an equal split at small D excludes nothing (Codex QE HIGH-3).
|
|
593
|
+
if (decisive >= FALSIFY_NO_LIFT_MIN_N && lift.upper < margin) {
|
|
594
|
+
return {
|
|
595
|
+
...base,
|
|
596
|
+
verdict: 'FALSIFIED',
|
|
597
|
+
refusal: null,
|
|
598
|
+
reason: `no lift: ${shape} — the lift UPPER bound is below the pre-registered margin ${margin} over ${decisive} decisive pair(s), so a lift worth having is excluded`,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
...base,
|
|
603
|
+
verdict: 'INCONCLUSIVE',
|
|
604
|
+
refusal: null,
|
|
605
|
+
reason: `${shape} — neither excludes zero nor excludes a ${margin} lift; under-powered, keep the corpus growing`,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
export const DEFAULT_MOCK_N = 12;
|
|
609
|
+
export const DEFAULT_MOCK_SEED = 20260729;
|
|
610
|
+
/**
|
|
611
|
+
* Synthetic judge outcomes from ONE seeded stream (`mulberry32` — the repo's only PRNG). Same
|
|
612
|
+
* (n, effect, tieRate, seed) → byte-identical outcomes, so a `--mock` demo is a reproducer.
|
|
613
|
+
*/
|
|
614
|
+
export function generateMockOutcomes(options = {}) {
|
|
615
|
+
const n = clampInt(options.n, DEFAULT_MOCK_N, 0, 100000);
|
|
616
|
+
const effect = clampNumber(options.effect, 0, -1, 1);
|
|
617
|
+
const tieRate = clampNumber(options.tieRate, 0, 0, 1);
|
|
618
|
+
const seed = clampInt(options.seed, DEFAULT_MOCK_SEED, 0, 2 ** 31 - 1);
|
|
619
|
+
const pWarm = Math.max(0, Math.min(1, 0.5 + effect / 2));
|
|
620
|
+
const rand = mulberry32(seed);
|
|
621
|
+
const out = [];
|
|
622
|
+
for (let i = 0; i < n; i++) {
|
|
623
|
+
const tieRoll = rand();
|
|
624
|
+
const armRoll = rand();
|
|
625
|
+
const winner = tieRoll < tieRate ? 'tie' : armRoll < pWarm ? 'warm' : 'cold';
|
|
626
|
+
out.push({ id: `mock-${i}`, class: options.class ?? null, winner });
|
|
627
|
+
}
|
|
628
|
+
return out;
|
|
629
|
+
}
|
|
630
|
+
// ── Rendering ──────────────────────────────────────────────────────────────────────────────────
|
|
631
|
+
function fmtCi(ci) {
|
|
632
|
+
return ci === null ? 'n/a' : `[${ci.lower.toFixed(3)}, ${ci.upper.toFixed(3)}]`;
|
|
633
|
+
}
|
|
634
|
+
export function renderEpochReplayResult(r) {
|
|
635
|
+
const out = [];
|
|
636
|
+
out.push('dz epoch-replay — cold (epoch 0) vs warm (epoch 1), Wilson-CI three-valued verdict');
|
|
637
|
+
out.push('');
|
|
638
|
+
if (r.refusal !== null) {
|
|
639
|
+
out.push(' REFUSED — the input is corrupt, so NO measurement was made:');
|
|
640
|
+
out.push(` ${r.refusal}`);
|
|
641
|
+
return out.join('\n');
|
|
642
|
+
}
|
|
643
|
+
out.push(` SLICE: ${r.slice} · ${r.n} scored instance(s) · ${r.decisive} DECISIVE pair(s)` +
|
|
644
|
+
`${r.ties > 0 ? ` · ${r.ties} tie(s) excluded from the test` : ''}`);
|
|
645
|
+
const ciLabel = r.z === WILSON_Z ? 'CI95' : `CI(z=${r.z})`;
|
|
646
|
+
out.push(` COLD (epoch 0, no injected lessons): ${r.cold.wins}/${r.cold.n} decisive ${ciLabel} ${fmtCi(r.cold.ci)}`);
|
|
647
|
+
out.push(` WARM (epoch 1, apply-leg lessons): ${r.warm.wins}/${r.warm.n} decisive ${ciLabel} ${fmtCi(r.warm.ci)}`);
|
|
648
|
+
out.push(` LIFT (paired, 2p−1 over decisive pairs): ${r.lift === null ? 'n/a' : `${r.lift.d >= 0 ? '+' : ''}${r.lift.d.toFixed(3)} [${r.lift.lower.toFixed(3)}, ${r.lift.upper.toFixed(3)}]`}`);
|
|
649
|
+
out.push('');
|
|
650
|
+
out.push(` VERDICT: ${r.verdict}`);
|
|
651
|
+
out.push(` ${r.reason}`);
|
|
652
|
+
out.push('');
|
|
653
|
+
out.push(` rule: ONE binomial over DECISIVE pairs (ties carry no direction and are excluded).`);
|
|
654
|
+
out.push(` SUPPORTED only when the lift interval lies ENTIRELY above zero.`);
|
|
655
|
+
out.push(` FALSIFIED only on HARM (entirely below zero) or on a passed NON-SUPERIORITY test — the lift UPPER bound below the pre-registered margin ${r.noLiftMargin} at D >= ${r.falsifyNoLiftMinN}.`);
|
|
656
|
+
out.push(` Everything else is INCONCLUSIVE (min ${r.minN} decisive pairs). A tie is UNDER-POWERED, never "refuted".`);
|
|
657
|
+
return out.join('\n');
|
|
658
|
+
}
|
|
659
|
+
export function renderWorkOrderSummary(order, outPath) {
|
|
660
|
+
const withClass = order.items.filter((i) => typeof i.class === 'string' && i.class !== '').length;
|
|
661
|
+
const out = [];
|
|
662
|
+
out.push(`dz epoch-replay --emit → ${outPath}`);
|
|
663
|
+
out.push('');
|
|
664
|
+
out.push(` ${order.items.length} instance(s) · seed ${order.seed} · blind A/B assignment PRE-REGISTERED`);
|
|
665
|
+
out.push(` integrity digest: ${order.digest.slice(0, 16)}… (covers version + seed + every id:warmIsA)`);
|
|
666
|
+
out.push(` slice labels assigned: ${withClass}/${order.items.length}`);
|
|
667
|
+
out.push(` plan length target: ${order.wordMin}-${order.wordMax} words per arm`);
|
|
668
|
+
out.push('');
|
|
669
|
+
out.push(' NEXT (this runner never calls a model):');
|
|
670
|
+
out.push(' 1. Fill coldPlan/warmPlan for every item with an agent, arms generated symmetrically.');
|
|
671
|
+
out.push(` 2. dz epoch-replay --judge ${outPath} → blind judge prompts`);
|
|
672
|
+
out.push(' 3. Have an EXTERNAL (cross-model) judge answer each prompt; collect {id, winner} rows.');
|
|
673
|
+
out.push(` 4. dz epoch-replay --score <judgments.json> --work-order ${outPath}`);
|
|
674
|
+
out.push('');
|
|
675
|
+
out.push(' PRIVACY: the work order embeds raw local prompt texts — keep it out of version control.');
|
|
676
|
+
return out.join('\n');
|
|
677
|
+
}
|
|
678
|
+
export function renderJudgePromptsSummary(result, outPath) {
|
|
679
|
+
const out = [];
|
|
680
|
+
out.push(`dz epoch-replay --judge → ${outPath}`);
|
|
681
|
+
out.push('');
|
|
682
|
+
out.push(` ${result.prompts.length} blind judge prompt(s) rendered — the file carries {id, prompt} and NOTHING else`);
|
|
683
|
+
if (result.skipped.length > 0) {
|
|
684
|
+
out.push(` ${result.skipped.length} item(s) SKIPPED (half a pair is not a comparison):`);
|
|
685
|
+
for (const s of result.skipped)
|
|
686
|
+
out.push(` · ${s.id}: ${s.reason}`);
|
|
687
|
+
out.push(' (this list stays HERE — its reasons name arms, so it is never written to the judge file)');
|
|
688
|
+
}
|
|
689
|
+
out.push('');
|
|
690
|
+
out.push(' Give each prompt to an EXTERNAL judge model (cross-model: not the generator).');
|
|
691
|
+
out.push(' Collect the answers as [{ "id": "<id>", "winner": "A|B|TIE" }, ...] — no arm labels:');
|
|
692
|
+
out.push(' un-blinding uses the work order\'s pre-registered assignment, not anything the judge wrote.');
|
|
693
|
+
return out.join('\n');
|
|
694
|
+
}
|
|
695
|
+
//# sourceMappingURL=epoch-replay.js.map
|