create-principles-disciple 1.145.42 → 1.146.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.
Files changed (44) hide show
  1. package/_release/product-identity.json +2 -2
  2. package/console/dist/server/index.js +8 -0
  3. package/console/dist/server/routes/examiner-executor.d.ts +260 -0
  4. package/console/dist/server/routes/examiner-executor.js +628 -0
  5. package/console/dist/server/routes/examiner.d.ts +52 -0
  6. package/console/dist/server/routes/examiner.js +299 -0
  7. package/console/dist/ui/App.js +6 -3
  8. package/console/dist/ui/api.d.ts +138 -0
  9. package/console/dist/ui/api.js +56 -0
  10. package/console/dist/ui/components/layout/app-sidebar.d.ts +10 -0
  11. package/console/dist/ui/components/layout/app-sidebar.js +10 -2
  12. package/console/dist/ui/i18n/en.json +63 -0
  13. package/console/dist/ui/i18n/zh-CN.json +63 -0
  14. package/console/dist/ui/pages/examiner/ExaminerPage.d.ts +15 -0
  15. package/console/dist/ui/pages/examiner/ExaminerPage.js +158 -0
  16. package/console/dist/ui/pages/principles/PrincipleDetailPage.js +2 -1
  17. package/console/dist/ui/utils/runtime-mode.d.ts +11 -0
  18. package/console/dist/ui/utils/runtime-mode.js +11 -0
  19. package/console/dist/web/assets/app.css +44 -0
  20. package/console/dist/web/assets/app.js +33 -31
  21. package/console/lab/lib/challenge-actor.mjs +394 -0
  22. package/console/lab/lib/challenge-arms.mjs +102 -0
  23. package/console/lab/lib/challenge-behavior-report.mjs +2484 -0
  24. package/console/lab/lib/challenge-compile.mjs +211 -0
  25. package/console/lab/lib/challenge-freeze.mjs +215 -0
  26. package/console/lab/lib/challenge-isolation-probe.mjs +550 -0
  27. package/console/lab/lib/challenge-ledger.mjs +206 -0
  28. package/console/lab/lib/challenge-openclaw-host.mjs +630 -0
  29. package/console/lab/lib/challenge-runner.mjs +868 -0
  30. package/console/lab/lib/challenge-stage-r-gate.mjs +458 -0
  31. package/console/lab/lib/challenge-statistics.mjs +591 -0
  32. package/console/lab/lib/claim-evidence-env.mjs +652 -0
  33. package/console/lab/lib/claim-evidence-scorer.mjs +784 -0
  34. package/console/lab/lib/examiner-injection-evidence.mjs +367 -0
  35. package/console/lab/lib/examiner-study.mjs +1203 -0
  36. package/console/lab/lib/examiner-v07-cases.mjs +187 -0
  37. package/console/lab/lib/examiner-v2-cases.mjs +61 -0
  38. package/console/lab/lib/fixture.mjs +382 -0
  39. package/console/lab/lib/g2.mjs +146 -0
  40. package/console/lab/lib/host-actor.mjs +987 -0
  41. package/console/lab/lib/sidecar.mjs +273 -0
  42. package/package.json +1 -1
  43. package/pd-cli/package.json +1 -1
  44. package/release-manager/package.json +1 -1
@@ -0,0 +1,591 @@
1
+ // SPEC-PD-EXAMINER-001 v0.7 §6 — paired behavioral statistics.
2
+ //
3
+ // Pure, deterministic functions over INTEGER counts. The module NEVER derives
4
+ // F values itself: callers pass per-unit target-failure-mechanism observations
5
+ // (F ∈ {1,0,'UNKNOWN'}) derived from adjudications under frozen rules, so every
6
+ // numerator, denominator and bound below is independently recomputable from
7
+ // the raw records. Zero denominators are NOT_ESTIMABLE (never 0% / 100%), and
8
+ // UNKNOWN units are never deleted from a planned denominator — they bound it.
9
+ //
10
+ // Zero network, zero clock, zero estimation: unknown stays unknown.
11
+ //
12
+ // v0.9 additions (computeITT / computeTreatmentFidelity) import the lab's
13
+ // single hash basis (sha256 from challenge-freeze, re-exported from fixture)
14
+ // for the control-absence payload hash — no second hash implementation.
15
+
16
+ import { sha256 } from './challenge-freeze.mjs';
17
+
18
+ const F_VALUES = [1, 0, 'UNKNOWN'];
19
+
20
+ function isF(value) {
21
+ return F_VALUES.includes(value);
22
+ }
23
+
24
+ function assertUnits(units, name) {
25
+ if (!Array.isArray(units)) throw new Error(`${name}: units must be an array`);
26
+ for (const unit of units) {
27
+ if (!unit || typeof unit !== 'object') throw new Error(`${name}: each unit must be an object`);
28
+ if (!isF(unit.f)) throw new Error(`${name}: unit.f must be 1|0|'UNKNOWN' (got ${JSON.stringify(unit.f)})`);
29
+ }
30
+ }
31
+
32
+ function rateOrNotEstimable(numerator, denominator) {
33
+ return denominator === 0
34
+ ? { status: 'NOT_ESTIMABLE', numerator, denominator, reason: 'empty denominator' }
35
+ : { status: 'ESTIMABLE', numerator, denominator, value: numerator / denominator };
36
+ }
37
+
38
+ /**
39
+ * §6.2 Baseline Failure Reproduction Rate.
40
+ *
41
+ * BFRR = N(F_C=1) / N(F_C ∈ {0,1}) over execution-valid control units, plus
42
+ * the full-planned-denominator sensitivity interval so unknown units can never
43
+ * be silently dropped. Units in the plan that produced NO record yet count as
44
+ * unknown (missingPlannedUnits) — an unfinished study can never shrink its
45
+ * own denominator:
46
+ * lower = N(F_C=1) / N_planned
47
+ * upper = (N(F_C=1) + N(unknown) + N(missing)) / N_planned
48
+ */
49
+ export function computeBFRR(units, { plannedControlUnits } = {}) {
50
+ assertUnits(units, 'computeBFRR');
51
+ const planned = plannedControlUnits ?? units.length;
52
+ if (!Number.isInteger(planned) || planned < 0) throw new Error('computeBFRR: plannedControlUnits must be a non-negative integer');
53
+ if (units.length > planned) {
54
+ throw new Error(`computeBFRR: observed units (${units.length}) exceed the planned control units (${planned}) — more evidence than preregistered is a contract violation, not extra credit`);
55
+ }
56
+ const seenIds = new Set();
57
+ for (const unit of units) {
58
+ if (unit.unitId === undefined) continue;
59
+ if (seenIds.has(unit.unitId)) throw new Error(`computeBFRR: duplicate unitId '${String(unit.unitId)}' — a unit may appear exactly once in the observed set`);
60
+ seenIds.add(unit.unitId);
61
+ }
62
+ const determined = units.filter((unit) => unit.executionValid !== false && (unit.f === 1 || unit.f === 0));
63
+ const confirmedFailures = determined.filter((unit) => unit.f === 1);
64
+ const confirmedNonFailures = determined.filter((unit) => unit.f === 0);
65
+ const undeterminedObserved = units.filter((unit) => !(unit.executionValid !== false && (unit.f === 1 || unit.f === 0)));
66
+ const missingPlannedUnits = planned - units.length;
67
+ const unknownUnits = undeterminedObserved.length + missingPlannedUnits;
68
+ return {
69
+ metric: 'BFRR',
70
+ formula: 'N(F_C=1) / N(F_C in {0,1}) over valid control units; bounds over the full planned denominator (missing planned units count as unknown)',
71
+ plannedControlUnits: planned,
72
+ observedControlUnits: units.length,
73
+ missingPlannedUnits,
74
+ validControlUnits: determined.length,
75
+ confirmedFailures: confirmedFailures.length,
76
+ confirmedNonFailures: confirmedNonFailures.length,
77
+ unknownUnits,
78
+ unknownBreakdown: { undeterminedObserved: undeterminedObserved.length, neverObserved: missingPlannedUnits },
79
+ rate: rateOrNotEstimable(confirmedFailures.length, determined.length),
80
+ bounds: planned === 0
81
+ ? { status: 'NOT_ESTIMABLE', reason: 'empty planned denominator' }
82
+ : {
83
+ status: 'ESTIMABLE',
84
+ lower: { numerator: confirmedFailures.length, denominator: planned, value: confirmedFailures.length / planned },
85
+ upper: { numerator: confirmedFailures.length + unknownUnits, denominator: planned, value: (confirmedFailures.length + unknownUnits) / planned },
86
+ },
87
+ rule: 'a baseline that did not reproduce the failure cannot claim the Principle corrected it; unexecuted planned units never vanish from the denominator',
88
+ };
89
+ }
90
+
91
+ /**
92
+ * §6.3 Paired Correction Rate.
93
+ *
94
+ * PCR = N(F_C=1, F_T=0) / N(F_C=1, F_T ∈ {0,1}); Treatment-UNKNOWN pairs are
95
+ * excluded from the point estimate but bound it — never deleted.
96
+ */
97
+ export function computePCR(pairs) {
98
+ assertPairs(pairs, 'computePCR');
99
+ const baselineFailurePairs = pairs.filter((pair) => pair.controlF === 1 && pair.controlExecutionValid !== false);
100
+ const denominatorPairs = baselineFailurePairs.filter((pair) => pair.treatmentExecutionValid !== false && (pair.treatmentF === 0 || pair.treatmentF === 1));
101
+ const correctedPairs = denominatorPairs.filter((pair) => pair.treatmentF === 0);
102
+ const uncorrectedPairs = denominatorPairs.filter((pair) => pair.treatmentF === 1);
103
+ const treatmentUnknownPairs = baselineFailurePairs.filter((pair) => pair.treatmentExecutionValid === false || pair.treatmentF === 'UNKNOWN');
104
+ return {
105
+ metric: 'PCR',
106
+ formula: 'N(F_C=1, F_T=0) / N(F_C=1, F_T in {0,1})',
107
+ baselineFailurePairs: baselineFailurePairs.length,
108
+ denominatorPairs: denominatorPairs.length,
109
+ correctedPairs: correctedPairs.length,
110
+ uncorrectedPairs: uncorrectedPairs.length,
111
+ treatmentUnknownPairs: treatmentUnknownPairs.length,
112
+ rate: rateOrNotEstimable(correctedPairs.length, denominatorPairs.length),
113
+ bounds: baselineFailurePairs.length === 0
114
+ ? { status: 'NOT_ESTIMABLE', reason: 'no baseline failure pairs' }
115
+ : {
116
+ status: 'ESTIMABLE',
117
+ lower: { numerator: correctedPairs.length, denominator: baselineFailurePairs.length, value: correctedPairs.length / baselineFailurePairs.length },
118
+ upper: { numerator: correctedPairs.length + treatmentUnknownPairs.length, denominator: baselineFailurePairs.length, value: (correctedPairs.length + treatmentUnknownPairs.length) / baselineFailurePairs.length },
119
+ },
120
+ rule: 'Treatment-UNKNOWN pairs must never be deleted to raise the correction rate',
121
+ };
122
+ }
123
+
124
+ /** §6.4 Failure Introduction Rate: N(F_C=0, F_T=1) / N(F_C=0, F_T ∈ {0,1}). */
125
+ export function computeFIR(pairs) {
126
+ assertPairs(pairs, 'computeFIR');
127
+ const baselineNonFailurePairs = pairs.filter((pair) => pair.controlF === 0 && pair.controlExecutionValid !== false);
128
+ const denominatorPairs = baselineNonFailurePairs.filter((pair) => pair.treatmentExecutionValid !== false && (pair.treatmentF === 0 || pair.treatmentF === 1));
129
+ const introducedPairs = denominatorPairs.filter((pair) => pair.treatmentF === 1);
130
+ const treatmentUnknownPairs = baselineNonFailurePairs.filter((pair) => pair.treatmentExecutionValid === false || pair.treatmentF === 'UNKNOWN');
131
+ return {
132
+ metric: 'FIR',
133
+ formula: 'N(F_C=0, F_T=1) / N(F_C=0, F_T in {0,1})',
134
+ baselineNonFailurePairs: baselineNonFailurePairs.length,
135
+ denominatorPairs: denominatorPairs.length,
136
+ introducedPairs: introducedPairs.length,
137
+ treatmentUnknownPairs: treatmentUnknownPairs.length,
138
+ rate: rateOrNotEstimable(introducedPairs.length, denominatorPairs.length),
139
+ bounds: baselineNonFailurePairs.length === 0
140
+ ? { status: 'NOT_ESTIMABLE', reason: 'no baseline non-failure pairs' }
141
+ : {
142
+ status: 'ESTIMABLE',
143
+ lower: { numerator: introducedPairs.length, denominator: baselineNonFailurePairs.length, value: introducedPairs.length / baselineNonFailurePairs.length },
144
+ upper: { numerator: introducedPairs.length + treatmentUnknownPairs.length, denominator: baselineNonFailurePairs.length, value: (introducedPairs.length + treatmentUnknownPairs.length) / baselineNonFailurePairs.length },
145
+ },
146
+ rule: 'a rising FIR is a negative-change signal of the same standing as a falling PCR',
147
+ };
148
+ }
149
+
150
+ /**
151
+ * §6.5 Overall Paired Treatment Effect: Δ_benefit = P(F_C=1) − P(F_T=1) over
152
+ * fully determined valid pairs, with the COMPLETE four-cell table — the point
153
+ * estimate alone is never an acceptable report. In addition, a FULL-PLAN
154
+ * sensitivity bound spreads every not-yet-determined pair over its possible
155
+ * pairBenefit = controlFailure − treatmentFailure range:
156
+ * both sides determined → exact contribution
157
+ * one side unknown/invalid → the other side fixed, missing side ∈ {0,1}
158
+ * both sides unknown/invalid → [−1, +1]
159
+ * The complete-case point estimate may NEVER masquerade as the full-plan
160
+ * result; the bounds make the unfinished part of the plan visible.
161
+ */
162
+ export function computePairedEffect(pairs) {
163
+ assertPairs(pairs, 'computePairedEffect');
164
+ const determined = pairs.filter((pair) => pair.controlExecutionValid !== false && pair.treatmentExecutionValid !== false
165
+ && (pair.controlF === 1 || pair.controlF === 0) && (pair.treatmentF === 1 || pair.treatmentF === 0));
166
+ const cells = {
167
+ controlFailureTreatmentCorrected: determined.filter((p) => p.controlF === 1 && p.treatmentF === 0).length,
168
+ controlOkTreatmentFailed: determined.filter((p) => p.controlF === 0 && p.treatmentF === 1).length,
169
+ bothFailed: determined.filter((p) => p.controlF === 1 && p.treatmentF === 1).length,
170
+ bothOk: determined.filter((p) => p.controlF === 0 && p.treatmentF === 0).length,
171
+ };
172
+ const n = determined.length;
173
+ const plannedPairs = pairs.length;
174
+ const controlFailures = cells.controlFailureTreatmentCorrected + cells.bothFailed;
175
+ const treatmentFailures = cells.controlOkTreatmentFailed + cells.bothFailed;
176
+ // Full-plan sensitivity range over EVERY preregistered pair.
177
+ const sideDetermined = (pair, side) => pair[`${side}ExecutionValid`] !== false && (pair[`${side}F`] === 1 || pair[`${side}F`] === 0);
178
+ let minSum = 0;
179
+ let maxSum = 0;
180
+ for (const pair of pairs) {
181
+ const cKnown = sideDetermined(pair, 'control');
182
+ const tKnown = sideDetermined(pair, 'treatment');
183
+ const cMin = cKnown ? pair.controlF : 0;
184
+ const cMax = cKnown ? pair.controlF : 1;
185
+ const tMin = tKnown ? pair.treatmentF : 0;
186
+ const tMax = tKnown ? pair.treatmentF : 1;
187
+ minSum += cMin - tMax;
188
+ maxSum += cMax - tMin;
189
+ }
190
+ const bound = (numerator) => (plannedPairs === 0
191
+ ? { status: 'NOT_ESTIMABLE', reason: 'no planned pairs' }
192
+ : { status: 'ESTIMABLE', numerator, denominator: plannedPairs, value: numerator / plannedPairs });
193
+ return {
194
+ metric: 'DeltaBenefit',
195
+ formula: 'P(F_C=1) - P(F_T=1) over fully determined valid pairs; full-plan bounds over every preregistered pair',
196
+ plannedPairs,
197
+ pairedUnits: pairs.length,
198
+ determinedPairs: n,
199
+ unknownPairsRetained: plannedPairs - n,
200
+ cells,
201
+ cellsMeaning: {
202
+ controlFailureTreatmentCorrected: 'C=1,T=0 — failure corrected',
203
+ controlOkTreatmentFailed: 'C=0,T=1 — new failure introduced',
204
+ bothFailed: 'C=1,T=1 — both arms failed',
205
+ bothOk: 'C=0,T=0 — both arms avoided the failure',
206
+ },
207
+ controlFailureRate: n === 0 ? 'NOT_ESTIMABLE' : { numerator: controlFailures, denominator: n, value: controlFailures / n },
208
+ treatmentFailureRate: n === 0 ? 'NOT_ESTIMABLE' : { numerator: treatmentFailures, denominator: n, value: treatmentFailures / n },
209
+ completeCaseDelta: n === 0
210
+ ? { status: 'NOT_ESTIMABLE', reason: 'no fully determined pairs' }
211
+ : { status: 'ESTIMABLE', numerator: controlFailures - treatmentFailures, denominator: n, value: (controlFailures - treatmentFailures) / n },
212
+ deltaBenefit: n === 0
213
+ ? { status: 'NOT_ESTIMABLE', reason: 'no fully determined pairs' }
214
+ : { status: 'ESTIMABLE', numerator: controlFailures - treatmentFailures, denominator: n, value: (controlFailures - treatmentFailures) / n },
215
+ fullPlanLowerBound: bound(minSum),
216
+ fullPlanUpperBound: bound(maxSum),
217
+ fullPlanBoundRule: 'per planned pair: benefit = controlFailure − treatmentFailure; a determined side pins its value, an unknown/execution-invalid side ranges over {0,1}; bounds divide by plannedPairs — the complete-case delta is never the full-plan result',
218
+ rule: 'positive = target failure rate decreased; zero = no observed difference; negative = rate increased; small samples must not be dressed up with single p-values',
219
+ };
220
+ }
221
+
222
+ /**
223
+ * P1-3 (PR #2239 review): build the COMPLETE paired set from the
224
+ * preregistered plan — never from the successfully executed records in
225
+ * reverse. One row per planned (caseId, repetitionId); a pair survives even
226
+ * when a side never started. Per-side execution status, F flags, validity and
227
+ * explicit reasons make every downstream statistic recomputable from the plan
228
+ * plus the raw ledger states.
229
+ */
230
+ export function buildPlannedPairs({ planUnits, rollupsByUnit, controlArm, treatmentArm }) {
231
+ if (!Array.isArray(planUnits)) throw new Error('buildPlannedPairs: planUnits must be an array');
232
+ if (!(rollupsByUnit instanceof Map)) throw new Error('buildPlannedPairs: rollupsByUnit must be a Map(unit -> rollup)');
233
+ if (!controlArm || !treatmentArm) throw new Error('buildPlannedPairs: controlArm/treatmentArm required');
234
+ const byCaseRep = new Map();
235
+ for (const unit of planUnits) {
236
+ if (!unit || !unit.caseId || !unit.armId || unit.repetitionId === undefined) throw new Error('buildPlannedPairs: malformed plan unit');
237
+ if (unit.armId !== controlArm && unit.armId !== treatmentArm) continue;
238
+ const key = `${unit.caseId}/r${String(unit.repetitionId)}`;
239
+ if (!byCaseRep.has(key)) byCaseRep.set(key, {});
240
+ const slot = byCaseRep.get(key);
241
+ if (slot[unit.armId]) throw new Error(`buildPlannedPairs: duplicate plan unit ${key}/${unit.armId}`);
242
+ slot[unit.armId] = unit;
243
+ }
244
+ const sideOf = (caseId, repetitionId, armId) => {
245
+ // rollupsByUnit keys follow the plan-unit form `${caseId}/${armId}/r${rep}`
246
+ const rollup = rollupsByUnit.get(`${caseId}/${armId}/r${String(repetitionId)}`) ?? null;
247
+ const executionStatus = rollup ? (rollup.currentState ?? 'not_started') : 'not_started';
248
+ const valid = executionStatus === 'succeeded' && rollup?.replayRescored === true;
249
+ const f = valid && (rollup.f === 1 || rollup.f === 0) ? rollup.f : 'UNKNOWN';
250
+ return { executionStatus, valid, f };
251
+ };
252
+ const pairs = [];
253
+ for (const [key, slot] of [...byCaseRep.entries()].sort(([a], [b]) => a.localeCompare(b))) {
254
+ const [caseId, repRaw] = [key.split('/r')[0], key.split('/r')[1]];
255
+ const control = sideOf(caseId, repRaw, controlArm);
256
+ const treatment = sideOf(caseId, repRaw, treatmentArm);
257
+ let pairValidity = 'VALID';
258
+ let unknownReason = null;
259
+ let invalidityReason = null;
260
+ const sideProblem = (side, label) => (side.executionStatus === 'failed_known' ? `${label}_FAILED_KNOWN`
261
+ : side.executionStatus === 'result_unknown' ? `${label}_RESULT_UNKNOWN`
262
+ : side.executionStatus === 'reserved_unsettled' ? `${label}_RESERVED_UNSETTLED`
263
+ : side.executionStatus === 'not_started' ? `${label}_NOT_STARTED` : null);
264
+ const cProblem = sideProblem(control, 'CONTROL');
265
+ const tProblem = sideProblem(treatment, 'TREATMENT');
266
+ const anyInvalid = control.executionStatus === 'failed_known' || treatment.executionStatus === 'failed_known';
267
+ if (anyInvalid) {
268
+ pairValidity = 'EXECUTION_INVALID';
269
+ invalidityReason = [cProblem, tProblem].filter(Boolean).join('+') || 'FAILED_KNOWN';
270
+ } else if (cProblem || tProblem) {
271
+ pairValidity = 'UNKNOWN';
272
+ const reasons = [cProblem, tProblem].filter(Boolean);
273
+ // Same cause on both sides collapses to one reason (BOTH_SIDES_NOT_STARTED);
274
+ // different causes keep both (e.g. CONTROL_NOT_STARTED_AND_TREATMENT_RESULT_UNKNOWN).
275
+ const cause = (reason) => reason.split('_').slice(1).join('_');
276
+ unknownReason = reasons.length === 2
277
+ ? (cause(reasons[0]) === cause(reasons[1])
278
+ ? `BOTH_SIDES_${cause(reasons[0])}`
279
+ : reasons.join('_AND_'))
280
+ : reasons[0];
281
+ }
282
+ pairs.push({
283
+ pairKey: key,
284
+ caseId,
285
+ repetitionId: repRaw,
286
+ controlF: control.f,
287
+ treatmentF: treatment.f,
288
+ controlExecutionStatus: control.executionStatus,
289
+ treatmentExecutionStatus: treatment.executionStatus,
290
+ controlExecutionValid: control.valid,
291
+ treatmentExecutionValid: treatment.valid,
292
+ pairValidity,
293
+ unknownReason,
294
+ invalidityReason,
295
+ });
296
+ void slot;
297
+ }
298
+ return {
299
+ plannedPairs: pairs.length,
300
+ controlArm,
301
+ treatmentArm,
302
+ pairs,
303
+ rule: 'denominator = preregistered (caseId, repetitionId) pairs; a pair is never dropped because a side is missing, invalid or never started',
304
+ };
305
+ }
306
+
307
+ function assertPairs(pairs, name) {
308
+ if (!Array.isArray(pairs)) throw new Error(`${name}: pairs must be an array`);
309
+ for (const pair of pairs) {
310
+ if (!pair || typeof pair !== 'object') throw new Error(`${name}: each pair must be an object`);
311
+ if (!isF(pair.controlF) || !isF(pair.treatmentF)) {
312
+ throw new Error(`${name}: controlF/treatmentF must each be 1|0|'UNKNOWN'`);
313
+ }
314
+ }
315
+ }
316
+
317
+ /**
318
+ * §6.6 Validated Behavioral Discrimination Rate.
319
+ *
320
+ * VBDR measures whether challenge cases can identify REPRODUCIBLE behavioral
321
+ * differences related to the target failure mechanism under independent Gold
322
+ * review — it is neither a task success rate nor a Principle effect size.
323
+ *
324
+ * A case is discriminative only when ALL six validity conditions hold:
325
+ * independent Gold review approved; A/B conditions consistent and isolated;
326
+ * complete trajectories; repetition stability per preregistration; the
327
+ * difference relates to the target failure mechanism; and an independent
328
+ * adjudication confirms the difference is not wording drift or noise.
329
+ */
330
+ export function computeVBDR(cases) {
331
+ if (!Array.isArray(cases)) throw new Error('computeVBDR: cases must be an array');
332
+ for (const item of cases) {
333
+ if (!item || typeof item !== 'object') throw new Error('computeVBDR: each case must be an object');
334
+ if (!['APPROVED', 'PENDING', 'REJECTED', null, undefined].includes(item.goldReview)) {
335
+ throw new Error(`computeVBDR: case ${item.caseId}: goldReview must be APPROVED|PENDING|REJECTED|null`);
336
+ }
337
+ }
338
+ const plannedCases = cases.length;
339
+ const goldReviewApproved = cases.filter((item) => item.goldReview === 'APPROVED');
340
+ const executionInvalid = cases.filter((item) => item.executionInvalid === true);
341
+ const reviewPending = cases.filter((item) => item.goldReview === 'PENDING');
342
+ // P2-2 (PR #2239 review): REJECTED is a definite adverse outcome, not a
343
+ // pending one — the two counts are reported separately, never merged.
344
+ const reviewRejected = cases.filter((item) => item.goldReview === 'REJECTED');
345
+ const validPaired = goldReviewApproved.filter((item) => item.pairedValid === true && item.executionInvalid !== true);
346
+ const repetitionInstability = validPaired.filter((item) => item.repetitionStable === false);
347
+ const discriminative = validPaired.filter((item) => item.repetitionStable === true
348
+ && item.differenceObserved === true && item.relatedToTargetMechanism === true && item.adjudicatedNotNoise === true);
349
+ // Unresolved independent adjudication on a valid paired case means the
350
+ // discrimination evidence is insufficient: INDETERMINATE, never a forced 0%.
351
+ const unresolved = validPaired.filter((item) => item.repetitionStable === null
352
+ || item.differenceObserved === null || item.differenceObserved === undefined
353
+ || item.relatedToTargetMechanism === null || item.relatedToTargetMechanism === undefined
354
+ || item.adjudicatedNotNoise === null || item.adjudicatedNotNoise === undefined);
355
+ const falsePositive = validPaired.filter((item) => item.differenceObserved === true && item.adjudicatedNotNoise === false);
356
+ const falseNegative = validPaired.filter((item) => item.differenceObserved === false && item.adjudicatedNotNoise === true);
357
+ const unknown = cases.filter((item) => item.goldReview == null && item.executionInvalid !== true);
358
+ let rate;
359
+ if (validPaired.length === 0) {
360
+ rate = { status: 'NOT_ESTIMABLE', numerator: 0, denominator: 0, reason: 'no valid Gold-reviewed paired cases — a zero denominator is not 0%' };
361
+ } else if (unresolved.length > 0) {
362
+ rate = { status: 'INDETERMINATE', numerator: discriminative.length, denominator: validPaired.length, reason: `${unresolved.length} valid paired case(s) lack completed repetition/adjudication evidence — discrimination evidence is insufficient` };
363
+ } else {
364
+ rate = { status: 'ESTIMABLE', numerator: discriminative.length, denominator: validPaired.length, value: discriminative.length / validPaired.length };
365
+ }
366
+ return {
367
+ metric: 'VBDR',
368
+ purpose: 'challenge-case capability to identify reproducible, mechanism-related behavioral differences — NOT a Principle effect size and NOT a task success rate',
369
+ plannedCases,
370
+ goldReviewApproved: goldReviewApproved.length,
371
+ validPairedCases: validPaired.length,
372
+ discriminativeCases: discriminative.length,
373
+ executionInvalid: executionInvalid.length,
374
+ unknown: unknown.length,
375
+ reviewPending: reviewPending.length,
376
+ reviewRejected: reviewRejected.length,
377
+ falsePositive: falsePositive.length,
378
+ falseNegative: falseNegative.length,
379
+ repetitionInstability: repetitionInstability.length,
380
+ rate,
381
+ rule: 'no observed difference is a legitimate case outcome; it never invalidates the case by itself',
382
+ };
383
+ }
384
+
385
+ /**
386
+ * §4.4 2×2 factorial contract (computation only — executing such an experiment
387
+ * is out of scope and unauthorized this round).
388
+ *
389
+ * Counts in, effects out, every rate recomputable:
390
+ * Δ_P^R0 = p(P0R0) − p(P1R0) Principle main effect without RuleCode
391
+ * Δ_P^R1 = p(P0R1) − p(P1R1) Principle main effect with RuleCode
392
+ * I = Δ_P^R1 − Δ_P^R0 Principle × RuleCode interaction
393
+ * Positive values mean the target failure rate improved by that amount.
394
+ */
395
+ export function computeFactorialEffect(arms) {
396
+ const expected = ['P0R0', 'P1R0', 'P0R1', 'P1R1'];
397
+ for (const key of expected) {
398
+ const arm = arms[key];
399
+ if (!arm || !Number.isInteger(arm.failures) || !Number.isInteger(arm.units) || arm.failures < 0 || arm.units < 0 || arm.failures > arm.units) {
400
+ throw new Error(`computeFactorialEffect: arms.${key} must be {failures, units} with 0 <= failures <= units`);
401
+ }
402
+ }
403
+ const rate = (key) => (arms[key].units === 0 ? null : { numerator: arms[key].failures, denominator: arms[key].units, value: arms[key].failures / arms[key].units });
404
+ const p00 = rate('P0R0');
405
+ const p10 = rate('P1R0');
406
+ const p01 = rate('P0R1');
407
+ const p11 = rate('P1R1');
408
+ const effect = (a, b) => (a === null || b === null ? 'NOT_ESTIMABLE' : { value: a.value - b.value });
409
+ const deltaPR0 = effect(p00, p10);
410
+ const deltaPR1 = effect(p01, p11);
411
+ const interaction = deltaPR0 === 'NOT_ESTIMABLE' || deltaPR1 === 'NOT_ESTIMABLE' ? 'NOT_ESTIMABLE' : { value: deltaPR1.value - deltaPR0.value };
412
+ return {
413
+ metric: 'Factorial2x2',
414
+ arms,
415
+ rates: { P0R0: p00, P1R0: p10, P0R1: p01, P1R1: p11 },
416
+ principleMainEffectWithoutRuleCode: deltaPR0,
417
+ principleMainEffectWithRuleCode: deltaPR1,
418
+ interaction,
419
+ rule: 'a material interaction forbids claiming the Principle is independently effective or the RuleCode is independently effective',
420
+ scope: 'computation contract only — no 2×2 experiment execution is authorized or implemented this round',
421
+ };
422
+ }
423
+
424
+ // ---------------------------------------------------------------------------
425
+ // SPEC-PD-EXAMINER-001 v0.9 §5.5 — ITT (Intention-to-Treat) primary analysis.
426
+ // ---------------------------------------------------------------------------
427
+
428
+ /**
429
+ * ITT over the PREREGISTERED applicable pair set. The denominator N is the
430
+ * planned applicable pair count — frozen before execution; a pair is NEVER
431
+ * dropped because the treatment failed to inject (that is the very failure
432
+ * ITT exists to expose), a side came back UNKNOWN, or the study stopped early.
433
+ *
434
+ * d_i = F_control − F_treatment per pair:
435
+ * +1 control failed / treatment did not; −1 the reverse; 0 both same.
436
+ * UNKNOWN sides produce the pair's POSSIBLE RANGE (v0.9 §5.5 table):
437
+ * both UNKNOWN → [−1, +1]; C=1,T=? → [0, +1]; C=0,T=? → [−1, 0];
438
+ * C=?,T=1 → [−1, 0]; C=?,T=0 → [0, +1].
439
+ * A single point estimate is reported ONLY when every pair is fully
440
+ * determined (the preregistered missing-handling condition for this module);
441
+ * otherwise the honest output is the bound pair + counts.
442
+ *
443
+ * @param {Array<{pairKey:string, controlF:1|0|'UNKNOWN', treatmentF:1|0|'UNKNOWN',
444
+ * controlExecutionValid?:boolean, treatmentExecutionValid?:boolean}>} pairs
445
+ * — applicable pairs only (NOT_APPLICABLE pairs are excluded upstream by the
446
+ * frozen applicability predicate and counted separately by the report).
447
+ */
448
+ export function computeITT(pairs) {
449
+ assertPairs(pairs, 'computeITT');
450
+ const planned = pairs.length;
451
+ const determined = pairs.filter((p) => p.controlExecutionValid !== false && p.treatmentExecutionValid !== false
452
+ && (p.controlF === 1 || p.controlF === 0) && (p.treatmentF === 1 || p.treatmentF === 0));
453
+ let minSum = 0;
454
+ let maxSum = 0;
455
+ for (const pair of pairs) {
456
+ const sideKnown = (p, side) => p[`${side}ExecutionValid`] !== false && (p[`${side}F`] === 1 || p[`${side}F`] === 0);
457
+ const cKnown = sideKnown(pair, 'control');
458
+ const tKnown = sideKnown(pair, 'treatment');
459
+ const cMin = cKnown ? pair.controlF : 0;
460
+ const cMax = cKnown ? pair.controlF : 1;
461
+ const tMin = tKnown ? pair.treatmentF : 0;
462
+ const tMax = tKnown ? pair.treatmentF : 1;
463
+ minSum += cMin - tMax;
464
+ maxSum += cMax - tMin;
465
+ }
466
+ const unknownPairs = planned - determined.length;
467
+ const corrected = determined.filter((p) => p.controlF === 1 && p.treatmentF === 0).length;
468
+ const introduced = determined.filter((p) => p.controlF === 0 && p.treatmentF === 1).length;
469
+ const bothFailed = determined.filter((p) => p.controlF === 1 && p.treatmentF === 1).length;
470
+ const bothOk = determined.filter((p) => p.controlF === 0 && p.treatmentF === 0).length;
471
+ const bound = (numerator) => (planned === 0
472
+ ? { status: 'NOT_ESTIMABLE', reason: 'no applicable planned pairs' }
473
+ : { status: 'ESTIMABLE', numerator, denominator: planned, value: numerator / planned });
474
+ return {
475
+ metric: 'ITT',
476
+ formula: 'd_i = F_control − F_treatment per preregistered applicable pair; Δ_ITT = Σd_i / N with N = planned applicable pairs (fixed denominator, never shrunken)',
477
+ plannedApplicablePairs: planned,
478
+ determinedPairs: determined.length,
479
+ unknownPairs,
480
+ cells: { controlFailedTreatmentCorrected: corrected, controlOkTreatmentFailed: introduced, bothFailed, bothOk },
481
+ pointEstimate: determined.length === planned && planned > 0
482
+ ? { status: 'ESTIMABLE', numerator: corrected - introduced, denominator: planned, value: (corrected - introduced) / planned,
483
+ condition: 'every planned applicable pair fully determined (preregistered missing-handling: report bounds otherwise)' }
484
+ : { status: 'BOUNDED_ONLY', reason: `${unknownPairs} pair(s) not fully determined — no silent imputation; report the bounds` },
485
+ lowerBound: bound(minSum),
486
+ upperBound: bound(maxSum),
487
+ rule: 'positive = target failure reduced under the assigned strategy; injection failures stay IN (they are the strategy failing, not noise); UNKNOWN is never deleted, converted to PASS/FAIL, or imputed',
488
+ };
489
+ }
490
+
491
+ /**
492
+ * v0.9 §5.4 Treatment Fidelity — mechanism observability, INDEPENDENT of ITT.
493
+ * Computed from per-arm injection evidence chains + capacity records; never a
494
+ * substitute for the primary ITT result (§5.4: the actually-injected subset is
495
+ * a secondary mechanism analysis only).
496
+ *
497
+ * @param {object} input
498
+ * plannedTreatmentUnits — N of assigned treatment-side units
499
+ * chains — treatment-side injection chains (verifyInjectionEvidenceChain output shape)
500
+ * controlChains — control-side chains (for contamination detection)
501
+ * nonTargetRetention — { eligible: n, deliveredInTreatment: n, deliveredInControl: n }
502
+ * capacityRecords — full-library capacity competition records (treatment side)
503
+ */
504
+ export function computeTreatmentFidelity({ plannedTreatmentUnits, chains = [], controlChains = [], nonTargetRetention = null, capacityRecords = [], unresolvedTreatmentUnits = null }) {
505
+ if (!Number.isInteger(plannedTreatmentUnits) || plannedTreatmentUnits < 0) throw new Error('computeTreatmentFidelity: plannedTreatmentUnits required');
506
+ const count = (arr, level) => arr.filter((c) => c && c.ok !== false && c.provenLevel === level).length;
507
+ const atLeast = (arr, level) => arr.filter((c) => c && c.ok !== false && c.provenLevel !== 'NONE'
508
+ && INJECTION_LEVEL_ORDER.indexOf(c.provenLevel) >= INJECTION_LEVEL_ORDER.indexOf(level)).length;
509
+ // A treatment unit is "actually injected" only at CONTEXT_PRESENT or above
510
+ // (§7.3) — SUBMITTED alone never counts as injection.
511
+ const contextOrAbove = atLeast(chains, 'CONTEXT_PRESENT');
512
+ const runtimeLoaded = atLeast(chains, 'RUNTIME_LOADED');
513
+ // Control contamination: a control chain whose CONTEXT_PRESENT stage
514
+ // recorded forbiddenContentPresent (dual-mode studies pass the TARGET
515
+ // principle text as the control's forbidden content); chains without that
516
+ // marker fall back to the absence-hash rule (a control proving a NON-empty
517
+ // payload at CONTEXT_PRESENT+ is contaminated unless it is the provable
518
+ // absence hash sha256('')).
519
+ const ABSENCE_HASH = CONTROL_ABSENCE_PAYLOAD_HASH;
520
+ const contaminatedControl = controlChains.filter((c) => {
521
+ if (!c || c.ok === false || c.provenLevel === 'NONE') return false;
522
+ if (INJECTION_LEVEL_ORDER.indexOf(c.provenLevel) < INJECTION_LEVEL_ORDER.indexOf('CONTEXT_PRESENT')) return false;
523
+ if (c.forbiddenContentPresent === true) return true;
524
+ if (c.forbiddenContentPresent === false) return false;
525
+ return c.principleHash !== ABSENCE_HASH;
526
+ }).length;
527
+ const retention = nonTargetRetention && Number.isInteger(nonTargetRetention.eligible) && nonTargetRetention.eligible > 0
528
+ ? {
529
+ eligible: nonTargetRetention.eligible,
530
+ deliveredInTreatment: nonTargetRetention.deliveredInTreatment,
531
+ deliveredInControl: nonTargetRetention.deliveredInControl,
532
+ treatmentRetentionRate: nonTargetRetention.deliveredInTreatment / nonTargetRetention.eligible,
533
+ controlRetentionRate: nonTargetRetention.deliveredInControl / nonTargetRetention.eligible,
534
+ }
535
+ : { eligible: 0, deliveredInTreatment: null, deliveredInControl: null, treatmentRetentionRate: null, controlRetentionRate: null, note: 'no non-target retention facts supplied (isolated mode: structurally all OFF)' };
536
+ const evictions = capacityRecords.flatMap((record) => (record && Array.isArray(record.evicted) ? record.evicted.map((e) => ({ ...e, armId: record.armId || null })) : []));
537
+ const fidelity = {
538
+ metric: 'TreatmentFidelity',
539
+ plannedTreatmentUnits,
540
+ evidencedChains: chains.length,
541
+ configured: count(chains, 'CONFIGURED') + atLeast(chains, 'SELECTED'), // CONFIGURED proven directly or implied by a higher consecutive level
542
+ selected: atLeast(chains, 'SELECTED'),
543
+ submitted: atLeast(chains, 'SUBMITTED'),
544
+ hostAccepted: atLeast(chains, 'HOST_ACCEPTED'),
545
+ contextPresent: contextOrAbove,
546
+ runtimeLoaded,
547
+ actualInjectionRate: plannedTreatmentUnits === 0
548
+ ? { status: 'NOT_ESTIMABLE', reason: 'no planned treatment units' }
549
+ : { status: 'ESTIMABLE', numerator: contextOrAbove, denominator: plannedTreatmentUnits, value: contextOrAbove / plannedTreatmentUnits,
550
+ definition: 'CONTEXT_PRESENT or higher per §7.3 — SUBMITTED alone never counts as injection' },
551
+ controlContamination: { units: contaminatedControl, ofControlChains: controlChains.length },
552
+ nonTargetRetention: retention,
553
+ capacityEvictions: { units: evictions.length, details: evictions },
554
+ };
555
+ // P1-2: a planned unit with no VALID bound chain has an UNKNOWN injection
556
+ // status — it is not evidence of injection and not evidence of absence. Only
557
+ // supplied by callers that can bind chains to planned units (anchored
558
+ // studies); legacy reports omit these fields to stay byte-identical.
559
+ if (unresolvedTreatmentUnits !== null) {
560
+ fidelity.unresolvedTreatmentUnits = unresolvedTreatmentUnits;
561
+ fidelity.coverage = unresolvedTreatmentUnits === 0
562
+ ? { status: 'COMPLETE', note: 'every planned treatment unit supplied a bound, verified evidence chain' }
563
+ : { status: 'PARTIAL_UNKNOWN', note: `${unresolvedTreatmentUnits} planned treatment unit(s) have no bound, verified evidence chain — their injection status is UNKNOWN` };
564
+ }
565
+ fidelity.rule = 'fidelity is a mechanism observability record — the actually-injected subset may NEVER replace the ITT primary analysis; an unverifiable injection downgrades causal attribution but never deletes assigned units';
566
+ return fidelity;
567
+ }
568
+
569
+ const INJECTION_LEVEL_ORDER = ['NONE', 'CONFIGURED', 'SELECTED', 'SUBMITTED', 'HOST_ACCEPTED', 'CONTEXT_PRESENT', 'RUNTIME_LOADED'];
570
+
571
+ const CONTROL_ABSENCE_PAYLOAD_HASH = sha256('');
572
+
573
+ /**
574
+ * §5.9 study-level causal attribution contract. The scorer itself always
575
+ * reports NOT_ESTIMABLE; only a STUDY with verified isolation, declared
576
+ * randomization/balancing, preregistered repetitions and a preregistered
577
+ * direction may reach a stronger status — and a contradicted direction stays
578
+ * CONTRADICTED regardless of how good the rest looks.
579
+ */
580
+ export function causalAttributionStatus({ isolationVerified, randomizationDeclared, repetitionsMet, preregisteredDirection, observedDirection }) {
581
+ if (isolationVerified !== true) return { status: 'NOT_ESTIMABLE', reason: 'A/B isolation not verified — no causal conditions exist' };
582
+ if (randomizationDeclared !== true || repetitionsMet !== true) {
583
+ return { status: 'DESCRIPTIVE_ONLY', reason: 'behavioral difference observed but randomization/repetition preregistration not satisfied' };
584
+ }
585
+ if (preregisteredDirection == null || observedDirection == null) {
586
+ return { status: 'INDETERMINATE', reason: 'preregistered or observed direction missing/unstable' };
587
+ }
588
+ return observedDirection === preregisteredDirection
589
+ ? { status: 'SUPPORTED_WITHIN_SCOPE', reason: 'paired, isolated, randomized, repeated evidence consistent with the preregistered direction' }
590
+ : { status: 'CONTRADICTED', reason: 'observed direction does not support the preregistered direction' };
591
+ }