@tachikomagundam/abathur 0.2.3 → 0.2.5

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.
@@ -0,0 +1,153 @@
1
+ // Campaign-1 defect pins (historian evidence task-07, live symptom rc=2):
2
+ // a `${VAR}` env-literal repoPath is a PATH-INJECTION convenience, NOT an
3
+ // engine-self marker. run-loop.ts routed selfMode on isEnvRepoLiteral — ANY
4
+ // literal — so the historian genome (`${ABATHUR_HISTORIAN_REPO}`) was forced
5
+ // into the tsc-build/toy-replay snapshot-overlay bench and died fail-closed
6
+ // demanding `selfbench/expected.json`. selfMode must mean
7
+ // `envRepoName(repoPath) === "ABATHUR_SELF_REPO"`. Secondary pin: the dry-run
8
+ // plan must display the RESOLVED repo path, never the raw literal joined to
9
+ // cwd (run-plan.ts printed `/…/historian/${ABATHUR_HISTORIAN_REPO}`).
10
+ // Harness mirrors run-loop.test.ts: toy genome + stub mutator, no model calls.
11
+ import assert from "node:assert/strict";
12
+ import { spawnSync } from "node:child_process";
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
14
+ import { mkdtemp } from "node:fs/promises";
15
+ import * as os from "node:os";
16
+ import path from "node:path";
17
+ import test, {} from "node:test";
18
+ import { ExitSignal } from "../exit.js";
19
+ import { registerGenome, requireGenomesByLabel } from "../core/genome.js";
20
+ import { Ledger } from "../core/ledger.js";
21
+ import { prepareToyGenome } from "../bench/toy.js";
22
+ import { runEvolution } from "../core/evolve/run-loop.js";
23
+ // Same three-candidate stub script as run-loop.test.ts (annotate-add seals,
24
+ // touch-kernel path-rejected, fix-add nominated).
25
+ const ADD_ANCHOR = "return a - b; // seeded bug: must be `return a + b;`";
26
+ const STUB_SOURCE = `#!/usr/bin/env node
27
+ import { readFileSync } from "node:fs";
28
+ const args = process.argv.slice(2);
29
+ const opt = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; };
30
+ const mode = opt("--mode");
31
+ const dir = opt("--dir");
32
+ const read = (f) => readFileSync(dir + "/" + f, "utf8");
33
+ function lineDiff(file, anchor, replacement) {
34
+ const lines = read(file).split("\\n");
35
+ const i = lines.findIndex((l) => l.includes(anchor));
36
+ if (i < 0) { process.stderr.write("stub: no anchor in " + file + "\\n"); process.exit(1); }
37
+ const changed = lines[i].replace(anchor, replacement);
38
+ return "--- a/" + file + "\\n+++ b/" + file + "\\n@@ -" + String(i + 1) + ",1 +" + String(i + 1) + ",1 @@\\n-" + lines[i] + "\\n+" + changed + "\\n";
39
+ }
40
+ const FIX = ${JSON.stringify(ADD_ANCHOR)};
41
+ const out = (candidates) => process.stdout.write(JSON.stringify({ candidates }) + "\\n");
42
+ if (mode === "three") {
43
+ out([
44
+ { id: "annotate-add", rationale: "annotate add()", diffs: [lineDiff("units/add.mjs", "export function add(", "export function add /* patched */(")] },
45
+ { id: "touch-kernel", rationale: "subvert grading", diffs: [lineDiff("grader.mjs", "model-free", "model-free-ish")] },
46
+ { id: "fix-add", rationale: "fix add(): subtraction to addition", diffs: [lineDiff("units/add.mjs", FIX, "return a + b;")] },
47
+ ]);
48
+ } else {
49
+ process.stderr.write("stub: unknown mode\\n");
50
+ process.exit(2);
51
+ }
52
+ `;
53
+ /**
54
+ * Toy genome registered with repoPath stored as the UNRESOLVED literal
55
+ * `${<envName>}` (the self-seed trick, pointed at the toy repo itself), env
56
+ * var set for the duration of the test. Fresh configDir per test.
57
+ */
58
+ async function literalFixture(t, envName) {
59
+ const root = await mkdtemp(path.join(os.tmpdir(), "abathur-selfmode-"));
60
+ t.after(() => {
61
+ // trusted snapshots are chmod-protected read-only: restore writability first.
62
+ spawnSync("chmod", ["-R", "u+rwX", root], { encoding: "utf8" });
63
+ spawnSync("rm", ["-rf", root], { encoding: "utf8" });
64
+ });
65
+ const configDir = path.join(root, "config");
66
+ mkdirSync(configDir, { recursive: true });
67
+ writeFileSync(path.join(configDir, "config.jsonc"), '{ "opencodeBin": null }\n', "utf8");
68
+ const repo = await prepareToyGenome(path.join(root, "genome"));
69
+ const specPath = path.join(repo, "genome.jsonc");
70
+ const doc = JSON.parse(readFileSync(specPath, "utf8"));
71
+ doc.repoPath = `\${${envName}}`;
72
+ writeFileSync(specPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
73
+ const saved = process.env[envName];
74
+ process.env[envName] = repo;
75
+ t.after(() => {
76
+ if (saved === undefined)
77
+ delete process.env[envName];
78
+ else
79
+ process.env[envName] = saved;
80
+ });
81
+ registerGenome(configDir, specPath);
82
+ const entry = requireGenomesByLabel(configDir, "toy-smoke").entries[0];
83
+ if (entry === undefined)
84
+ throw new Error("fixture: toy-smoke registration vanished");
85
+ const stub = path.join(root, "stub.mjs");
86
+ writeFileSync(stub, STUB_SOURCE, "utf8");
87
+ chmodSync(stub, 0o755);
88
+ return {
89
+ root,
90
+ configDir,
91
+ repo,
92
+ entry,
93
+ mutatorCommand: `node ${stub} --mode three --dir {worktree} --brief {brief}`,
94
+ env: { XDG_CACHE_HOME: path.join(root, "xdg-cache"), HOME: path.join(root, "home") },
95
+ sandboxRoot: path.join(root, "sandboxes"),
96
+ };
97
+ }
98
+ function generationRowCount(f) {
99
+ return Ledger.open(f.repo)
100
+ .readAll()
101
+ .filter((r) => r.kind === "generation_complete").length;
102
+ }
103
+ // ------------------------------------------------- non-self literal: normal bench
104
+ test("campaign-1 defect: a non-self env-literal repoPath must NOT enter the self-bench overlay", async (t) => {
105
+ const f = await literalFixture(t, "ABATHUR_HISTORIAN_REPO");
106
+ const out = await runEvolution({
107
+ entry: f.entry,
108
+ configDir: f.configDir,
109
+ mutatorCommand: f.mutatorCommand,
110
+ env: f.env,
111
+ sandboxRoot: f.sandboxRoot,
112
+ });
113
+ const plan = out.lines.join("\n");
114
+ assert.equal(out.exitCode, 0, `expected exit 0, lines: ${plan}`);
115
+ assert.ok(!/selfbench|self-snapshot|self-bench|overlay/i.test(plan), `non-self literal must never touch the self-bench machinery: ${plan}`);
116
+ assert.ok(generationRowCount(f) >= 2, "incumbent + candidate rows benched through the NORMAL toy path");
117
+ assert.equal(existsSync(path.join(f.root, "selfbench")), false);
118
+ });
119
+ // ------------------------------------- self literal: overlay path preserved
120
+ test("regression pin: the ${ABATHUR_SELF_REPO} literal still routes into the self-bench path", async (t) => {
121
+ // The toy repo carries no selfbench/expected.json, so entering the overlay
122
+ // bench FAILS CLOSED with exactly that demand — the observable proof that
123
+ // selfMode is still true for the engine-self literal (live symptom shape).
124
+ const f = await literalFixture(t, "ABATHUR_SELF_REPO");
125
+ await assert.rejects(runEvolution({
126
+ entry: f.entry,
127
+ configDir: f.configDir,
128
+ mutatorCommand: f.mutatorCommand,
129
+ env: f.env,
130
+ sandboxRoot: f.sandboxRoot,
131
+ }), (cause) => {
132
+ assert.ok(cause instanceof ExitSignal, `expected ExitSignal, got ${String(cause)}`);
133
+ assert.equal(cause.code, 2);
134
+ assert.match(cause.message, /self-snapshot: trusted selfbench\/expected\.json missing/);
135
+ return true;
136
+ });
137
+ });
138
+ // ----------------------------------------------------- dry-run plan display
139
+ test("--dry-run plan displays the RESOLVED repo path, never the literal against cwd", async (t) => {
140
+ const f = await literalFixture(t, "ABATHUR_HISTORIAN_REPO");
141
+ const out = await runEvolution({
142
+ entry: f.entry,
143
+ configDir: f.configDir,
144
+ mutatorCommand: f.mutatorCommand,
145
+ dryRun: true,
146
+ env: f.env,
147
+ sandboxRoot: f.sandboxRoot,
148
+ });
149
+ assert.equal(out.exitCode, 0, out.lines.join("\n"));
150
+ const plan = out.lines.join("\n");
151
+ assert.ok(plan.includes(`repo: ${f.repo}`), `plan must show the resolved path:\n${plan}`);
152
+ assert.ok(!plan.includes("${"), `plan must never echo the raw literal (resolved against cwd garbage):\n${plan}`);
153
+ });
@@ -234,6 +234,24 @@ describe("evaluate: n=1 ⇒ indeterminate, never nominated", () => {
234
234
  assert.ok(v.failures.some((f) => f.includes("variance undefined")));
235
235
  assert.notEqual(v.exitCode, 0);
236
236
  });
237
+ it("campaign-6 regression: incumbent train unit n=0 ⇒ indeterminate, never a bogus negative gain", () => {
238
+ const v = evaluate({
239
+ candidate: candidate("g-c6", [
240
+ { unitId: "u-train", split: "train", scores: [0.8333333, 0.8333333] },
241
+ { unitId: "u-val", split: "val", scores: [1, 1] },
242
+ ]),
243
+ incumbent: { units: [
244
+ { unitId: "u-train", split: "train", scores: [] },
245
+ { unitId: "u-val", split: "val", scores: [0.875, 1] },
246
+ ] },
247
+ stats: STATS,
248
+ budgetCaps: CAPS,
249
+ nPairs: 1,
250
+ });
251
+ assert.equal(v.verdict, "indeterminate", summaryOf(v));
252
+ assert.ok(v.failures.some((f) => f.includes("baseline variance undefined")));
253
+ assert.equal(v.gain, null);
254
+ });
237
255
  it("candidate missing an incumbent val unit entirely ⇒ indeterminate", () => {
238
256
  const v = evaluate({
239
257
  candidate: candidate("g-miss", [unit("u-train", "train", 0.85, 0.001)]),
@@ -0,0 +1,72 @@
1
+ // S4 engine seam (deliverable f) — `abathur status` must resolve the stored
2
+ // repoPath through effectiveRepoPath (the run/genome/kernel/self-eval pattern,
3
+ // run-loop.ts:142) before touching ANY filesystem seam. Pre-fix, status passed
4
+ // the STORED UNRESOLVED `${VAR}` literal as the git cwd; Node reports a missing
5
+ // cwd as the misleading `spawn git ENOENT` (task-09 evidence §C). Pins:
6
+ // (1) env-literal genome + env exported ⇒ status exits 0 (real git + ledger
7
+ // seam on the resolved tree — never an ENOENT);
8
+ // (2) env-literal genome + env UNSET ⇒ clean exit 2 NAMING the variable, and
9
+ // the misleading spawn-ENOENT string is gone;
10
+ // (3) concrete repoPath keeps working unchanged (regression guard).
11
+ import assert from "node:assert/strict";
12
+ import { spawnSync } from "node:child_process";
13
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { mkdtempSync, rmSync } from "node:fs";
15
+ import * as os from "node:os";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { test } from "node:test";
19
+ import { registerGenome } from "../core/genome.js";
20
+ import { prepareToyGenome } from "../bench/toy.js";
21
+ const CLI = fileURLToPath(new URL("../../dist/cli.js", import.meta.url));
22
+ const LIT_VAR = "ABATHUR_SEAM_STATUS_REPO";
23
+ async function fixture(t, label) {
24
+ const root = mkdtempSync(path.join(os.tmpdir(), "seam-status-"));
25
+ t.after(() => rmSync(root, { recursive: true, force: true }));
26
+ const configDir = path.join(root, "config");
27
+ mkdirSync(configDir, { recursive: true });
28
+ writeFileSync(path.join(configDir, "config.jsonc"), '{ "opencodeBin": null }\n', "utf8");
29
+ const repo = await prepareToyGenome(path.join(root, "genome"));
30
+ // Register under the exported env (add resolves the same way run does),
31
+ // storing the spec with the UNRESOLVED literal (0.2.x convention).
32
+ const doc = JSON.parse(readFileSync(path.join(repo, "genome.jsonc"), "utf8"));
33
+ doc["label"] = label;
34
+ doc["repoPath"] = `\${${LIT_VAR}}`;
35
+ const specFile = path.join(root, "private.jsonc");
36
+ writeFileSync(specFile, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
37
+ process.env[LIT_VAR] = repo;
38
+ try {
39
+ registerGenome(configDir, specFile);
40
+ }
41
+ finally {
42
+ delete process.env[LIT_VAR];
43
+ }
44
+ return { root, configDir, repo };
45
+ }
46
+ function cli(f, label, env) {
47
+ return spawnSync(process.execPath, [CLI, "status", label], {
48
+ encoding: "utf8",
49
+ cwd: f.root,
50
+ env: {
51
+ ...process.env,
52
+ ABATHUR_CONFIG: path.join(f.configDir, "config.jsonc"),
53
+ HOME: path.join(f.root, "home"),
54
+ XDG_CACHE_HOME: path.join(f.root, "xdg-cache"),
55
+ ...env,
56
+ },
57
+ });
58
+ }
59
+ test("status resolves an env-literal repoPath via effectiveRepoPath: exported ⇒ exit 0", async (t) => {
60
+ const f = await fixture(t, "seam-self");
61
+ const r = cli(f, "seam-self", { [LIT_VAR]: f.repo });
62
+ assert.equal(r.status, 0, `stdout: ${r.stdout}\nstderr: ${r.stderr}`);
63
+ assert.match(r.stdout, /genome: seam-self/);
64
+ assert.ok(!r.stderr.includes("ENOENT"), "the misleading spawn-ENOENT string must be gone");
65
+ });
66
+ test("status on an env-literal repoPath with the env UNSET: clean exit 2 naming the variable", async (t) => {
67
+ const f = await fixture(t, "seam-unset");
68
+ const r = cli(f, "seam-unset", { [LIT_VAR]: undefined });
69
+ assert.equal(r.status, 2);
70
+ assert.match(r.stderr, new RegExp(`requires env var ${LIT_VAR}`));
71
+ assert.ok(!r.stderr.includes("ENOENT"), `misleading ENOENT returned: ${r.stderr}`);
72
+ });
@@ -1,10 +1,14 @@
1
1
  // Type declarations for grader-core.mjs — the plan-185 scoring engine.
2
2
  // Runtime lives in the .mjs; this file is the compile-time contract.
3
3
 
4
+ import type { ToolEvent } from "./grader-support.mjs";
5
+
4
6
  export type DimKey = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H";
5
7
  export type Bit = 0 | 1;
6
8
  export type FullDims = Readonly<Record<DimKey, Bit>>;
7
9
  export type WorthDims = Readonly<{ G: Bit; H: Bit; J: Bit }>;
10
+ export type IntegrityDims = Readonly<{ I: Bit; J: Bit }>;
11
+ export type IntegrityNote = string;
8
12
 
9
13
  export interface CreatedPage {
10
14
  readonly path: string;
@@ -13,6 +17,26 @@ export interface CreatedPage {
13
17
  readonly content: string;
14
18
  }
15
19
 
20
+ /** Post-run sandbox row + seed captures the I/J checkers diff evidence against.
21
+ * Twin-row units (scenario-13: one path, en+zh rows; scenario-15: the created
22
+ * archive twin) key the locale-aware maps `rowIdByLocalePath`/
23
+ * `seedRowIdByLocalePath` by `path + "\u0000" + locale`; scenario-14/15 seeds
24
+ * are EN-only but ride the same locale-keyed lookups. */
25
+ export interface IntegrityState {
26
+ readonly sandboxRows: ReadonlyArray<{ readonly path: string; readonly id: string; readonly description: string; readonly locale?: string | undefined }>;
27
+ readonly content: Readonly<Record<string, string>>;
28
+ readonly rowIdByPath: ReadonlyMap<string, string>;
29
+ readonly rowIdByLocalePath?: ReadonlyMap<string, string> | undefined;
30
+ readonly descByPath: ReadonlyMap<string, string>;
31
+ readonly seedDescByPath: ReadonlyMap<string, string>;
32
+ readonly seedRowIdByLocalePath?: ReadonlyMap<string, string> | undefined;
33
+ readonly seedContent: Readonly<Record<string, string>>;
34
+ /** scenario-17 only: the parsed `.bench/judge-verdicts.json` instrument output
35
+ * (judge-bench blind double runs + B4 §4 arbitration rows). undefined = the
36
+ * file is absent (the 蜂判 legs fail CLOSED); {parseError} = unparseable. */
37
+ readonly judgeVerdicts?: unknown;
38
+ }
39
+
16
40
  export interface Observation {
17
41
  readonly scenarioNo: number;
18
42
  readonly created: readonly CreatedPage[];
@@ -31,6 +55,10 @@ export interface Observation {
31
55
  readonly backlinkBodies: ReadonlyArray<{ readonly path: string; readonly locale: string; readonly content: string }>;
32
56
  readonly finalMessage: string;
33
57
  readonly urlChecks: readonly { readonly url: string; readonly status: number }[];
58
+ /** Required for integrity units (scenario-10/11/12): transcript tool events. */
59
+ readonly tools?: readonly ToolEvent[] | undefined;
60
+ /** Required for integrity units: seed-state-derived wiki context. */
61
+ readonly integrity?: IntegrityState | undefined;
34
62
  }
35
63
 
36
64
  export interface UnitScore {
@@ -47,7 +75,100 @@ export type ScoredUnit = UnitScore & {
47
75
 
48
76
  export const WEIGHTS: Readonly<Record<DimKey, number>>;
49
77
 
78
+ export const S10_PATH: string;
79
+ export const S10_DECOY: string;
80
+ export const S11_CARD: string;
81
+ export const S11_SUPERSEDE: string;
82
+ export const S11_SEED_DATE: string;
83
+ export const VERIFY_TOKEN: string;
84
+ export const S12_JUNK: readonly string[];
85
+ export const S12_DECOY: readonly string[];
86
+ export const S13_RUNBOOK: string;
87
+ export const S13_DECOY: string;
88
+ export const S13_FAB_STAMP: string;
89
+ export const S13_FAB_MIGRATE: string;
90
+ export const S14_HUB: string;
91
+ export const S14_OPS: string;
92
+ export const S14_DUP_A: string;
93
+ export const S14_DUP_B: string;
94
+ export const S14_ORPHAN: string;
95
+ export const S14_DECOY: string;
96
+ export const S14_GHOST_A: string;
97
+ export const S14_GHOST_B: string;
98
+ export const S15_HUB: string;
99
+ export const S15_LEGACY: string;
100
+ export const S15_INCIDENT: string;
101
+ export const S15_DORMANT: string;
102
+ export const S15_ARCHIVE: string;
103
+ export const S15_TOKEN: string;
104
+ export const S16_HUB: string;
105
+ export const S16_EXEMPLAR: string;
106
+ export const S16_BAIT: string;
107
+ export const S16_PAGE: string;
108
+ export const S17_HUB: string;
109
+ export const S17_SOURCE: string;
110
+ export const S17_BAIT: string;
111
+ export const S17_PAGE: string;
112
+ export const S17_JUDGE_RUBRICS: readonly string[];
113
+ export const S17_JUDGE_LOCALES: readonly string[];
114
+ export const APPLICABLE: Readonly<Record<number, Readonly<Record<string, number>>>>;
115
+
116
+ /** Doctrine machine-line verdict (scenario-16): deterministic check result. */
117
+ export interface DoctrineCheck {
118
+ readonly ok: boolean;
119
+ readonly why?: string | undefined;
120
+ readonly exempt?: boolean | undefined;
121
+ }
122
+
123
+ export interface DoctrineSignature {
124
+ readonly h: number;
125
+ readonly t: number;
126
+ readonly c: number;
127
+ readonly b: number;
128
+ }
129
+
130
+ /** (A) R1 形态门 — S1 stamp-form closed set, pre-first-H2 block, cap-5 tail. */
131
+ export function s16R1FormGate(content: string): DoctrineCheck;
132
+ /** (B) R2 — cell ≤120 chars, rendered row ≤120 cols, >20 rows need a grouping row. */
133
+ export function s16R2Tables(content: string): DoctrineCheck;
134
+ /** (C) R3 — emphasis spans per 2000 narrative non-ws chars, en ≤22 / zh ≤35. */
135
+ export function s16R3Emphasis(content: string, locale: string): DoctrineCheck;
136
+ /** (D) R9 — trailer dangling / normalized long-line repeat / placeholder closed set. */
137
+ export function s16R9Hygiene(content: string, region?: "page" | "trailer"): DoctrineCheck;
138
+ /** (E) R5 — twin structure signature (h, t, c, b), per-axis deviation cap. */
139
+ export function s16Signature(content: string): DoctrineSignature;
140
+ export function s16R5TwinParity(enBody: string, zhBody: string): DoctrineCheck;
141
+
142
+ export interface StatusTokens {
143
+ readonly header: string | null;
144
+ readonly rows: readonly string[];
145
+ }
146
+
147
+ /** Deterministic fold over `.bench/judge-verdicts.json` (scenario-17 蜂判 lines).
148
+ * coverageOk = the expected (rubric × locale) key set is fully double-run covered
149
+ * (2 ok reps, rep3 when the two disagree), malformed rows excluded-with-count;
150
+ * allOne = every expected key resolves to a majority 1. Absent/unparseable input
151
+ * fails both closed. Pure — no LLM, no IO. */
152
+ export interface JudgeFold {
153
+ readonly coverageOk: boolean;
154
+ readonly allOne: boolean;
155
+ readonly coverageNotes: readonly string[];
156
+ readonly foldNotes: readonly string[];
157
+ readonly majority: ReadonlyMap<string, 0 | 1>;
158
+ }
159
+ export function s17FoldJudgeVerdicts(verdicts: unknown, page?: string): JudgeFold;
160
+
161
+ export interface IntegrityResult extends IntegrityDims {
162
+ readonly notes: readonly IntegrityNote[];
163
+ }
164
+
50
165
  export function computeDims(obs: Observation): FullDims;
51
166
  export function judgment(scenarioNo: number, created: readonly CreatedPage[], finalMessage: string): Bit;
52
167
  export function scoreFromDims(scenarioNo: number, dims: FullDims | WorthDims): UnitScore;
53
168
  export function scoreUnit(obs: Observation): ScoredUnit;
169
+ export function statusTokens(content: string): StatusTokens;
170
+ /** Parse a tool event's output as JSON, following opencode's >45KB externalization
171
+ * stubs (`Full output saved to: <ref>`) to the ref file. Fail-closed: undefined
172
+ * when neither the inline output nor the referenced file parses. */
173
+ export function resolveToolJson(event: ToolEvent): unknown;
174
+ export function integrityDims(scenarioNo: number, obs: Observation, tools: readonly ToolEvent[], integrity: IntegrityState): IntegrityResult;