@tachikomagundam/abathur 0.2.5 → 0.2.7
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 +3 -3
- package/dist/cli.js +4 -0
- package/dist/commands/genome.js +2 -2
- package/dist/commands/graft.js +1 -1
- package/dist/commands/readjudicate.js +28 -0
- package/dist/commands/retract.js +53 -0
- package/dist/commands/run.js +1 -1
- package/dist/commands/self-eval.js +1 -1
- package/dist/commands/tombstone.js +30 -16
- package/dist/core/evolve/run-loop.js +14 -0
- package/dist/core/evolve/run-rows.js +14 -0
- package/dist/core/evolve/score-bank.js +214 -0
- package/dist/core/graft-rebench.js +3 -0
- package/dist/core/ledger.js +8 -1
- package/dist/core/promote.js +63 -47
- package/dist/core/readjudicate.js +247 -0
- package/dist/core/retract.js +166 -0
- package/dist/core/stats-math.js +39 -0
- package/dist/core/stats.js +93 -19
- package/dist/core/worktree.js +1 -1
- package/dist/genomes/toy-smoke/init.mjs +1 -1
- package/dist/test/bundle.test.js +1 -1
- package/dist/test/fixture-loop.test.js +1 -1
- package/dist/test/fixtures-self.js +1 -1
- package/dist/test/fixtures-wt.js +1 -1
- package/dist/test/friction.test.js +3 -3
- package/dist/test/graft.test.js +1 -1
- package/dist/test/historian-grader-integrity.test.js +509 -2
- package/dist/test/promote.test.js +222 -5
- package/dist/test/readjudicate.test.js +212 -0
- package/dist/test/reflect.test.js +1 -1
- package/dist/test/repopath-seams.test.js +55 -0
- package/dist/test/score-bank.test.js +235 -0
- package/dist/test/seam-gates.test.js +84 -0
- package/dist/test/self-snapshot.test.js +2 -2
- package/dist/test/stats-acceptance.test.js +329 -0
- package/graders/historian/grader-core.d.mts +27 -0
- package/graders/historian/grader-core.mjs +311 -19
- package/graders/historian/grader.mjs +5 -5
- package/graders/historian/judge-poststage-19.mjs +212 -0
- package/graders/historian/run-scenario-19.sh +43 -0
- package/graders/historian/run-scenario.sh +16 -0
- package/package.json +1 -1
- package/plugin/abathur-command.md +3 -3
- package/plugin/abathur.ts +6 -6
package/dist/core/stats.js
CHANGED
|
@@ -26,15 +26,39 @@
|
|
|
26
26
|
// - Pareto over (trainScore, tokens, wallS); budget-truncated generations
|
|
27
27
|
// (complete:false) never enter the set.
|
|
28
28
|
//
|
|
29
|
+
// ACCEPTANCE SEMANTICS (2026-09-24 redesign; design doc lives in the target
|
|
30
|
+
// genome repo's evidence folder, not shipped here): when EvaluateInput.bank
|
|
31
|
+
// carries the historical per-unit variance bank, precision/regression checks
|
|
32
|
+
// price NOISE
|
|
33
|
+
// with bank σ instead of per-arm t(df=n−1) CIs (the campaign-9/10 resolution
|
|
34
|
+
// floor: t(0.05,1)=12.706 turned a 2-point sample into a 1.5883 half-width).
|
|
35
|
+
// A val unit with bank σ clears when P(Δ ≤ −halfWidth) < q_pair AND the
|
|
36
|
+
// acceptance band Z(q)·σ/√n_candidate ≤ halfWidth; the aggregate additionally
|
|
37
|
+
// requires P(gain shift > 0) = Φ(gain/SE) ≥ q_pair, q_pair = 1−(1−ACCEPT_Q)/nPairs.
|
|
38
|
+
// EVERY fail-closed rule ABOVE the bank path is untouched and can never be
|
|
39
|
+
// bypassed by it: budget⇒inconclusive, one-sided n<2⇒indeterminate,
|
|
40
|
+
// both-unmeasured⇒symmetric exclusion. A unit the bank cannot price falls back
|
|
41
|
+
// to the exact legacy t-CI line; a null/absent bank reproduces the pre-2026-09-24
|
|
42
|
+
// gate verbatim. minEffect 0.1 / halfWidth 0.15 point semantics stand.
|
|
43
|
+
//
|
|
29
44
|
// The inverse Student-t lives in stats-math.ts and the Pareto ranking in
|
|
30
45
|
// stats-pareto.ts; both are re-exported here so the public surface of this
|
|
31
46
|
// module — the one todos 9/10/12 consume — is unchanged. Accuracy: |t − table|
|
|
32
47
|
// < 1e-3 for the t-distribution rows checked in stats.test.ts (df=1..30, α=0.05).
|
|
33
48
|
import { EXIT_BLOCKED, EXIT_CANNOT_ANSWER, EXIT_OK } from "../exit.js";
|
|
34
49
|
/** Per-unit replicate scores (train and val units alike). */
|
|
35
|
-
import { studentTQuantile } from "./stats-math.js";
|
|
50
|
+
import { studentTQuantile, normalCdf, normalQuantile } from "./stats-math.js";
|
|
36
51
|
import { seededRandom, paretoFrontier } from "./stats-pareto.js";
|
|
37
|
-
export { studentTQuantile, seededRandom, paretoFrontier };
|
|
52
|
+
export { studentTQuantile, normalCdf, normalQuantile, seededRandom, paretoFrontier };
|
|
53
|
+
/** Nomination acceptance probability (per finalist pair after Bonferroni). */
|
|
54
|
+
export const ACCEPT_Q = 0.9;
|
|
55
|
+
/** q_pair = 1 − (1 − ACCEPT_Q)/nPairs — Bonferroni on the acceptance ERROR rate. */
|
|
56
|
+
export function bonferroniQ(nPairs) {
|
|
57
|
+
if (!Number.isInteger(nPairs) || nPairs < 1) {
|
|
58
|
+
throw new RangeError(`bonferroniQ: nPairs=${String(nPairs)} must be an integer >= 1`);
|
|
59
|
+
}
|
|
60
|
+
return Math.min(0.9999, 1 - (1 - ACCEPT_Q) / nPairs);
|
|
61
|
+
}
|
|
38
62
|
/** Family-wise alpha across finalist pairs (bonferroniAlpha divides it). */
|
|
39
63
|
export const FAMILY_ALPHA = 0.05;
|
|
40
64
|
/** Verdict → process exit code. inconclusive = 2 (cannot answer), never 0. */
|
|
@@ -165,7 +189,14 @@ export function evaluate(input) {
|
|
|
165
189
|
const candKept = candidate.units.filter((u) => !excluded.has(u.unitId));
|
|
166
190
|
const incKept = incumbent.units.filter((u) => !excluded.has(u.unitId));
|
|
167
191
|
const candByUnitKept = new Map(candKept.map((u) => [u.unitId, u]));
|
|
168
|
-
// 3) Per-val-unit comparisons + the two precision gates.
|
|
192
|
+
// 3) Per-val-unit comparisons + the two precision gates. With a bank that
|
|
193
|
+
// knows the unit, acceptance semantics (see module header) replace the
|
|
194
|
+
// per-arm t-CI machinery FOR THAT UNIT ONLY; bank-unknown units keep the
|
|
195
|
+
// legacy lines verbatim. The fail-closed rules above run before and
|
|
196
|
+
// independently of any bank.
|
|
197
|
+
const bank = input.bank ?? null;
|
|
198
|
+
const qPair = bonferroniQ(nPairs);
|
|
199
|
+
const acceptanceUnits = [];
|
|
169
200
|
const unitComparisons = [];
|
|
170
201
|
for (const [unitId, incUnit] of incByUnit) {
|
|
171
202
|
if (incUnit.split !== "val" || excluded.has(unitId))
|
|
@@ -173,30 +204,73 @@ export function evaluate(input) {
|
|
|
173
204
|
const candUnit = candByUnitKept.get(unitId);
|
|
174
205
|
if (candUnit === undefined)
|
|
175
206
|
continue; // excluded/unreachable
|
|
176
|
-
const cs = summarizeUnit(candUnit.scores, alpha);
|
|
177
207
|
const incumbentMean = summarizeUnit(incUnit.scores).mean;
|
|
208
|
+
const cs = summarizeUnit(candUnit.scores, alpha);
|
|
178
209
|
const delta = cs.mean - incumbentMean;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
210
|
+
const banked = bank?.get(unitId) ?? null;
|
|
211
|
+
if (banked === null) {
|
|
212
|
+
unitComparisons.push({
|
|
213
|
+
unitId,
|
|
214
|
+
candidateMean: cs.mean,
|
|
215
|
+
incumbentMean,
|
|
216
|
+
ciHalfWidth: cs.ciHalfWidth,
|
|
217
|
+
delta,
|
|
218
|
+
ciPasses: cs.ciHalfWidth <= stats.halfWidth,
|
|
219
|
+
passes: delta >= -cs.ciHalfWidth, // ties allowed
|
|
220
|
+
});
|
|
221
|
+
if (cs.ciHalfWidth > stats.halfWidth) {
|
|
222
|
+
failures.push(`CI half-width ${fmt(cs.ciHalfWidth)} > halfWidth ${fmt(stats.halfWidth)} on unit '${unitId}'`);
|
|
223
|
+
}
|
|
224
|
+
if (delta < -cs.ciHalfWidth) {
|
|
225
|
+
failures.push(`regression on unit '${unitId}': delta ${fmt(delta)} < ${fmt(-cs.ciHalfWidth)} (mean ${fmt(cs.mean)} < ${fmt(incumbentMean)} - ${fmt(cs.ciHalfWidth)})`);
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const se = banked.sigma * Math.sqrt(1 / candUnit.scores.length + 1 / incUnit.scores.length);
|
|
230
|
+
const band = (normalQuantile(qPair) * banked.sigma) / Math.sqrt(candUnit.scores.length);
|
|
231
|
+
const pRegression = se === 0 ? (delta <= -stats.halfWidth ? 1 : 0) : normalCdf((-stats.halfWidth - delta) / se);
|
|
232
|
+
const ciPasses = band <= stats.halfWidth;
|
|
233
|
+
const passes = pRegression < qPair; // ties at −halfWidth sit at Φ(0)=0.5 < q ⇒ still pass
|
|
234
|
+
unitComparisons.push({ unitId, candidateMean: cs.mean, incumbentMean, ciHalfWidth: band, delta, ciPasses, passes });
|
|
235
|
+
acceptanceUnits.push({ unitId, sigma: banked.sigma, df: banked.df, delta, se, band, pRegression });
|
|
236
|
+
if (!ciPasses) {
|
|
237
|
+
failures.push(`acceptance band ${fmt(band)} > halfWidth ${fmt(stats.halfWidth)} on unit '${unitId}' (bank sigma ${fmt(banked.sigma)}, df ${String(banked.df)})`);
|
|
190
238
|
}
|
|
191
|
-
if (
|
|
192
|
-
failures.push(`regression on unit '${unitId}': delta ${fmt(delta)}
|
|
239
|
+
else if (!passes) {
|
|
240
|
+
failures.push(`regression on unit '${unitId}': delta ${fmt(delta)} — P(Δ ≤ −${fmt(stats.halfWidth)}) = ${fmt(pRegression)} ≥ q ${fmt(qPair)} (bank sigma ${fmt(banked.sigma)})`);
|
|
193
241
|
}
|
|
194
242
|
}
|
|
195
|
-
// 4) Effect-size floor on the aggregate gain (shared, symmetric pool)
|
|
243
|
+
// 4) Effect-size floor on the aggregate gain (shared, symmetric pool) —
|
|
244
|
+
// unchanged — plus the acceptance gate: when the bank can price EVERY
|
|
245
|
+
// unit of the gain pool, nomination additionally requires P(Δ>0) ≥ q.
|
|
246
|
+
// A bank-priced point mass exactly at 0 (se=0, gain=0) fails (pShift=0):
|
|
247
|
+
// demonstrated-identical arms are never nominated on a tie.
|
|
196
248
|
const gain = aggregateScore(candKept) - aggregateScore(incKept);
|
|
197
249
|
if (gain < stats.minEffect) {
|
|
198
250
|
failures.push(`gain ${fmt(gain)} < minEffect ${fmt(stats.minEffect)}`);
|
|
199
251
|
}
|
|
252
|
+
let gainSe = null;
|
|
253
|
+
let pShift = null;
|
|
254
|
+
if (bank !== null) {
|
|
255
|
+
const train = candKept.filter((u) => u.split === "train");
|
|
256
|
+
const pool = train.length > 0 ? train : candKept;
|
|
257
|
+
const incCounts = new Map(incKept.map((u) => [u.unitId, u.scores.length]));
|
|
258
|
+
const priced = pool.map((u) => bank.get(u.unitId) ?? null);
|
|
259
|
+
if (priced.every((s) => s !== null)) {
|
|
260
|
+
const k = pool.length;
|
|
261
|
+
let variance = 0;
|
|
262
|
+
pool.forEach((u, index) => {
|
|
263
|
+
const s = priced[index];
|
|
264
|
+
variance += ((s.sigma * s.sigma) * (1 / u.scores.length + 1 / (incCounts.get(u.unitId) ?? 0))) / (k * k);
|
|
265
|
+
});
|
|
266
|
+
gainSe = Math.sqrt(variance);
|
|
267
|
+
pShift = gainSe === 0 ? (gain > 0 ? 1 : 0) : normalCdf(gain / gainSe);
|
|
268
|
+
if (pShift < qPair) {
|
|
269
|
+
failures.push(`acceptance: P(gain shift > 0) = ${fmt(pShift)} < q ${fmt(qPair)} (se ${fmt(gainSe)}, gain ${fmt(gain)})`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
200
273
|
const verdict = failures.length === 0 ? "nominated" : "culled";
|
|
201
|
-
|
|
274
|
+
const acceptance = bank === null ? undefined : { q: qPair, gainSe, pShift, units: acceptanceUnits };
|
|
275
|
+
return { ...base, verdict, exitCode: needsExit(verdict), gain, unitComparisons, failures, ...(acceptance === undefined ? {} : { acceptance }) };
|
|
202
276
|
}
|
package/dist/core/worktree.js
CHANGED
|
@@ -23,7 +23,7 @@ export { snapshotCommit } from "./snapshot.js";
|
|
|
23
23
|
export { INCUMBENT_BRANCH, fastForwardIncumbent } from "./incumbent.js";
|
|
24
24
|
const DAY_MS = 86_400_000;
|
|
25
25
|
/** Fixed committer identity for sealed generations (repo config is never trusted or touched). */
|
|
26
|
-
const SEAL_IDENTITY = ["-c", "user.name=abathur", "-c", "user.email=
|
|
26
|
+
const SEAL_IDENTITY = ["-c", "user.name=abathur", "-c", "user.email=agent@host.example"];
|
|
27
27
|
// ------------------------------------------------------------------ openGenome
|
|
28
28
|
/** Porcelain v1 with -z: "XY path" records; rename/copy records carry the source as an extra NUL field. */
|
|
29
29
|
function parsePorcelain(raw) {
|
|
@@ -47,7 +47,7 @@ if (!existsSync(path.join(dest, ".git"))) {
|
|
|
47
47
|
git(["add", "-A"]);
|
|
48
48
|
// identity pinned via -c BEFORE the subcommand (global gitconfig immune;
|
|
49
49
|
// `git init` itself rejects -c after the subcommand — learnings todo 3).
|
|
50
|
-
git(["-c", "user.name=abathur", "-c", "user.email=
|
|
50
|
+
git(["-c", "user.name=abathur", "-c", "user.email=agent@host.example", "commit", "-m", "toy-smoke fixture seed"]);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
process.stdout.write(`${dest}\n`);
|
package/dist/test/bundle.test.js
CHANGED
|
@@ -81,7 +81,7 @@ async function bundleFixture(t, opts = {}) {
|
|
|
81
81
|
writeFileSync(path.join(repo, "genome.jsonc"), `${JSON.stringify({ ...spec, bundle: { maskLiterals: [...opts.maskLiterals] } }, null, 2)}\n`, "utf8");
|
|
82
82
|
// commit the edit: export self-describes from the gen COMMIT tree, so the
|
|
83
83
|
// registry spec and the committed spec must agree by construction.
|
|
84
|
-
gitC(repo, "-c", "user.name=abathur", "-c", "user.email=
|
|
84
|
+
gitC(repo, "-c", "user.name=abathur", "-c", "user.email=agent@host.example", "commit", "-am", "bundle: declare maskLiterals");
|
|
85
85
|
}
|
|
86
86
|
registerGenome(configDir, path.join(repo, "genome.jsonc"));
|
|
87
87
|
const entry = requireGenomesByLabel(configDir, "toy-smoke").entries[0];
|
|
@@ -149,7 +149,7 @@ async function fixtureLoop(t) {
|
|
|
149
149
|
git(repo, ["init", "-b", "main"]);
|
|
150
150
|
git(repo, ["add", "-A"]);
|
|
151
151
|
// identity pinned via -c BEFORE the subcommand (toy init.mjs convention)
|
|
152
|
-
git(repo, ["-c", "user.name=abathur", "-c", "user.email=
|
|
152
|
+
git(repo, ["-c", "user.name=abathur", "-c", "user.email=agent@host.example", "commit", "-m", "fixloop seed"]);
|
|
153
153
|
registerGenome(configDir, specPath);
|
|
154
154
|
const entry = requireGenomesByLabel(configDir, "fixloop-germ").entries[0];
|
|
155
155
|
if (entry === undefined)
|
|
@@ -42,7 +42,7 @@ test("self-sanity: stub mutators deterministic", () => {
|
|
|
42
42
|
});
|
|
43
43
|
`;
|
|
44
44
|
function git(cwd, ...args) {
|
|
45
|
-
return execFileSync("git", ["-c", "user.name=abathur", "-c", "user.email=
|
|
45
|
+
return execFileSync("git", ["-c", "user.name=abathur", "-c", "user.email=agent@host.example", "-C", cwd, ...args], {
|
|
46
46
|
encoding: "utf8",
|
|
47
47
|
env: { ...process.env, LC_ALL: "C" },
|
|
48
48
|
}).trim();
|
package/dist/test/fixtures-wt.js
CHANGED
|
@@ -250,7 +250,7 @@ test("AC-2: canary planted in the val unit NEVER appears in the friction queue",
|
|
|
250
250
|
assert.deepEqual(val, { count: 1, aliases: ["val-1"], samples: 4 }, "scrubbed val material is present as counts/aliases");
|
|
251
251
|
});
|
|
252
252
|
function gitIn(repo, ...args) {
|
|
253
|
-
execFileSync("git", ["-c", "user.name=abathur", "-c", "user.email=
|
|
253
|
+
execFileSync("git", ["-c", "user.name=abathur", "-c", "user.email=agent@host.example", "-C", repo, ...args], { stdio: "pipe" });
|
|
254
254
|
}
|
|
255
255
|
test("friction: run CLI on drifted kernel records cause cli-error (spawned exit 1)", (t) => {
|
|
256
256
|
const root = mkdtempSync(path.join(os.tmpdir(), "abathur-drift-"));
|
|
@@ -264,8 +264,8 @@ test("friction: run CLI on drifted kernel records cause cli-error (spawned exit
|
|
|
264
264
|
touchSeal(repo, ["units/add.mjs", "units/mul.mjs", "units/explode.mjs", "units/sub.mjs", "grader.mjs"]);
|
|
265
265
|
writeFileSync(path.join(repo, "genome.jsonc"), toySpec(repo), "utf8");
|
|
266
266
|
execFileSync("git", ["init", "-b", "main", repo], { stdio: "pipe" });
|
|
267
|
-
execFileSync("git", ["-c", "user.name=abathur", "-c", "user.email=
|
|
268
|
-
execFileSync("git", ["-c", "user.name=abathur", "-c", "user.email=
|
|
267
|
+
execFileSync("git", ["-c", "user.name=abathur", "-c", "user.email=agent@host.example", "-C", repo, "add", "-A"], { stdio: "pipe" });
|
|
268
|
+
execFileSync("git", ["-c", "user.name=abathur", "-c", "user.email=agent@host.example", "-C", repo, "commit", "-m", "genome"], { stdio: "pipe" });
|
|
269
269
|
registerGenome(configDir, path.join(repo, "genome.jsonc"));
|
|
270
270
|
// drift the sealed grader AFTER registration:
|
|
271
271
|
writeFileSync(path.join(repo, "grader.mjs"), "// tampered\n", "utf8");
|
package/dist/test/graft.test.js
CHANGED
|
@@ -96,7 +96,7 @@ async function makeInstance(s, name, opts = {}) {
|
|
|
96
96
|
...(opts.requires === undefined ? {} : { requires: opts.requires }),
|
|
97
97
|
};
|
|
98
98
|
writeFileSync(path.join(repo, "genome.jsonc"), `${JSON.stringify(edited, null, 2)}\n`, "utf8");
|
|
99
|
-
gitC(repo, "-c", "user.name=abathur", "-c", "user.email=
|
|
99
|
+
gitC(repo, "-c", "user.name=abathur", "-c", "user.email=agent@host.example", "commit", "-am", "graft fixture: spec edit");
|
|
100
100
|
}
|
|
101
101
|
registerGenome(configDir, path.join(repo, "genome.jsonc"));
|
|
102
102
|
const entry = requireGenomesByLabel(configDir, "toy-smoke").entries[0];
|