forgedock 1.0.17 → 1.0.18
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/README.md +34 -1
- package/bin/cross-publish.mjs +344 -0
- package/bin/forgedock.mjs +229 -17
- package/commands/autopilot.md +77 -0
- package/commands/design-bench.md +176 -0
- package/commands/design-render-critique-loop.md +103 -0
- package/commands/design.md +259 -0
- package/commands/issue.md +7 -3
- package/commands/orchestrate.md +121 -0
- package/commands/qa-sweep.md +37 -2
- package/commands/quality-gate.md +30 -0
- package/commands/review-pr-staging.md +158 -2
- package/commands/test-gate.md +929 -0
- package/commands/work-on/build/implement.md +5 -3
- package/commands/work-on.md +53 -3
- package/docs/CI.md +177 -0
- package/docs/FORGE-PROTOCOL.md +324 -0
- package/docs/articles/b-vs-c-baseline.md +210 -0
- package/docs/articles/full-abc-benchmark.md +205 -0
- package/docs/articles/selective-spec-loading-token-savings.md +140 -0
- package/docs/design/abc-benchmark.md +195 -0
- package/docs/design/design-architect-rationale.md +123 -0
- package/docs/design/design-memory.md +61 -0
- package/docs/design/design-spec-schema.md +248 -0
- package/docs/design/design-system-lint.md +150 -0
- package/docs/design/divergent-generation.md +106 -0
- package/docs/design/effects-appropriateness.md +133 -0
- package/docs/design/fixtures/briefs/README.md +47 -0
- package/docs/design/fixtures/briefs/cadence.md +39 -0
- package/docs/design/fixtures/briefs/plume.md +39 -0
- package/docs/design/fixtures/briefs/slipstream.md +39 -0
- package/docs/design/fixtures/briefs/tender.md +39 -0
- package/docs/design/fixtures/briefs/voltage.md +39 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-1.html +364 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-2.html +420 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-3.html +506 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-1.html +611 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-2.html +594 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-3.html +817 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/runs.json +175 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/scorecard.json +138 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-1.html +373 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-2.html +398 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-3.html +595 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-1.html +531 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-2.html +565 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-3.html +674 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-1.html +388 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-2.html +463 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-3.html +568 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-1.html +562 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-2.html +618 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-3.html +618 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-1.html +364 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-2.html +420 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-3.html +506 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-1.html +527 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-2.html +445 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-3.html +483 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-1.html +611 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-2.html +594 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-3.html +817 -0
- package/docs/design/fixtures/runs/full-abc/runs.json +190 -0
- package/docs/design/fixtures/runs/full-abc/scorecard.json +144 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-1.html +496 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-2.html +447 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-3.html +564 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-1.html +373 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-2.html +398 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-3.html +595 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-1.html +622 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-2.html +475 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-3.html +660 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-1.html +531 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-2.html +565 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-3.html +674 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-1.html +525 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-2.html +446 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-3.html +463 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-1.html +388 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-2.html +463 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-3.html +568 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-1.html +1068 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-2.html +1423 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-3.html +1245 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-1.html +305 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-2.html +327 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-3.html +428 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-1.html +1270 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-2.html +1172 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-3.html +1532 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-1.html +315 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-2.html +342 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-3.html +353 -0
- package/docs/design/fixtures/runs/full-abc-opus/runs.json +190 -0
- package/docs/design/fixtures/runs/full-abc-opus/scorecard.json +144 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-1.html +345 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-2.html +396 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-3.html +502 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-1.html +408 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-2.html +446 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-3.html +496 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-1.html +813 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-2.html +711 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-3.html +721 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-1.html +280 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-2.html +291 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-3.html +366 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-1.html +1010 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-2.html +836 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-3.html +930 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-1.html +407 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-2.html +396 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-3.html +529 -0
- package/docs/design/foundations/brutalist.css +294 -0
- package/docs/design/foundations/editorial.css +300 -0
- package/docs/design/foundations/minimal-luxury.css +295 -0
- package/docs/design/foundations/technical.css +293 -0
- package/docs/design/foundations/warm-photo.css +299 -0
- package/docs/design/reference-corpus.md +1726 -0
- package/docs/design/render-critique-loop.md +57 -0
- package/docs/spec-graph-schema.md +397 -0
- package/package.json +1 -1
- package/scripts/bench-scorecard.mjs +239 -0
- package/scripts/build-spec-graph.mjs +509 -0
- package/scripts/classify-lane.sh +0 -7
- package/scripts/design-system-lint.mjs +993 -0
- package/scripts/graph-query.sh +486 -0
- package/scripts/transition-label.sh +10 -2
- package/scripts/validate-spec-graph.sh +423 -0
- package/templates/workflows/forgedock-review.yml +96 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/bench-scorecard.mjs — ABC benchmark scorecard aggregator (#878)
|
|
4
|
+
*
|
|
5
|
+
* Deterministic, zero-dependency aggregator for the UI Taste Harness ABC benchmark.
|
|
6
|
+
* Takes a raw "runs" file (per-run blind-pairwise outcomes + 1-5 rubric scores + slop
|
|
7
|
+
* counts for arms A/B/C across 3-5 products) and emits a scorecard: win-rates vs the
|
|
8
|
+
* gold-standard arm C, the A-vs-B harness delta, rubric distributions (mean + stdev),
|
|
9
|
+
* mean slop counts, and a judge-calibration check (C must beat A and B).
|
|
10
|
+
*
|
|
11
|
+
* It does NOT call any model, network, or render anything — it only aggregates the
|
|
12
|
+
* judge output that the /design-bench command produces. See docs/design/abc-benchmark.md.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node scripts/bench-scorecard.mjs <runs.json> # read file, print scorecard JSON
|
|
16
|
+
* node scripts/bench-scorecard.mjs - # read runs JSON from stdin
|
|
17
|
+
* cat runs.json | node scripts/bench-scorecard.mjs # (no arg) read from stdin
|
|
18
|
+
*
|
|
19
|
+
* Methodology rule (enforced): n=1 is a lie. Any product with fewer than 3 runs is a
|
|
20
|
+
* HARD ERROR — the script exits non-zero rather than emit a misleading point estimate.
|
|
21
|
+
*
|
|
22
|
+
* Exit codes:
|
|
23
|
+
* 0 = scorecard emitted successfully
|
|
24
|
+
* 1 = invalid input (bad JSON, n<3, missing fields, unknown winner token)
|
|
25
|
+
* 2 = aggregation produced a miscalibrated-judge warning (scorecard still emitted to stdout)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const ARMS = ["A", "B", "C"];
|
|
29
|
+
const RUBRIC_DIMS = ["hierarchy", "typography", "color", "whitespace", "originality", "mobile"];
|
|
30
|
+
const PAIRWISE_KEYS = ["A_vs_C", "B_vs_C", "A_vs_B"];
|
|
31
|
+
const VALID_WINNERS = new Set(["A", "B", "C", "tie"]);
|
|
32
|
+
const MIN_RUNS = 3;
|
|
33
|
+
|
|
34
|
+
/** Win-rate of `arm` against `opponent` across an array of pairwise outcome objects.
|
|
35
|
+
* Ties count as half a win. Comparisons not involving `arm`-vs-`opponent` are ignored. */
|
|
36
|
+
export function winRate(runs, arm, opponent) {
|
|
37
|
+
const key = `${arm}_vs_${opponent}`;
|
|
38
|
+
const flipped = `${opponent}_vs_${arm}`;
|
|
39
|
+
let wins = 0;
|
|
40
|
+
let total = 0;
|
|
41
|
+
for (const r of runs) {
|
|
42
|
+
const pw = r.pairwise || {};
|
|
43
|
+
let outcome;
|
|
44
|
+
let flip = false;
|
|
45
|
+
if (Object.prototype.hasOwnProperty.call(pw, key)) {
|
|
46
|
+
outcome = pw[key];
|
|
47
|
+
} else if (Object.prototype.hasOwnProperty.call(pw, flipped)) {
|
|
48
|
+
outcome = pw[flipped];
|
|
49
|
+
flip = true;
|
|
50
|
+
} else {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!VALID_WINNERS.has(outcome)) {
|
|
54
|
+
throw new Error(`invalid pairwise winner "${outcome}" for ${flip ? flipped : key}`);
|
|
55
|
+
}
|
|
56
|
+
total += 1;
|
|
57
|
+
if (outcome === "tie") wins += 0.5;
|
|
58
|
+
else if (outcome === arm) wins += 1;
|
|
59
|
+
}
|
|
60
|
+
return total === 0 ? null : wins / total;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Mean and sample standard deviation of a numeric array. */
|
|
64
|
+
export function meanStdev(values) {
|
|
65
|
+
const n = values.length;
|
|
66
|
+
if (n === 0) return { mean: null, stdev: null };
|
|
67
|
+
const mean = values.reduce((a, b) => a + b, 0) / n;
|
|
68
|
+
if (n === 1) return { mean: round(mean), stdev: 0 };
|
|
69
|
+
const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / (n - 1);
|
|
70
|
+
return { mean: round(mean), stdev: round(Math.sqrt(variance)) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Per-arm, per-dimension rubric distribution across all runs of all products. */
|
|
74
|
+
export function rubricStats(products) {
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const arm of ARMS) {
|
|
77
|
+
out[arm] = {};
|
|
78
|
+
for (const dim of RUBRIC_DIMS) {
|
|
79
|
+
const vals = [];
|
|
80
|
+
for (const p of products) {
|
|
81
|
+
for (const r of p.runs) {
|
|
82
|
+
const score = r.rubric?.[arm]?.[dim];
|
|
83
|
+
if (typeof score === "number") vals.push(score);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
out[arm][dim] = meanStdev(vals);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function round(x) {
|
|
93
|
+
return Math.round(x * 1000) / 1000;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Validate the runs file shape and the n>=3 rule. Throws on any violation. */
|
|
97
|
+
export function validate(data) {
|
|
98
|
+
if (!data || typeof data !== "object") throw new Error("runs file must be a JSON object");
|
|
99
|
+
if (!Array.isArray(data.products) || data.products.length === 0) {
|
|
100
|
+
throw new Error("runs file must contain a non-empty `products` array");
|
|
101
|
+
}
|
|
102
|
+
for (const p of data.products) {
|
|
103
|
+
if (!p.product) throw new Error("each product must have a `product` name");
|
|
104
|
+
if (!Array.isArray(p.runs)) throw new Error(`product "${p.product}" must have a runs array`);
|
|
105
|
+
if (p.runs.length < MIN_RUNS) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`product "${p.product}" has n=${p.runs.length} runs; n=1 is a lie — minimum is ${MIN_RUNS}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Aggregate validated runs data into a scorecard object. */
|
|
114
|
+
export function aggregate(data) {
|
|
115
|
+
validate(data);
|
|
116
|
+
const products = data.products;
|
|
117
|
+
|
|
118
|
+
// Primary metric: win-rate vs C, per arm, overall and per-product.
|
|
119
|
+
const winVsC = {};
|
|
120
|
+
for (const arm of ["A", "B"]) {
|
|
121
|
+
const perProduct = {};
|
|
122
|
+
const allRuns = [];
|
|
123
|
+
for (const p of products) {
|
|
124
|
+
const wr = winRate(p.runs, arm, "C");
|
|
125
|
+
perProduct[p.product] = wr;
|
|
126
|
+
allRuns.push(...p.runs);
|
|
127
|
+
}
|
|
128
|
+
winVsC[arm] = { mean: winRate(allRuns, arm, "C"), by_product: perProduct };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Harness delta: A vs B across all runs.
|
|
132
|
+
const allRuns = products.flatMap((p) => p.runs);
|
|
133
|
+
const aVsB = winRate(allRuns, "A", "B");
|
|
134
|
+
|
|
135
|
+
// Slop: mean negatives present per arm (lower is better).
|
|
136
|
+
const slop = {};
|
|
137
|
+
for (const arm of ARMS) {
|
|
138
|
+
const vals = [];
|
|
139
|
+
for (const p of products) {
|
|
140
|
+
for (const r of p.runs) {
|
|
141
|
+
const s = r.slop?.[arm];
|
|
142
|
+
if (typeof s === "number") vals.push(s);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
slop[arm] = vals.length ? round(vals.reduce((a, b) => a + b, 0) / vals.length) : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Judge calibration: C must win A_vs_C and B_vs_C. Any run where C loses is miscalibrated.
|
|
149
|
+
const miscalibrated = [];
|
|
150
|
+
for (const p of products) {
|
|
151
|
+
p.runs.forEach((r, i) => {
|
|
152
|
+
const pw = r.pairwise || {};
|
|
153
|
+
for (const key of ["A_vs_C", "B_vs_C"]) {
|
|
154
|
+
if (Object.prototype.hasOwnProperty.call(pw, key) && pw[key] !== "C" && pw[key] !== "tie") {
|
|
155
|
+
miscalibrated.push({ product: p.product, run: r.run ?? i + 1, comparison: key, winner: pw[key] });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const nRunsCounts = products.map((p) => p.runs.length);
|
|
162
|
+
const minRuns = Math.min(...nRunsCounts);
|
|
163
|
+
const maxRuns = Math.max(...nRunsCounts);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
corpus_version: data.corpus_version ?? null,
|
|
167
|
+
generation_model: data.generation_model ?? null,
|
|
168
|
+
judge_model: data.judge_model ?? null,
|
|
169
|
+
n_products: products.length,
|
|
170
|
+
n_runs_per_product: minRuns === maxRuns ? minRuns : { min: minRuns, max: maxRuns },
|
|
171
|
+
win_rate_vs_C: {
|
|
172
|
+
A: { mean: round0(winVsC.A.mean), by_product: roundMap(winVsC.A.by_product) },
|
|
173
|
+
B: { mean: round0(winVsC.B.mean), by_product: roundMap(winVsC.B.by_product) },
|
|
174
|
+
},
|
|
175
|
+
a_vs_b_win_rate: round0(aVsB),
|
|
176
|
+
rubric: rubricStats(products),
|
|
177
|
+
slop,
|
|
178
|
+
judge_calibration: {
|
|
179
|
+
miscalibrated_runs: miscalibrated,
|
|
180
|
+
ok: miscalibrated.length === 0,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function round0(x) {
|
|
186
|
+
return x === null ? null : round(x);
|
|
187
|
+
}
|
|
188
|
+
function roundMap(obj) {
|
|
189
|
+
const out = {};
|
|
190
|
+
for (const [k, v] of Object.entries(obj)) out[k] = round0(v);
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function readInput(arg) {
|
|
195
|
+
const { readFileSync } = await import("node:fs");
|
|
196
|
+
if (!arg || arg === "-") {
|
|
197
|
+
return readFileSync(0, "utf8"); // fd 0 = stdin
|
|
198
|
+
}
|
|
199
|
+
return readFileSync(arg, "utf8");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function main() {
|
|
203
|
+
const arg = process.argv[2];
|
|
204
|
+
let raw;
|
|
205
|
+
try {
|
|
206
|
+
raw = await readInput(arg);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
process.stderr.write(`ERROR: cannot read input: ${e.message}\n`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
let data;
|
|
212
|
+
try {
|
|
213
|
+
data = JSON.parse(raw);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
process.stderr.write(`ERROR: invalid JSON: ${e.message}\n`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
let scorecard;
|
|
219
|
+
try {
|
|
220
|
+
scorecard = aggregate(data);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
process.stderr.write(`ERROR: ${e.message}\n`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
process.stdout.write(JSON.stringify(scorecard, null, 2) + "\n");
|
|
226
|
+
if (!scorecard.judge_calibration.ok) {
|
|
227
|
+
process.stderr.write(
|
|
228
|
+
`WARNING: judge miscalibrated — C lost in ${scorecard.judge_calibration.miscalibrated_runs.length} comparison(s). ` +
|
|
229
|
+
`A real reference page should not lose to a generated arm; treat affected runs as suspect.\n`,
|
|
230
|
+
);
|
|
231
|
+
process.exit(2);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Run as CLI only when invoked directly, not when imported by tests.
|
|
236
|
+
import { fileURLToPath } from "node:url";
|
|
237
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
238
|
+
main();
|
|
239
|
+
}
|