@tachikomagundam/abathur 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.github/workflows/ci.yml +29 -0
  2. package/.github/workflows/publish.yml +74 -0
  3. package/LICENSE +21 -0
  4. package/README.md +461 -0
  5. package/config/abathur.jsonc +17 -0
  6. package/config/genomes/historian.example.jsonc +124 -0
  7. package/dist/bench/adapter.js +201 -0
  8. package/dist/bench/fixture-probe.js +92 -0
  9. package/dist/bench/fixture-support.js +173 -0
  10. package/dist/bench/fixture.js +236 -0
  11. package/dist/bench/toy.js +152 -0
  12. package/dist/cli.js +110 -0
  13. package/dist/commands/bundle.js +79 -0
  14. package/dist/commands/genome.js +94 -0
  15. package/dist/commands/graft.js +71 -0
  16. package/dist/commands/kernel.js +47 -0
  17. package/dist/commands/promote.js +25 -0
  18. package/dist/commands/run.js +145 -0
  19. package/dist/commands/self-eval.js +240 -0
  20. package/dist/commands/status.js +186 -0
  21. package/dist/commands/tombstone.js +72 -0
  22. package/dist/config.js +161 -0
  23. package/dist/core/bundle-common.js +119 -0
  24. package/dist/core/bundle-export.js +212 -0
  25. package/dist/core/bundle-inspect.js +143 -0
  26. package/dist/core/bundle-manifest.js +105 -0
  27. package/dist/core/bundle-mask.js +75 -0
  28. package/dist/core/bundle-tar.js +240 -0
  29. package/dist/core/bundle.js +9 -0
  30. package/dist/core/evolve/brief.js +45 -0
  31. package/dist/core/evolve/candidate.js +140 -0
  32. package/dist/core/evolve/child-track.js +197 -0
  33. package/dist/core/evolve/friction.js +150 -0
  34. package/dist/core/evolve/reflect.js +191 -0
  35. package/dist/core/evolve/run-bench.js +170 -0
  36. package/dist/core/evolve/run-friction.js +63 -0
  37. package/dist/core/evolve/run-loop.js +282 -0
  38. package/dist/core/evolve/run-plan.js +39 -0
  39. package/dist/core/evolve/run-rows.js +145 -0
  40. package/dist/core/evolve/self-overlay.js +213 -0
  41. package/dist/core/evolve/self-snapshot.js +170 -0
  42. package/dist/core/evolve/stub-mutators.mjs +105 -0
  43. package/dist/core/evolve/udiff.js +189 -0
  44. package/dist/core/genome-paths.js +76 -0
  45. package/dist/core/genome.js +176 -0
  46. package/dist/core/glob.js +106 -0
  47. package/dist/core/graft-gates.js +184 -0
  48. package/dist/core/graft-rebench.js +187 -0
  49. package/dist/core/graft-support.js +181 -0
  50. package/dist/core/graft.js +218 -0
  51. package/dist/core/ids.js +154 -0
  52. package/dist/core/incumbent.js +46 -0
  53. package/dist/core/kernel.js +112 -0
  54. package/dist/core/ledger.js +198 -0
  55. package/dist/core/locks.js +172 -0
  56. package/dist/core/promote.js +119 -0
  57. package/dist/core/snapshot.js +61 -0
  58. package/dist/core/spec.js +178 -0
  59. package/dist/core/stats-math.js +102 -0
  60. package/dist/core/stats-pareto.js +57 -0
  61. package/dist/core/stats.js +184 -0
  62. package/dist/core/worktree.js +190 -0
  63. package/dist/exit.js +32 -0
  64. package/dist/genomes/toy-smoke/genome.jsonc +30 -0
  65. package/dist/genomes/toy-smoke/grader.mjs +61 -0
  66. package/dist/genomes/toy-smoke/init.mjs +63 -0
  67. package/dist/genomes/toy-smoke/units/add.mjs +17 -0
  68. package/dist/genomes/toy-smoke/units/explode.mjs +4 -0
  69. package/dist/genomes/toy-smoke/units/hang.mjs +16 -0
  70. package/dist/genomes/toy-smoke/units/mul.mjs +16 -0
  71. package/dist/genomes/toy-smoke/units/mutate.mjs +18 -0
  72. package/dist/genomes/toy-smoke/units/sub.mjs +16 -0
  73. package/dist/jsonc.js +77 -0
  74. package/dist/out.js +5 -0
  75. package/dist/test/bench-adapter.test.js +33 -0
  76. package/dist/test/bench-fixture.test.js +407 -0
  77. package/dist/test/bench-toy.test.js +251 -0
  78. package/dist/test/bundle.test.js +659 -0
  79. package/dist/test/config.test.js +185 -0
  80. package/dist/test/d7-gate.test.js +56 -0
  81. package/dist/test/fixture-loop.test.js +267 -0
  82. package/dist/test/fixtures/friction-writer.js +16 -0
  83. package/dist/test/fixtures-historian.js +82 -0
  84. package/dist/test/fixtures-self.js +143 -0
  85. package/dist/test/fixtures-wt.js +64 -0
  86. package/dist/test/friction.test.js +398 -0
  87. package/dist/test/genome.test.js +453 -0
  88. package/dist/test/git.test.js +69 -0
  89. package/dist/test/graft.test.js +567 -0
  90. package/dist/test/historian-genome.test.js +134 -0
  91. package/dist/test/historian-grader-io.test.js +148 -0
  92. package/dist/test/historian-grader.test.js +209 -0
  93. package/dist/test/ids.test.js +116 -0
  94. package/dist/test/include-val.test.js +120 -0
  95. package/dist/test/ledger-lock.test.js +99 -0
  96. package/dist/test/ledger.test.js +102 -0
  97. package/dist/test/promote.test.js +394 -0
  98. package/dist/test/reflect.test.js +410 -0
  99. package/dist/test/run-loop.test.js +433 -0
  100. package/dist/test/self-snapshot.test.js +328 -0
  101. package/dist/test/snapshot.test.js +86 -0
  102. package/dist/test/stats.test.js +423 -0
  103. package/dist/test/stub-mutators.test.js +17 -0
  104. package/dist/test/testutil.js +30 -0
  105. package/dist/test/worktree.test.js +198 -0
  106. package/dist/util/freeze.js +30 -0
  107. package/dist/util/git.js +85 -0
  108. package/docs/federation.md +184 -0
  109. package/docs/immutable-kernel.md +87 -0
  110. package/graders/historian/grader-core.d.mts +53 -0
  111. package/graders/historian/grader-core.mjs +276 -0
  112. package/graders/historian/grader-support.d.mts +57 -0
  113. package/graders/historian/grader-support.mjs +137 -0
  114. package/graders/historian/grader.mjs +113 -0
  115. package/graders/historian/mutate.sh +114 -0
  116. package/graders/historian/reset-sandbox.sh +60 -0
  117. package/graders/historian/run-scenario.sh +49 -0
  118. package/graders/historian/seed-wrapped.sh +32 -0
  119. package/package.json +42 -0
@@ -0,0 +1,251 @@
1
+ // Todo 5 acceptance pins (adapter-level only — the CLI `run` lives in todo 9):
2
+ // (a) seed→run→score cycle; (b) identical seed ⇒ identical score matrix;
3
+ // (c) state-mutating unit ⇒ reset restores the exact pre-state digest;
4
+ // (d) grader exit-1 ⇒ inconclusive-for-unit, never a crash;
5
+ // (e) hanging unit ⇒ process-group kill + recorded timeout status.
6
+ // Plan-literal: toy path needs no opencode, no network, no absolute machine paths.
7
+ import assert from "node:assert/strict";
8
+ import { spawnSync } from "node:child_process";
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
11
+ import * as os from "node:os";
12
+ import path from "node:path";
13
+ import { test } from "node:test";
14
+ import { ExitSignal } from "../exit.js";
15
+ import { treeDigestAt } from "../core/ids.js";
16
+ import { loadGenomeSpecFile } from "../core/spec.js";
17
+ import { applyPatch, scriptedPatches } from "../core/evolve/stub-mutators.mjs";
18
+ import {} from "../bench/adapter.js";
19
+ import { prepareToyGenome, ToyBenchAdapter } from "../bench/toy.js";
20
+ // ------------------------------------------------------------------- fixtures
21
+ function keep(t, dir) {
22
+ t.after(() => rm(dir, { recursive: true, force: true }));
23
+ return dir;
24
+ }
25
+ async function freshDir(t, prefix) {
26
+ return keep(t, await mkdtemp(path.join(os.tmpdir(), `abathur-${prefix}-`)));
27
+ }
28
+ /** Self-init toy genome: tmp copy + git repo + repoPath rewritten (init.mjs contract). */
29
+ async function toyFixture(t) {
30
+ const genomeDir = path.join(await freshDir(t, "toygerm"), "toy-smoke");
31
+ await prepareToyGenome(genomeDir);
32
+ return { genomeDir, spec: loadGenomeSpecFile(path.join(genomeDir, "genome.jsonc")) };
33
+ }
34
+ function unitById(spec, id) {
35
+ const unit = spec.bench.units.find((u) => u.id === id);
36
+ if (unit === undefined)
37
+ throw new Error(`fixture spec has no unit '${id}'`);
38
+ return unit;
39
+ }
40
+ function scored(outcome) {
41
+ if (outcome.kind === "scored")
42
+ return outcome.result;
43
+ assert.fail(`expected scored, got inconclusive: ${outcome.reason}`);
44
+ }
45
+ async function scoreMatrix(spec, sandboxDir) {
46
+ const adapter = new ToyBenchAdapter(spec);
47
+ await adapter.reset(sandboxDir);
48
+ await adapter.seed(sandboxDir);
49
+ const rows = [];
50
+ for (const unit of spec.bench.units) {
51
+ rows.push({
52
+ unitId: unit.id,
53
+ run: await adapter.run(unit, sandboxDir, spec.bench.timeoutS),
54
+ score: await adapter.score(unit),
55
+ });
56
+ }
57
+ return rows;
58
+ }
59
+ // ------------------------------------------------------------------ AC (a)
60
+ test("(a) seed→run→score cycle: ok statuses, provenance, metrics, val+train gradable", async (t) => {
61
+ const { genomeDir, spec } = await toyFixture(t);
62
+ const sandbox = await freshDir(t, "toysbx");
63
+ const adapter = new ToyBenchAdapter(spec);
64
+ await adapter.reset(sandbox);
65
+ await adapter.seed(sandbox);
66
+ const rows = new Map();
67
+ for (const unit of spec.bench.units) {
68
+ rows.set(unit.id, {
69
+ run: await adapter.run(unit, sandbox, spec.bench.timeoutS),
70
+ score: await adapter.score(unit),
71
+ });
72
+ }
73
+ for (const { run } of rows.values()) {
74
+ assert.equal(run.status, "ok");
75
+ assert.equal(run.benchProvenance.benchType, "toy");
76
+ assert.deepEqual(run.benchProvenance.versions, [{ bin: "node", version: process.version }]);
77
+ assert.equal(run.metrics.turns, 1);
78
+ assert.ok(run.metrics.tokensEst >= 1);
79
+ }
80
+ // a non-zero unit exit is an observation on an ok run, not an infra failure
81
+ assert.equal(rows.get("explode")?.run.exitCode, 1);
82
+ assert.equal(rows.get("add")?.run.exitCode, 0);
83
+ assert.equal(rows.get("add")?.run.note, undefined);
84
+ assert.equal(scored(rows.get("add").score).score, 0); // seeded bug is graded
85
+ assert.equal(scored(rows.get("add").score).pass, false);
86
+ assert.equal(scored(rows.get("mul").score).score, 1);
87
+ assert.equal(scored(rows.get("sub").score).pass, true); // val unit gradable
88
+ const mulBytes = readFileSync(path.join(sandbox, "units", "mul.mjs")).length;
89
+ assert.equal(rows.get("mul")?.run.metrics.tokensEst, Math.ceil(mulBytes / 4));
90
+ assert.ok(existsSync(path.join(genomeDir, ".git")), "init.mjs must self-create the git repo");
91
+ assert.equal(spec.repoPath, genomeDir); // repoPath rewritten to the tmp copy
92
+ });
93
+ // ------------------------------------------------------------------ AC (b)
94
+ test("(b) identical seed ⇒ identical score matrix (determinism)", async (t) => {
95
+ const { spec } = await toyFixture(t);
96
+ const matrixA = await scoreMatrix(spec, await freshDir(t, "toysbx"));
97
+ const matrixB = await scoreMatrix(spec, await freshDir(t, "toysbx"));
98
+ assert.deepEqual(matrixA, matrixB);
99
+ assert.equal(matrixA.length, spec.bench.units.length);
100
+ });
101
+ // ------------------------------------------------------------------ AC (c)
102
+ test("(c) reset+seed undoes unit state mutation — digest before run 2 == run 1", async (t) => {
103
+ const { spec } = await toyFixture(t);
104
+ const sandbox = await freshDir(t, "toysbx");
105
+ const adapter = new ToyBenchAdapter(spec);
106
+ await adapter.reset(sandbox);
107
+ await adapter.seed(sandbox);
108
+ const preDigest = treeDigestAt(sandbox);
109
+ const mutate = { id: "mutate", path: "units/mutate.mjs", split: "train" };
110
+ const run1 = await adapter.run(mutate, sandbox, spec.bench.timeoutS);
111
+ assert.equal(run1.status, "ok");
112
+ assert.ok(existsSync(path.join(sandbox, "poison.txt")), "mutating unit must dirty the sandbox");
113
+ assert.notEqual(treeDigestAt(sandbox), preDigest);
114
+ await adapter.reset(sandbox);
115
+ await adapter.seed(sandbox);
116
+ assert.ok(!existsSync(path.join(sandbox, "poison.txt")));
117
+ assert.equal(treeDigestAt(sandbox), preDigest); // exact pre-state restored
118
+ const run2 = await adapter.run(mutate, sandbox, spec.bench.timeoutS);
119
+ assert.deepEqual(run2, run1); // identical pre-state ⇒ identical observation
120
+ });
121
+ // ------------------------------------------------------------------ AC (d)
122
+ test("(d) grader exit-1 unit ⇒ inconclusive-for-unit, not a crash", async (t) => {
123
+ const { spec } = await toyFixture(t);
124
+ const sandbox = await freshDir(t, "toysbx");
125
+ const adapter = new ToyBenchAdapter(spec);
126
+ await adapter.reset(sandbox);
127
+ await adapter.seed(sandbox);
128
+ const outcome = await adapter.score(unitById(spec, "explode"));
129
+ assert.equal(outcome.kind, "inconclusive");
130
+ if (outcome.kind === "inconclusive") {
131
+ assert.equal(outcome.unitId, "explode");
132
+ assert.match(outcome.reason, /grader exited 1/);
133
+ }
134
+ // a scored sibling in the same sandbox proves the bench never died with the grader
135
+ assert.equal(scored(await adapter.score(unitById(spec, "mul"))).pass, true);
136
+ // stale-state guard: scoring with no sandbox ever seeded/run is a tool error (exit 2)
137
+ const cold = new ToyBenchAdapter(spec);
138
+ await assert.rejects(() => cold.score(unitById(spec, "mul")), (cause) => cause instanceof ExitSignal && cause.code === 2);
139
+ });
140
+ test("(d2) malformed grader output degrades to inconclusive, never a parse crash", async (t) => {
141
+ const { spec } = await toyFixture(t);
142
+ // tamper the SANDBOX copy of the grader (fixture repo itself stays sealed-clean)
143
+ const sandbox = await freshDir(t, "toysbx");
144
+ const adapter = new ToyBenchAdapter(spec);
145
+ await adapter.reset(sandbox);
146
+ await adapter.seed(sandbox);
147
+ await writeFile(path.join(sandbox, "grader.mjs"), 'process.stdout.write("not json at all\\n");\n', "utf8");
148
+ const outcome = await adapter.score(unitById(spec, "mul"));
149
+ assert.equal(outcome.kind, "inconclusive");
150
+ if (outcome.kind === "inconclusive")
151
+ assert.ok(outcome.reason.length > 0);
152
+ });
153
+ // ------------------------------------------------------------------ AC (e)
154
+ test("(e) hanging unit: process-group SIGKILL at timeoutS, recorded not thrown", async (t) => {
155
+ const { spec } = await toyFixture(t);
156
+ const sandbox = await freshDir(t, "toysbx");
157
+ const adapter = new ToyBenchAdapter(spec);
158
+ await adapter.reset(sandbox);
159
+ await adapter.seed(sandbox);
160
+ const hang = { id: "hang", path: "units/hang.mjs", split: "train" };
161
+ const startedMs = Date.now();
162
+ const run = await adapter.run(hang, sandbox, 1);
163
+ const elapsedMs = Date.now() - startedMs;
164
+ assert.equal(run.status, "timeout");
165
+ assert.equal(run.exitCode, null);
166
+ assert.deepEqual(run.metrics, { tokensEst: 0, turns: 0 });
167
+ assert.match(run.note ?? "", /killed after 1s: process group SIGKILL/);
168
+ assert.ok(elapsedMs < 6000, `timeout path took ${String(elapsedMs)}ms`);
169
+ // group kill must have reaped the exec'd grandchild, not just node itself
170
+ const ps = spawnSync("ps", ["-eo", "args"], { encoding: "utf8" });
171
+ assert.equal(ps.status, 0);
172
+ assert.ok(!ps.stdout.includes("sleep 31.7"), `orphaned 'sleep 31.7' survived the kill`);
173
+ });
174
+ // ----------------------------------------------------- infra / hooks / render
175
+ test("missing runner binary records infra_failed with a reason, excluded not zeroed", async (t) => {
176
+ const { spec } = await toyFixture(t);
177
+ const broken = {
178
+ ...spec,
179
+ bench: { ...spec.bench, runCommand: "abathur-no-such-bin {unit.path}" },
180
+ };
181
+ const sandbox = await freshDir(t, "toysbx");
182
+ const adapter = new ToyBenchAdapter(broken);
183
+ await adapter.reset(sandbox);
184
+ await adapter.seed(sandbox);
185
+ const run = await adapter.run(unitById(spec, "mul"), sandbox, 5);
186
+ assert.equal(run.status, "infra_failed");
187
+ assert.equal(run.exitCode, null);
188
+ assert.match(run.note ?? "", /spawn failed/);
189
+ });
190
+ test("seedCommand / resetCommand hooks execute inside the sandbox when configured", async (t) => {
191
+ const { spec } = await toyFixture(t);
192
+ const hooked = {
193
+ ...spec,
194
+ bench: { ...spec.bench, seedCommand: "touch seeded.flag", resetCommand: "touch reset.flag" },
195
+ };
196
+ const sandbox = path.join(await freshDir(t, "toysbx"), "nested"); // mkdir -p path
197
+ const adapter = new ToyBenchAdapter(hooked);
198
+ await adapter.reset(sandbox);
199
+ await adapter.seed(sandbox);
200
+ assert.ok(existsSync(path.join(sandbox, "reset.flag")), "resetCommand must run after the wipe");
201
+ assert.ok(existsSync(path.join(sandbox, "seeded.flag")));
202
+ });
203
+ // -------------------------------------------------------------- stub-mutators
204
+ test("stub-mutators: applyPatch refuses missing/ambiguous anchors; fix-add lifts the score", async (t) => {
205
+ const { genomeDir, spec } = await toyFixture(t);
206
+ const scratch = await freshDir(t, "mutscratch");
207
+ await writeFile(path.join(scratch, "dup.txt"), "x\nx\n", "utf8");
208
+ assert.equal(applyPatch(scratch, { file: "dup.txt", from: "x", to: "y" }), false); // ambiguous
209
+ assert.equal(applyPatch(scratch, { file: "nope.txt", from: "x", to: "y" }), false); // missing
210
+ assert.equal(readFileSync(path.join(scratch, "dup.txt"), "utf8"), "x\nx\n"); // nothing written
211
+ const fix = scriptedPatches().find((p) => p.id === "fix-add");
212
+ if (fix === undefined)
213
+ throw new Error("fix-add patch missing from table");
214
+ const sandbox = await freshDir(t, "toysbx");
215
+ const adapter = new ToyBenchAdapter(spec);
216
+ await adapter.reset(sandbox);
217
+ await adapter.seed(sandbox);
218
+ assert.equal(scored(await adapter.score(unitById(spec, "add"))).score, 0); // before: seeded bug
219
+ assert.equal(applyPatch(genomeDir, fix), true);
220
+ await adapter.reset(sandbox);
221
+ await adapter.seed(sandbox);
222
+ assert.equal(scored(await adapter.score(unitById(spec, "add"))).score, 1); // after: evolved
223
+ // re-apply of the same patch is a no-op refusal (anchor already rewritten)
224
+ assert.equal(applyPatch(genomeDir, fix), false);
225
+ });
226
+ // ------------------------------------------------------------------ self-init
227
+ test("init.mjs is idempotent: re-init keeps the git repo and rewrites repoPath", async (t) => {
228
+ const { genomeDir } = await toyFixture(t);
229
+ const head = () => {
230
+ const run = spawnSync("git", ["rev-parse", "HEAD"], { cwd: genomeDir, encoding: "utf8" });
231
+ assert.equal(run.status, 0);
232
+ return (run.stdout ?? "").trim();
233
+ };
234
+ const before = head();
235
+ await prepareToyGenome(genomeDir); // .git already exists ⇒ must NOT re-init
236
+ assert.equal(head(), before);
237
+ });
238
+ test("prepareToyGenome creates missing parent dirs (regression: missing cwd masquerades as spawn ENOENT)", async (t) => {
239
+ const dest = path.join(await freshDir(t, "toyparent"), "missing", "nested-g");
240
+ await prepareToyGenome(dest);
241
+ assert.ok(existsSync(path.join(dest, "genome.jsonc")));
242
+ assert.ok(existsSync(path.join(dest, ".git")));
243
+ const head = () => {
244
+ const run = spawnSync("git", ["rev-parse", "HEAD"], { cwd: dest, encoding: "utf8" });
245
+ assert.equal(run.status, 0);
246
+ return (run.stdout ?? "").trim();
247
+ };
248
+ const before = head();
249
+ await prepareToyGenome(dest); // same dest again ⇒ still idempotent, .git preserved
250
+ assert.equal(head(), before);
251
+ });