@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.
Files changed (45) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +4 -0
  3. package/dist/commands/genome.js +2 -2
  4. package/dist/commands/graft.js +1 -1
  5. package/dist/commands/readjudicate.js +28 -0
  6. package/dist/commands/retract.js +53 -0
  7. package/dist/commands/run.js +1 -1
  8. package/dist/commands/self-eval.js +1 -1
  9. package/dist/commands/tombstone.js +30 -16
  10. package/dist/core/evolve/run-loop.js +14 -0
  11. package/dist/core/evolve/run-rows.js +14 -0
  12. package/dist/core/evolve/score-bank.js +214 -0
  13. package/dist/core/graft-rebench.js +3 -0
  14. package/dist/core/ledger.js +8 -1
  15. package/dist/core/promote.js +63 -47
  16. package/dist/core/readjudicate.js +247 -0
  17. package/dist/core/retract.js +166 -0
  18. package/dist/core/stats-math.js +39 -0
  19. package/dist/core/stats.js +93 -19
  20. package/dist/core/worktree.js +1 -1
  21. package/dist/genomes/toy-smoke/init.mjs +1 -1
  22. package/dist/test/bundle.test.js +1 -1
  23. package/dist/test/fixture-loop.test.js +1 -1
  24. package/dist/test/fixtures-self.js +1 -1
  25. package/dist/test/fixtures-wt.js +1 -1
  26. package/dist/test/friction.test.js +3 -3
  27. package/dist/test/graft.test.js +1 -1
  28. package/dist/test/historian-grader-integrity.test.js +509 -2
  29. package/dist/test/promote.test.js +222 -5
  30. package/dist/test/readjudicate.test.js +212 -0
  31. package/dist/test/reflect.test.js +1 -1
  32. package/dist/test/repopath-seams.test.js +55 -0
  33. package/dist/test/score-bank.test.js +235 -0
  34. package/dist/test/seam-gates.test.js +84 -0
  35. package/dist/test/self-snapshot.test.js +2 -2
  36. package/dist/test/stats-acceptance.test.js +329 -0
  37. package/graders/historian/grader-core.d.mts +27 -0
  38. package/graders/historian/grader-core.mjs +311 -19
  39. package/graders/historian/grader.mjs +5 -5
  40. package/graders/historian/judge-poststage-19.mjs +212 -0
  41. package/graders/historian/run-scenario-19.sh +43 -0
  42. package/graders/historian/run-scenario.sh +16 -0
  43. package/package.json +1 -1
  44. package/plugin/abathur-command.md +3 -3
  45. package/plugin/abathur.ts +6 -6
@@ -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
- unitComparisons.push({
180
- unitId,
181
- candidateMean: cs.mean,
182
- incumbentMean,
183
- ciHalfWidth: cs.ciHalfWidth,
184
- delta,
185
- ciPasses: cs.ciHalfWidth <= stats.halfWidth,
186
- passes: delta >= -cs.ciHalfWidth, // ties allowed
187
- });
188
- if (cs.ciHalfWidth > stats.halfWidth) {
189
- failures.push(`CI half-width ${fmt(cs.ciHalfWidth)} > halfWidth ${fmt(stats.halfWidth)} on unit '${unitId}'`);
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 (delta < -cs.ciHalfWidth) {
192
- failures.push(`regression on unit '${unitId}': delta ${fmt(delta)} < ${fmt(-cs.ciHalfWidth)} (mean ${fmt(cs.mean)} < ${fmt(incumbentMean)} - ${fmt(cs.ciHalfWidth)})`);
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
- return { ...base, verdict, exitCode: needsExit(verdict), gain, unitComparisons, failures };
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
  }
@@ -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=abathur@harness.local"];
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=abathur@harness.local", "commit", "-m", "toy-smoke fixture seed"]);
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`);
@@ -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=abathur@harness.local", "commit", "-am", "bundle: declare maskLiterals");
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=abathur@harness.local", "commit", "-m", "fixloop seed"]);
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=abathur@harness.local", "-C", cwd, ...args], {
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();
@@ -13,7 +13,7 @@ export const GIT_ID = [
13
13
  "-c",
14
14
  "user.name=abathur-test",
15
15
  "-c",
16
- "user.email=test@abathur.local",
16
+ "user.email=ci@host.example",
17
17
  "-c",
18
18
  "commit.gpgsign=false",
19
19
  ];
@@ -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=abathur@harness.local", "-C", repo, ...args], { stdio: "pipe" });
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=abathur@harness.local", "-C", repo, "add", "-A"], { stdio: "pipe" });
268
- execFileSync("git", ["-c", "user.name=abathur", "-c", "user.email=abathur@harness.local", "-C", repo, "commit", "-m", "genome"], { stdio: "pipe" });
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");
@@ -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=abathur@harness.local", "commit", "-am", "graft fixture: spec edit");
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];