@avee1234/selfpatch 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.
package/src/verify.js ADDED
@@ -0,0 +1,103 @@
1
+ // selfpatch — `verify` (v0.1). The reward-hacking tripwire.
2
+ //
3
+ // Turn a self-patch's `eval_contract` into a list of checks and run each check
4
+ // twice — once for the **before** (baseline) world and once for the **after**
5
+ // (candidate) world — through an injected `runCheck(command, phase)`, then
6
+ // report `{ passed, regressed, checks }`. `passed` = candidate green now;
7
+ // `regressed` = was green before, red after. A self-edit that games its visible
8
+ // signal but breaks the held-out check fails here, by construction.
9
+ //
10
+ // Pure and synchronous: the core does NO process, filesystem, or network I/O of
11
+ // its own. `runCheck` is the only side-effecting seam, so the tripwire logic is
12
+ // fully unit-testable with a fake executor. This module must never
13
+ // `import node:child_process` — all execution lives in the CLI.
14
+
15
+ // Thrown when handed a patch that cannot be verified: a missing/non-function
16
+ // `runCheck`, or an `eval_contract` that yields no runnable command. Fail
17
+ // closed — an unverifiable self-patch is blocked, not silently "passed".
18
+ // Named/exported per the #3 spec (mirrors `ProposeError` from #2) so callers
19
+ // can rely on the type.
20
+ export class VerifyError extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = "VerifyError";
24
+ }
25
+ }
26
+
27
+ // Normalize an `eval_contract` into a list of `{ kind, command }` checks. v0.1:
28
+ // a single `{ kind: "command", command }` ⇒ a one-element array. Shaped as an
29
+ // array so a future multi-command contract slots in without changing the return
30
+ // shape. Throws `VerifyError` if no non-empty command string is present or the
31
+ // kind is unsupported.
32
+ function normalizeChecks(evalContract) {
33
+ if (evalContract === null || typeof evalContract !== "object" || Array.isArray(evalContract)) {
34
+ throw new VerifyError(
35
+ "verifySelfPatch: `eval_contract` must be an object with a runnable command"
36
+ );
37
+ }
38
+ const { kind, command } = evalContract;
39
+ if (kind !== "command") {
40
+ throw new VerifyError(
41
+ `verifySelfPatch: unsupported eval_contract.kind ${JSON.stringify(kind)} (v0.1 supports "command")`
42
+ );
43
+ }
44
+ if (typeof command !== "string" || command.trim().length === 0) {
45
+ throw new VerifyError(
46
+ "verifySelfPatch: eval_contract.command must be a non-empty string"
47
+ );
48
+ }
49
+ return [{ kind, command }];
50
+ }
51
+
52
+ // A check is green iff its result exists and exited 0. Everything else —
53
+ // nonzero, `null` (timeout/signal), a missing result — is red. Fail closed.
54
+ function isGreen(result) {
55
+ return result != null && result.exitCode === 0;
56
+ }
57
+
58
+ /**
59
+ * Run a self-patch's held-out check in two worlds and report the verdict.
60
+ *
61
+ * @param {object} patch A self-patch object (as produced by `propose`, valid
62
+ * per #1). Only its `eval_contract` is read; nothing is applied.
63
+ * @param {object} opts
64
+ * @param {(command: string, phase: "before"|"after") => object} opts.runCheck
65
+ * Injected executor. `phase` is `"before"` (baseline) or `"after"`
66
+ * (candidate). It returns a result whose `exitCode` (number, or `null` for
67
+ * timeout/signal) decides green (`0`) vs red (anything else); any extra fields
68
+ * (`stdout`, `stderr`, `durationMs`, `timedOut`, …) pass through into the
69
+ * report unchanged. May be called with the same world for both phases (no
70
+ * distinct baseline) — then `regressed` can never fire, the safe default.
71
+ * @returns {{ passed: boolean, regressed: boolean, checks: Array<object> }}
72
+ * `passed` = every check green after; `regressed` = any check green before and
73
+ * red after. Each `checks[i]` carries `{ command, kind, before, after,
74
+ * passed, regressed }`.
75
+ * @throws {VerifyError} If `runCheck` is missing/not a function, or the
76
+ * `eval_contract` yields no runnable command / an unsupported kind.
77
+ */
78
+ export function verifySelfPatch(patch, { runCheck } = {}) {
79
+ if (typeof runCheck !== "function") {
80
+ throw new VerifyError("verifySelfPatch: `runCheck` must be a function");
81
+ }
82
+ const patchObj = patch !== null && typeof patch === "object" ? patch : {};
83
+ const checks = normalizeChecks(patchObj.eval_contract);
84
+
85
+ const results = checks.map(({ kind, command }) => {
86
+ const before = runCheck(command, "before");
87
+ const after = runCheck(command, "after");
88
+ return {
89
+ command,
90
+ kind,
91
+ before,
92
+ after,
93
+ passed: isGreen(after),
94
+ regressed: isGreen(before) && !isGreen(after),
95
+ };
96
+ });
97
+
98
+ return {
99
+ passed: results.every((c) => c.passed),
100
+ regressed: results.some((c) => c.regressed),
101
+ checks: results,
102
+ };
103
+ }