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.
- package/_release/product-identity.json +2 -2
- package/console/dist/server/index.js +8 -0
- package/console/dist/server/routes/examiner-executor.d.ts +260 -0
- package/console/dist/server/routes/examiner-executor.js +628 -0
- package/console/dist/server/routes/examiner.d.ts +52 -0
- package/console/dist/server/routes/examiner.js +299 -0
- package/console/dist/ui/App.js +6 -3
- package/console/dist/ui/api.d.ts +138 -0
- package/console/dist/ui/api.js +56 -0
- package/console/dist/ui/components/layout/app-sidebar.d.ts +10 -0
- package/console/dist/ui/components/layout/app-sidebar.js +10 -2
- package/console/dist/ui/i18n/en.json +63 -0
- package/console/dist/ui/i18n/zh-CN.json +63 -0
- package/console/dist/ui/pages/examiner/ExaminerPage.d.ts +15 -0
- package/console/dist/ui/pages/examiner/ExaminerPage.js +158 -0
- package/console/dist/ui/pages/principles/PrincipleDetailPage.js +2 -1
- package/console/dist/ui/utils/runtime-mode.d.ts +11 -0
- package/console/dist/ui/utils/runtime-mode.js +11 -0
- package/console/dist/web/assets/app.css +44 -0
- package/console/dist/web/assets/app.js +33 -31
- package/console/lab/lib/challenge-actor.mjs +394 -0
- package/console/lab/lib/challenge-arms.mjs +102 -0
- package/console/lab/lib/challenge-behavior-report.mjs +2484 -0
- package/console/lab/lib/challenge-compile.mjs +211 -0
- package/console/lab/lib/challenge-freeze.mjs +215 -0
- package/console/lab/lib/challenge-isolation-probe.mjs +550 -0
- package/console/lab/lib/challenge-ledger.mjs +206 -0
- package/console/lab/lib/challenge-openclaw-host.mjs +630 -0
- package/console/lab/lib/challenge-runner.mjs +868 -0
- package/console/lab/lib/challenge-stage-r-gate.mjs +458 -0
- package/console/lab/lib/challenge-statistics.mjs +591 -0
- package/console/lab/lib/claim-evidence-env.mjs +652 -0
- package/console/lab/lib/claim-evidence-scorer.mjs +784 -0
- package/console/lab/lib/examiner-injection-evidence.mjs +367 -0
- package/console/lab/lib/examiner-study.mjs +1203 -0
- package/console/lab/lib/examiner-v07-cases.mjs +187 -0
- package/console/lab/lib/examiner-v2-cases.mjs +61 -0
- package/console/lab/lib/fixture.mjs +382 -0
- package/console/lab/lib/g2.mjs +146 -0
- package/console/lab/lib/host-actor.mjs +987 -0
- package/console/lab/lib/sidecar.mjs +273 -0
- package/package.json +1 -1
- package/pd-cli/package.json +1 -1
- 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
|
+
}
|