toolroll 0.9.81 → 0.9.82

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 (137) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cli.js +1 -0
  3. package/dist/commit-extract.d.ts +18 -0
  4. package/dist/commit-extract.js +39 -1
  5. package/dist/converse.d.ts +12 -0
  6. package/dist/converse.js +18 -2
  7. package/dist/deploy-gate.js +8 -0
  8. package/dist/eval-gate.d.ts +51 -0
  9. package/dist/eval-gate.js +199 -0
  10. package/dist/eval-hooks.d.ts +39 -0
  11. package/dist/eval-hooks.js +477 -0
  12. package/dist/eval-oracle-main.d.ts +1 -0
  13. package/dist/eval-oracle-main.js +31 -0
  14. package/dist/eval-oracle.d.ts +60 -0
  15. package/dist/eval-oracle.js +217 -0
  16. package/dist/eval-world.d.ts +17 -0
  17. package/dist/eval-world.js +97 -0
  18. package/dist/evals.d.ts +299 -0
  19. package/dist/evals.js +964 -0
  20. package/dist/exec.js +15 -1
  21. package/dist/flow-draft.js +4 -0
  22. package/dist/flow-sort.js +4 -0
  23. package/dist/github-jobs.d.ts +25 -0
  24. package/dist/github-jobs.js +22 -0
  25. package/dist/invoke.d.ts +1 -1
  26. package/dist/invoke.js +14 -0
  27. package/dist/lead.js +4 -1
  28. package/dist/memory-pass.js +4 -0
  29. package/dist/nightly-check.d.ts +4 -1
  30. package/dist/nightly-check.js +5 -2
  31. package/dist/nightly-github.d.ts +316 -0
  32. package/dist/nightly-github.js +1022 -0
  33. package/dist/operate.d.ts +91 -1
  34. package/dist/operate.js +158 -1
  35. package/dist/project-knowledge.d.ts +4 -4
  36. package/dist/provider.d.ts +3 -0
  37. package/dist/provider.js +9 -2
  38. package/dist/release-adapters.d.ts +21 -21
  39. package/dist/release-adapters.js +52 -19
  40. package/dist/release-auto.js +26 -0
  41. package/dist/release-card.js +3 -0
  42. package/dist/release-cli.js +59 -3
  43. package/dist/release-view.d.ts +3 -0
  44. package/dist/release-view.js +2 -0
  45. package/dist/release.d.ts +16 -0
  46. package/dist/release.js +16 -0
  47. package/dist/reported-usage.d.ts +69 -0
  48. package/dist/reported-usage.js +97 -0
  49. package/dist/server/chat.js +4 -0
  50. package/dist/server/render-tasks.js +1 -0
  51. package/dist/server/tasks.js +8 -1
  52. package/dist/store.d.ts +109 -1
  53. package/dist/store.js +394 -4
  54. package/dist/subagents.js +4 -0
  55. package/dist/subscription-chat.d.ts +2 -0
  56. package/dist/subscription-chat.js +52 -13
  57. package/dist/task-sizing.js +4 -0
  58. package/dist/teardown-git.d.ts +9 -2
  59. package/dist/teardown-git.js +56 -33
  60. package/evals/cases/catalog/case.json +25 -0
  61. package/evals/cases/catalog/fixture/README.md +3 -0
  62. package/evals/cases/catalog/fixture/package.json +8 -0
  63. package/evals/cases/catalog/fixture/test/catalog.test.mjs +4 -0
  64. package/evals/cases/catalog/oracle.mjs +23 -0
  65. package/evals/cases/catalog/probe.mjs +10 -0
  66. package/evals/cases/catalog/source.md +9 -0
  67. package/evals/cases/csv-quoting/case.json +23 -0
  68. package/evals/cases/csv-quoting/fixture/README.md +3 -0
  69. package/evals/cases/csv-quoting/fixture/csv.js +1 -0
  70. package/evals/cases/csv-quoting/fixture/package.json +8 -0
  71. package/evals/cases/csv-quoting/fixture/test/csv.test.mjs +5 -0
  72. package/evals/cases/csv-quoting/oracle.mjs +14 -0
  73. package/evals/cases/csv-quoting/probe.mjs +9 -0
  74. package/evals/cases/csv-quoting/source.md +9 -0
  75. package/evals/cases/dedupe/case.json +23 -0
  76. package/evals/cases/dedupe/fixture/README.md +3 -0
  77. package/evals/cases/dedupe/fixture/dedupe.js +1 -0
  78. package/evals/cases/dedupe/fixture/package.json +8 -0
  79. package/evals/cases/dedupe/fixture/test/dedupe.test.mjs +5 -0
  80. package/evals/cases/dedupe/oracle.mjs +16 -0
  81. package/evals/cases/dedupe/probe.mjs +13 -0
  82. package/evals/cases/dedupe/source.md +9 -0
  83. package/evals/cases/endpoint/case.json +25 -0
  84. package/evals/cases/endpoint/fixture/README.md +3 -0
  85. package/evals/cases/endpoint/fixture/package.json +8 -0
  86. package/evals/cases/endpoint/fixture/test/endpoint.test.mjs +4 -0
  87. package/evals/cases/endpoint/oracle.mjs +27 -0
  88. package/evals/cases/endpoint/probe.mjs +10 -0
  89. package/evals/cases/endpoint/source.md +11 -0
  90. package/evals/cases/math-divide/case.json +23 -0
  91. package/evals/cases/math-divide/fixture/README.md +3 -0
  92. package/evals/cases/math-divide/fixture/package.json +8 -0
  93. package/evals/cases/math-divide/fixture/src/math.js +1 -0
  94. package/evals/cases/math-divide/fixture/test/math.test.js +5 -0
  95. package/evals/cases/math-divide/oracle.mjs +13 -0
  96. package/evals/cases/math-divide/probe.mjs +9 -0
  97. package/evals/cases/math-divide/source.md +9 -0
  98. package/evals/cases/math-multiply/case.json +23 -0
  99. package/evals/cases/math-multiply/fixture/README.md +3 -0
  100. package/evals/cases/math-multiply/fixture/package.json +8 -0
  101. package/evals/cases/math-multiply/fixture/src/math.js +2 -0
  102. package/evals/cases/math-multiply/fixture/test/math.test.js +5 -0
  103. package/evals/cases/math-multiply/oracle.mjs +12 -0
  104. package/evals/cases/math-multiply/probe.mjs +9 -0
  105. package/evals/cases/math-multiply/source.md +9 -0
  106. package/evals/cases/math-subtract/case.json +23 -0
  107. package/evals/cases/math-subtract/fixture/README.md +3 -0
  108. package/evals/cases/math-subtract/fixture/package.json +8 -0
  109. package/evals/cases/math-subtract/fixture/src/math.js +1 -0
  110. package/evals/cases/math-subtract/fixture/test/math.test.js +5 -0
  111. package/evals/cases/math-subtract/oracle.mjs +12 -0
  112. package/evals/cases/math-subtract/probe.mjs +9 -0
  113. package/evals/cases/math-subtract/source.md +9 -0
  114. package/evals/cases/merge-config/case.json +23 -0
  115. package/evals/cases/merge-config/fixture/README.md +3 -0
  116. package/evals/cases/merge-config/fixture/config.js +1 -0
  117. package/evals/cases/merge-config/fixture/package.json +8 -0
  118. package/evals/cases/merge-config/fixture/test/config.test.mjs +5 -0
  119. package/evals/cases/merge-config/oracle.mjs +28 -0
  120. package/evals/cases/merge-config/probe.mjs +15 -0
  121. package/evals/cases/merge-config/source.md +9 -0
  122. package/evals/cases/paginate/case.json +23 -0
  123. package/evals/cases/paginate/fixture/README.md +3 -0
  124. package/evals/cases/paginate/fixture/package.json +8 -0
  125. package/evals/cases/paginate/fixture/paginate.js +1 -0
  126. package/evals/cases/paginate/fixture/test/paginate.test.mjs +5 -0
  127. package/evals/cases/paginate/oracle.mjs +18 -0
  128. package/evals/cases/paginate/probe.mjs +13 -0
  129. package/evals/cases/paginate/source.md +9 -0
  130. package/evals/cases/round2-asks/case.json +24 -0
  131. package/evals/cases/round2-asks/fixture/README.md +3 -0
  132. package/evals/cases/round2-asks/fixture/package.json +8 -0
  133. package/evals/cases/round2-asks/fixture/src/math.js +1 -0
  134. package/evals/cases/round2-asks/fixture/test/math.test.js +5 -0
  135. package/evals/cases/round2-asks/source.md +11 -0
  136. package/evals/manifest.json +15 -0
  137. package/package.json +2 -1
@@ -0,0 +1,477 @@
1
+ /**
2
+ * The eval and nightly enforcement hooks a release can't drop without a person (docs/plans/evals-nightly.md, part C,
3
+ * "Policy code needs a person", Codex round 2 #10 and C1 round 1 #5, #6).
4
+ *
5
+ * The policy modules themselves are person-only by path (release-auto.ts autoPathProblem: `evals/`, `src/evals*.ts`,
6
+ * `src/eval-*.ts`, `src/reviewer-evals.ts`, `src/nightly-github.ts`, `src/contracts/evals*.ts`, and the accounting
7
+ * readings `src/reported-usage.ts` and `src/run-efficiency.ts`). The calls into them, and the store's accounting reads,
8
+ * live in large product files that ship without a person, so rather than protect those files whole this manifest
9
+ * lists each required call and the function it must be in (EVAL_HOOKS), and each function whose body a release may not
10
+ * change by itself (EVAL_PINNED). The INSTALLED plane checks a candidate's tree against it (`evalHookProblem`, called
11
+ * from release-auto.ts changeProblem): a listed call missing or moved out of its function, its function gone, a pinned
12
+ * body changed, or a listed file that doesn't parse, needs a person. This file is itself a policy path.
13
+ *
14
+ * The check reads the syntax tree (TypeScript's own parser, the runtime dependency `typescript-parser`): only a real
15
+ * call expression of that name inside that top-level function counts, never a comment, a string or a regular
16
+ * expression. The name must also be bound to what the manifest says (`from`): an un-renamed named import from that
17
+ * module, the file's own top-level function, or a local of the function itself, declared exactly once in the file and
18
+ * never assigned (PR #263 review: a renamed import beside a local stand-in of the same name passed). Each enclosing
19
+ * function and pinned body is held the same way, and a pinned `Class#method` is that class's one method of that name,
20
+ * never assigned or redefined by name. With `args`, the call's arguments must read as the base has them. It proves
21
+ * the call is in its function, not that it is reached (a call behind `if (false)` still reads as present); a person
22
+ * reviewing a change to these files remains the backstop.
23
+ */
24
+ import { createHash } from "node:crypto";
25
+ import { createRequire } from "node:module";
26
+ let compiler;
27
+ const typescript = () => compiler ??= createRequire(import.meta.url)("typescript-parser");
28
+ export const EVAL_HOOKS = Object.freeze([
29
+ { file: "src/deploy-gate.ts", inside: "deploymentGate", calls: "nightlyDebtHold", from: "./nightly-github.js", why: "GitHub's nightly holds every deploy" },
30
+ { file: "src/store.ts", inside: "reserveRelease", calls: "releaseNightlyProblem", from: "self", why: "GitHub's nightly holds the merge reservation" },
31
+ { file: "src/store.ts", inside: "runReleaseOperation", calls: "releaseNightlyProblem", from: "self", why: "GitHub's nightly holds a reserved operation's start" },
32
+ { file: "src/store.ts", inside: "releaseNightlyProblem", calls: "nightlyDebtHold", from: "./nightly-github.js", why: "the store's nightly check reads the debt" },
33
+ { file: "src/release.ts", inside: "runRelease", calls: "nightlyHolds", from: "local", why: "GitHub's nightly holds deploy, merge and the tag" },
34
+ { file: "src/release-adapters.ts", inside: "storeReleaseAdapter", calls: "nightlyBoundary", from: "./nightly-github.js", why: "the release's boundary read of GitHub's nightly" },
35
+ { file: "src/release-auto.ts", inside: "sealAutoGreen", calls: "nightlyDebtHold", from: "./nightly-github.js", why: "GitHub's nightly holds automatic approval" },
36
+ { file: "src/release-auto.ts", inside: "changeProblem", calls: "evalHookProblem", from: "./eval-hooks.js", why: "this check itself" },
37
+ { file: "src/release-auto.ts", inside: "changeProblem", calls: "autoPathProblem", from: "self", why: "policy paths need a person" },
38
+ { file: "src/release-auto.ts", inside: "changeProblem", calls: "storeRewriteProblem", from: "./eval-hooks.js", why: "no changed file rewrites a pinned Store method" },
39
+ // The invocation gateway (invoke.ts): every agent run an eval world spawns takes a slot.
40
+ { file: "src/invoke.ts", inside: "invokeAgent", calls: "takeEvalDispatch", from: "./eval-gate.js", why: "every agent dispatch in an eval world takes a slot" },
41
+ // PR #263 review: what the run's billing basis and its unknown cost are computed from is held too, not only the call.
42
+ { file: "src/invoke.ts", inside: "invokeAgent", calls: "recordRunBillingBasis", from: "./store.js", args: true, why: "a run's billing says whether it was known or guessed" },
43
+ { file: "src/invoke.ts", inside: "invokeAgent", calls: "recordRunCostUnknown", from: "./store.js", args: true, why: "a run whose cost its provider never reported stays unknown money" },
44
+ { file: "src/subscription-chat.ts", inside: "performSubscriptionLeadRequest", calls: "takeEvalDispatch", from: "./eval-gate.js", why: "every subscription lead request in an eval world takes a slot" },
45
+ { file: "src/converse.ts", inside: "performLeadRequest", calls: "takeEvalDispatch", from: "./eval-gate.js", why: "every direct lead request in an eval world takes a slot" },
46
+ { file: "src/converse.ts", inside: "performChatRequest", calls: "takeEvalDispatch", from: "./eval-gate.js", why: "every fleet chat request in an eval world takes a slot" },
47
+ { file: "src/memory-pass.ts", inside: "defaultAnalyzer", calls: "inEvalWorld", from: "./eval-gate.js", why: "the memory pass makes no unrecorded call in an eval world" },
48
+ { file: "src/task-sizing.ts", inside: "claudeSizer", calls: "inEvalWorld", from: "./eval-gate.js", why: "task sizing makes no unrecorded call in an eval world" },
49
+ { file: "src/flow-draft.ts", inside: "claudeDraftRunner", calls: "inEvalWorld", from: "./eval-gate.js", why: "flow drafts make no unrecorded call in an eval world" },
50
+ { file: "src/subagents.ts", inside: "claudeTurnRunner", calls: "inEvalWorld", from: "./eval-gate.js", why: "subagent turns make no unrecorded call in an eval world" },
51
+ { file: "src/flow-sort.ts", inside: "askJev", calls: "inEvalWorld", from: "./eval-gate.js", why: "flow sort steps make no unrecorded call in an eval world" },
52
+ ]);
53
+ export const EVAL_PINNED = Object.freeze([
54
+ { file: "src/store.ts", fn: "runAccounting", why: "how a run's spending reads as API dollars, subscription usage or unknown" },
55
+ { file: "src/store.ts", fn: "chatTurnAccounting", why: "how a chat request's spending reads" },
56
+ { file: "src/store.ts", fn: "recordRunModelReported", why: "the exact model a run is held to its pin by" },
57
+ { file: "src/store.ts", fn: "recordRunBillingBasis", why: "whether a run's billing was known" },
58
+ { file: "src/store.ts", fn: "recordRunCostUnknown", why: "a run's unknown cost, once set, is never cleared" },
59
+ // PR #263 review: the store's own writes of what a run or chat request spent.
60
+ { file: "src/store.ts", fn: "Store#recordUsage", why: "what a run's provider said it cost" },
61
+ { file: "src/store.ts", fn: "Store#settleRunSpend", why: "how a run's spending is priced" },
62
+ { file: "src/store.ts", fn: "Store#fixRunBilling", why: "how a run was billed" },
63
+ { file: "src/store.ts", fn: "Store#finalizeChatTurn", why: "what a chat request spent and how it was billed" },
64
+ // PR #263 security scan: sealed on the prototype from the class's first static block (before any other code reaches
65
+ // it), and locked on every instance by the constructor.
66
+ { file: "src/store.ts", fn: "Store#static", why: "the pinned Store methods are sealed as the class is defined" },
67
+ { file: "src/store.ts", fn: "Store#constructor", why: "each Store instance locks the pinned methods" },
68
+ { file: "src/store.ts", fn: "sealStoreMoneyMethods", why: "the pinned Store methods can't be replaced at run time" },
69
+ { file: "src/store.ts", fn: "lockStoreMoneyMethods", why: "the pinned Store methods can't be shadowed on an instance" },
70
+ ]);
71
+ /** The Store methods pinned by body, which no file may write over (storeRewriteProblem, store.ts sealStoreMoneyMethods). */
72
+ export const PINNED_STORE_METHODS = Object.freeze(EVAL_PINNED.filter(pin => pin.fn.startsWith("Store#") && pin.fn !== "Store#static" && pin.fn !== "Store#constructor")
73
+ .map(pin => pin.fn.slice("Store#".length)));
74
+ /** A file's syntax tree, or why it can't be read (missing, or not valid TypeScript: a person decides). */
75
+ function parse(file, bytes) {
76
+ if (bytes === null)
77
+ return { problem: `${file} can't be read` };
78
+ const compiler = typescript();
79
+ const source = compiler.createSourceFile(file, bytes.toString("utf8"), compiler.ScriptTarget.Latest, true, compiler.ScriptKind.TS);
80
+ const diagnostics = source.parseDiagnostics ?? [];
81
+ return diagnostics.length === 0 ? { source } : { problem: `${file} doesn't parse` };
82
+ }
83
+ /** The one top-level function declaration with this name, or null when there is none or more than one. */
84
+ function topFunction(source, name) {
85
+ const compiler = typescript();
86
+ const found = source.statements.filter((one) => compiler.isFunctionDeclaration(one) && one.name?.text === name);
87
+ return found.length === 1 ? found[0] : null;
88
+ }
89
+ /** `Class#method`: the top-level class's one method of that name (no overload, accessor or property of it either),
90
+ * never assigned or redefined by name anywhere in the file; null otherwise. `Class#constructor` is its one
91
+ * constructor; `Class#static` its one static block, which must be the first of its static elements that runs code
92
+ * (no static field initializer or other block runs before it). A class with a decorator anywhere (on it, a member or a
93
+ * parameter: code that runs before the seal) holds none. */
94
+ function classMethod(source, spec) {
95
+ const compiler = typescript();
96
+ const [className, method] = spec.split("#");
97
+ const classes = source.statements.filter((one) => compiler.isClassDeclaration(one) && one.name?.text === className);
98
+ if (classes.length !== 1 || hasDecorator(classes[0]))
99
+ return null;
100
+ if (method === "constructor") {
101
+ const made = classes[0].members.filter(member => compiler.isConstructorDeclaration(member));
102
+ return made.length === 1 && made[0].body !== undefined ? made[0] : null;
103
+ }
104
+ if (method === "static") {
105
+ const runs = classes[0].members.filter(member => compiler.isClassStaticBlockDeclaration(member)
106
+ || (compiler.isPropertyDeclaration(member) && member.initializer !== undefined && (compiler.getModifiers(member) ?? []).some(one => one.kind === compiler.SyntaxKind.StaticKeyword)));
107
+ const blocks = runs.filter(member => compiler.isClassStaticBlockDeclaration(member));
108
+ return blocks.length === 1 && runs[0] === blocks[0] ? blocks[0] : null;
109
+ }
110
+ const named = classes[0].members.filter(member => member.name !== undefined && (compiler.isIdentifier(member.name) || compiler.isStringLiteral(member.name)) && member.name.text === method);
111
+ if (named.length !== 1 || !compiler.isMethodDeclaration(named[0]) || named[0].body === undefined)
112
+ return null;
113
+ return memberRewritten(source, method) ? null : named[0];
114
+ }
115
+ /** Whether a decorator appears anywhere in this node. */
116
+ function hasDecorator(node) {
117
+ const compiler = typescript();
118
+ if (compiler.isDecorator(node))
119
+ return true;
120
+ let hit = false;
121
+ compiler.forEachChild(node, one => { if (!hit && hasDecorator(one))
122
+ hit = true; });
123
+ return hit;
124
+ }
125
+ /** The class header (modifiers, name, type parameters and heritage, up to its opening brace), whitespace normalized:
126
+ * pinned with `Class#static`, so a changed `extends` or modifier needs a person. */
127
+ function classHeader(source, node) {
128
+ const compiler = typescript();
129
+ const owner = node.parent;
130
+ if (!compiler.isClassDeclaration(owner))
131
+ return "";
132
+ return source.text.slice(owner.getStart(source), owner.members.pos).replace(/\s+/g, " ").trim();
133
+ }
134
+ /** Whether the file writes a property of this name anywhere (`x.name = …`, `x["name"] = …`) or names it as a string to
135
+ * a property-defining call (`Object.defineProperty(…, "name", …)`): a method so replaced isn't the body pinned. */
136
+ function memberRewritten(source, name) {
137
+ const compiler = typescript();
138
+ let hit = false;
139
+ const visit = (one) => {
140
+ if (hit)
141
+ return;
142
+ if (compiler.isBinaryExpression(one) && isAssignment(one.operatorToken.kind)) {
143
+ const left = one.left;
144
+ if ((compiler.isPropertyAccessExpression(left) && left.name.text === name)
145
+ || (compiler.isElementAccessExpression(left) && compiler.isStringLiteralLike(left.argumentExpression) && left.argumentExpression.text === name)) {
146
+ hit = true;
147
+ return;
148
+ }
149
+ }
150
+ if (compiler.isCallExpression(one) && /define(Property|Properties)$|^assign$|setPrototypeOf$/.test(calleeName(one.expression))
151
+ && one.arguments.some(argument => mentionsString(argument, name))) {
152
+ hit = true;
153
+ return;
154
+ }
155
+ compiler.forEachChild(one, visit);
156
+ };
157
+ visit(source);
158
+ return hit;
159
+ }
160
+ function calleeName(node) {
161
+ const compiler = typescript();
162
+ return compiler.isIdentifier(node) ? node.text : compiler.isPropertyAccessExpression(node) ? node.name.text : "";
163
+ }
164
+ function mentionsString(node, text) {
165
+ const compiler = typescript();
166
+ if ((compiler.isStringLiteralLike(node) && node.text === text) || (compiler.isIdentifier(node) && node.text === text))
167
+ return true;
168
+ let hit = false;
169
+ compiler.forEachChild(node, one => { if (!hit && mentionsString(one, text))
170
+ hit = true; });
171
+ return hit;
172
+ }
173
+ function isAssignment(kind) {
174
+ const compiler = typescript();
175
+ return kind >= compiler.SyntaxKind.FirstAssignment && kind <= compiler.SyntaxKind.LastAssignment;
176
+ }
177
+ /** Every place in the file that binds or rebinds `name`: declarations of any kind (import specifiers, functions,
178
+ * classes, variables, parameters, destructuring, catch variables, enums, namespaces, types) and assignments to it. */
179
+ function bindingsOf(source, name) {
180
+ const compiler = typescript();
181
+ const found = [];
182
+ const assigned = (target) => {
183
+ if (compiler.isIdentifier(target))
184
+ return target.text === name;
185
+ if (compiler.isParenthesizedExpression(target))
186
+ return assigned(target.expression);
187
+ if (compiler.isObjectLiteralExpression(target))
188
+ return target.properties.some(one => compiler.isShorthandPropertyAssignment(one) ? one.name.text === name
189
+ : compiler.isPropertyAssignment(one) ? assigned(one.initializer)
190
+ : compiler.isSpreadAssignment(one) ? assigned(one.expression) : false);
191
+ if (compiler.isArrayLiteralExpression(target))
192
+ return target.elements.some(assigned);
193
+ if (compiler.isSpreadElement(target))
194
+ return assigned(target.expression);
195
+ if (compiler.isBinaryExpression(target) && target.operatorToken.kind === compiler.SyntaxKind.EqualsToken)
196
+ return assigned(target.left);
197
+ return false;
198
+ };
199
+ const visit = (one) => {
200
+ const declared = one.name;
201
+ if (declared !== undefined && compiler.isIdentifier(declared) && declared.text === name && (compiler.isImportSpecifier(one) || compiler.isImportClause(one) || compiler.isNamespaceImport(one) || compiler.isImportEqualsDeclaration(one)
202
+ || compiler.isFunctionDeclaration(one) || compiler.isFunctionExpression(one) || compiler.isClassDeclaration(one) || compiler.isClassExpression(one)
203
+ || compiler.isVariableDeclaration(one) || compiler.isParameter(one) || compiler.isBindingElement(one)
204
+ || compiler.isEnumDeclaration(one) || compiler.isModuleDeclaration(one) || compiler.isTypeAliasDeclaration(one) || compiler.isInterfaceDeclaration(one)))
205
+ found.push(one);
206
+ if (compiler.isBinaryExpression(one) && isAssignment(one.operatorToken.kind) && assigned(one.left))
207
+ found.push(one);
208
+ if ((compiler.isPrefixUnaryExpression(one) || compiler.isPostfixUnaryExpression(one)) && compiler.isIdentifier(one.operand) && one.operand.text === name
209
+ && (one.operator === compiler.SyntaxKind.PlusPlusToken || one.operator === compiler.SyntaxKind.MinusMinusToken))
210
+ found.push(one);
211
+ if ((compiler.isForOfStatement(one) || compiler.isForInStatement(one)) && !compiler.isVariableDeclarationList(one.initializer) && assigned(one.initializer))
212
+ found.push(one);
213
+ compiler.forEachChild(one, visit);
214
+ };
215
+ visit(source);
216
+ return found;
217
+ }
218
+ /** Whether `name` is bound in this file only as `from` says: exactly one binding, and it is an un-renamed, value
219
+ * (not type-only) named import from that module; the top-level function (`self`); or a declaration inside `inside`
220
+ * (`local`). Null when it is; otherwise why not. */
221
+ function boundAs(source, name, from, inside) {
222
+ const compiler = typescript();
223
+ const all = bindingsOf(source, name);
224
+ if (all.length !== 1)
225
+ return all.length === 0 ? `${name} isn't bound` : `${name} is declared or assigned more than once`;
226
+ const only = all[0];
227
+ if (from === "self")
228
+ return compiler.isFunctionDeclaration(only) && only.parent === source ? null : `${name} isn't its own top-level function`;
229
+ if (from === "local")
230
+ return only.pos >= inside.pos && only.end <= inside.end && !compiler.isParameter(only) ? null : `${name} isn't declared inside ${inside.name?.text ?? "it"}`;
231
+ if (!compiler.isImportSpecifier(only) || only.propertyName !== undefined || only.isTypeOnly)
232
+ return `${name} isn't imported by its own name`;
233
+ const clause = only.parent.parent;
234
+ const declaration = clause.parent;
235
+ if (clause.isTypeOnly || !compiler.isStringLiteral(declaration.moduleSpecifier) || declaration.moduleSpecifier.text !== from)
236
+ return `${name} isn't imported from ${from}`;
237
+ return null;
238
+ }
239
+ /** The call expressions in `node` whose callee is the plain identifier `name`. */
240
+ function callsOf(node, name) {
241
+ const compiler = typescript();
242
+ const found = [];
243
+ const visit = (one) => {
244
+ if (compiler.isCallExpression(one) && compiler.isIdentifier(one.expression) && one.expression.text === name)
245
+ found.push(one);
246
+ compiler.forEachChild(one, visit);
247
+ };
248
+ visit(node);
249
+ return found;
250
+ }
251
+ /** A function the manifest names, held like a hook's callee: one top-level function, never rebound (a later
252
+ * `name = …` would replace the body that was checked). */
253
+ function heldFunction(source, name) {
254
+ const fn = topFunction(source, name);
255
+ return fn === null || boundAs(source, name, "self", fn) !== null ? null : fn;
256
+ }
257
+ /** A reader that parses each file once. */
258
+ function parser(read) {
259
+ const seen = new Map();
260
+ return file => { if (!seen.has(file))
261
+ seen.set(file, parse(file, read(file))); return seen.get(file); };
262
+ }
263
+ /** This hook as `tree` holds it: its calls' arguments as text (for `args`), or why it doesn't hold it. */
264
+ function hookIn(tree, hook) {
265
+ const parsed = tree(hook.file);
266
+ if ("problem" in parsed)
267
+ return { missing: `${parsed.problem}, and it holds a required call (${hook.why})` };
268
+ const fn = heldFunction(parsed.source, hook.inside);
269
+ if (fn === null || fn.body === undefined)
270
+ return { missing: `${hook.file} no longer has ${hook.inside} as one function, which must call ${hook.calls} (${hook.why})` };
271
+ const found = callsOf(fn.body, hook.calls);
272
+ if (found.length === 0)
273
+ return { missing: `${hook.file}'s ${hook.inside} no longer calls ${hook.calls} (${hook.why})` };
274
+ const bound = boundAs(parsed.source, hook.calls, hook.from, fn);
275
+ if (bound !== null)
276
+ return { missing: `${hook.file}'s ${hook.inside} no longer calls the real ${hook.calls}: ${bound} (${hook.why})` };
277
+ return { args: found.map(call => call.arguments.map(argument => argument.getText(parsed.source).replace(/\s+/g, " ")).join(", ")).sort() };
278
+ }
279
+ /** A pinned function's text in `tree`, or null when it can't be found once. */
280
+ function pinnedText(tree, pin) {
281
+ const parsed = tree(pin.file);
282
+ if ("problem" in parsed)
283
+ return null;
284
+ const fn = pin.fn.includes("#") ? classMethod(parsed.source, pin.fn) : heldFunction(parsed.source, pin.fn);
285
+ if (fn === null)
286
+ return null;
287
+ // The static block's pin holds the class header too: its name, modifiers and heritage, as the base has them.
288
+ return pin.fn.endsWith("#static") ? `${classHeader(parsed.source, fn)}\n${fn.getText(parsed.source)}` : fn.getText(parsed.source);
289
+ }
290
+ /**
291
+ * Why a candidate drops a required hook or changes a pinned body, or null. `candidate` gives a listed file's bytes at
292
+ * the candidate (null: missing or unreadable, which needs a person too). With `base` (the plane's own record of where
293
+ * the release started, which the candidate can't rewrite), only what the base holds is required: a project that isn't
294
+ * Toolroll holds none, and a hook not yet landed can't be dropped.
295
+ */
296
+ export function evalHookProblem(candidate, base, hooks = EVAL_HOOKS, pinned = EVAL_PINNED) {
297
+ const now = parser(candidate);
298
+ const was = base === undefined ? null : parser(base);
299
+ for (const hook of hooks) {
300
+ const before = was === null ? null : hookIn(was, hook);
301
+ if (before !== null && "missing" in before)
302
+ continue;
303
+ const after = hookIn(now, hook);
304
+ if ("missing" in after)
305
+ return after.missing;
306
+ if (hook.args === true && before !== null && after.args.join("\n") !== before.args.join("\n"))
307
+ return `${hook.file}'s ${hook.inside} changed what it passes to ${hook.calls} (${hook.why})`;
308
+ }
309
+ for (const pin of pinned) {
310
+ const before = was === null ? null : pinnedText(was, pin);
311
+ if (was !== null && before === null)
312
+ continue;
313
+ const after = pinnedText(now, pin);
314
+ if (after === null)
315
+ return `${pin.file} no longer has ${pin.fn} as one function (${pin.why})`;
316
+ if (before !== null && after !== before)
317
+ return `${pin.file}'s ${pin.fn} changed (${pin.why})`;
318
+ }
319
+ return null;
320
+ }
321
+ /** Every file the manifest reads, for one verified read of each tree. */
322
+ export const EVAL_HOOK_FILES = Object.freeze([...new Set([...EVAL_HOOKS.map(hook => hook.file), ...EVAL_PINNED.map(pin => pin.file)])]);
323
+ /** Names whose every new mention in a changed file needs a person: the ways to define, reflect on, wrap or climb to
324
+ * an object's class. */
325
+ const REFLECTIVE = new Set(["defineProperty", "defineProperties", "getOwnPropertyDescriptor", "getOwnPropertyDescriptors", "assign", "setPrototypeOf", "getPrototypeOf",
326
+ "__proto__", "__defineGetter__", "__defineSetter__", "Reflect", "Proxy", "constructor"]);
327
+ /** Built-ins whose members (`Object.x`, `Function.prototype.x`, ...) a changed file may never write: the store's seal
328
+ * captures Object's at load, and nothing should replace them. */
329
+ const BUILTIN_HOLDERS = new Set(["Object", "Function"]);
330
+ const STORE_NAME = /store/i;
331
+ const GLOBAL_HOLDERS = new Set(["Object", "Reflect", "globalThis", "Store"]);
332
+ /**
333
+ * The constructs that could reach a pinned Store method from another file, each as its whole normalized text (the
334
+ * owner, 2026-10-31: one blunt rule, not a chase of syntax variants; store.ts's runtime seal is the other half):
335
+ * a computed member name; a write through a computed element access; a computed element access on Object, Reflect,
336
+ * globalThis or anything named store; any mention of a reflective name (REFLECTIVE, `.constructor` included); a class
337
+ * whose `extends` isn't a plain identifier or names a store; any member, property or string named like a pinned
338
+ * method that isn't a call of it; any decorator (it runs code as a class is defined); and any write to a member of
339
+ * Object or Function (`Object.defineProperty = …`, `Function.prototype.call = …`).
340
+ */
341
+ function storeRewrites(source, pinned, ownClass) {
342
+ const compiler = typescript();
343
+ const names = new Set(pinned);
344
+ const found = [];
345
+ const say = (node) => { found.push(node.getText(source).replace(/\s+/g, " ")); };
346
+ const computed = (one) => !compiler.isStringLiteralLike(one.argumentExpression) && !compiler.isNumericLiteral(one.argumentExpression);
347
+ const writesThrough = (target) => {
348
+ if (compiler.isElementAccessExpression(target) && computed(target))
349
+ return true;
350
+ let hit = false;
351
+ if (compiler.isObjectLiteralExpression(target) || compiler.isArrayLiteralExpression(target) || compiler.isSpreadElement(target) || compiler.isSpreadAssignment(target)
352
+ || compiler.isPropertyAssignment(target) || compiler.isShorthandPropertyAssignment(target) || compiler.isParenthesizedExpression(target)
353
+ || (compiler.isBinaryExpression(target) && target.operatorToken.kind === compiler.SyntaxKind.EqualsToken)) {
354
+ compiler.forEachChild(target, one => { if (!hit && writesThrough(one))
355
+ hit = true; });
356
+ }
357
+ return hit;
358
+ };
359
+ // A member of Object or Function anywhere along the access chain (`Object.x`, `globalThis.Object.x`,
360
+ // `Function.prototype.x`, `Object["x"]`), or such a target inside a destructuring pattern.
361
+ const writesBuiltin = (target) => {
362
+ if (compiler.isPropertyAccessExpression(target) || compiler.isElementAccessExpression(target)) {
363
+ for (let holder = target.expression;; holder = holder.expression) {
364
+ while (compiler.isParenthesizedExpression(holder) || compiler.isNonNullExpression(holder) || compiler.isAsExpression(holder))
365
+ holder = holder.expression;
366
+ if (compiler.isIdentifier(holder))
367
+ return BUILTIN_HOLDERS.has(holder.text);
368
+ if (compiler.isPropertyAccessExpression(holder) && BUILTIN_HOLDERS.has(holder.name.text))
369
+ return true;
370
+ if (compiler.isElementAccessExpression(holder) && compiler.isStringLiteralLike(holder.argumentExpression) && BUILTIN_HOLDERS.has(holder.argumentExpression.text))
371
+ return true;
372
+ if (!compiler.isPropertyAccessExpression(holder) && !compiler.isElementAccessExpression(holder))
373
+ return false;
374
+ }
375
+ }
376
+ let hit = false;
377
+ if (compiler.isObjectLiteralExpression(target) || compiler.isArrayLiteralExpression(target) || compiler.isSpreadElement(target) || compiler.isSpreadAssignment(target)
378
+ || compiler.isPropertyAssignment(target) || compiler.isShorthandPropertyAssignment(target) || compiler.isParenthesizedExpression(target)
379
+ || (compiler.isBinaryExpression(target) && target.operatorToken.kind === compiler.SyntaxKind.EqualsToken)) {
380
+ compiler.forEachChild(target, one => { if (!hit && writesBuiltin(one))
381
+ hit = true; });
382
+ }
383
+ return hit;
384
+ };
385
+ const writes = (target) => writesThrough(target) || writesBuiltin(target);
386
+ const visit = (one) => {
387
+ if (compiler.isComputedPropertyName(one))
388
+ say(one.parent);
389
+ if (compiler.isDecorator(one))
390
+ say(one);
391
+ if (compiler.isBinaryExpression(one) && isAssignment(one.operatorToken.kind) && writes(one.left))
392
+ say(one);
393
+ if ((compiler.isPrefixUnaryExpression(one) || compiler.isPostfixUnaryExpression(one)) && (writesThrough(one.operand)
394
+ || ((one.operator === compiler.SyntaxKind.PlusPlusToken || one.operator === compiler.SyntaxKind.MinusMinusToken) && writesBuiltin(one.operand))))
395
+ say(one);
396
+ if (compiler.isDeleteExpression(one) && writes(one.expression))
397
+ say(one);
398
+ if ((compiler.isForInStatement(one) || compiler.isForOfStatement(one)) && writes(one.initializer))
399
+ say(one);
400
+ if (compiler.isElementAccessExpression(one) && computed(one)) {
401
+ const holder = one.expression;
402
+ const named = compiler.isIdentifier(holder) ? holder.text : compiler.isPropertyAccessExpression(holder) ? holder.name.text : "";
403
+ if (GLOBAL_HOLDERS.has(named) || STORE_NAME.test(named))
404
+ say(one);
405
+ }
406
+ if ((compiler.isIdentifier(one) || compiler.isPrivateIdentifier(one) || compiler.isStringLiteralLike(one)) && REFLECTIVE.has(one.text))
407
+ say(one);
408
+ if ((compiler.isClassDeclaration(one) || compiler.isClassExpression(one)) && one.heritageClauses !== undefined) {
409
+ for (const clause of one.heritageClauses) {
410
+ if (clause.token !== compiler.SyntaxKind.ExtendsKeyword)
411
+ continue;
412
+ for (const type of clause.types)
413
+ if (!compiler.isIdentifier(type.expression) || STORE_NAME.test(type.expression.text))
414
+ say(clause);
415
+ }
416
+ }
417
+ // Named like a pinned method: anything but calling it (`store.recordUsage(…)`) or referring to it.
418
+ if ((compiler.isIdentifier(one) || compiler.isPrivateIdentifier(one) || compiler.isStringLiteralLike(one)) && names.has(one.text)) {
419
+ const parent = one.parent;
420
+ // store.ts's own declaration of the method (its body is pinned) is the one exempt construct.
421
+ const own = ownClass !== null && compiler.isMethodDeclaration(parent) && parent.name === one && compiler.isClassDeclaration(parent.parent) && parent.parent.name?.text === ownClass
422
+ && compiler.isSourceFile(parent.parent.parent);
423
+ if (own) {
424
+ compiler.forEachChild(one, visit);
425
+ return;
426
+ }
427
+ const reference = compiler.isPropertyAccessExpression(parent) && parent.name === one
428
+ && !(compiler.isBinaryExpression(parent.parent) && isAssignment(parent.parent.operatorToken.kind) && parent.parent.left === parent)
429
+ && !((compiler.isPrefixUnaryExpression(parent.parent) || compiler.isPostfixUnaryExpression(parent.parent) || compiler.isDeleteExpression(parent.parent)));
430
+ if (!reference)
431
+ say(parent);
432
+ }
433
+ compiler.forEachChild(one, visit);
434
+ };
435
+ visit(source);
436
+ return found;
437
+ }
438
+ /**
439
+ * Why a changed file could write over a pinned Store method, or null (PR #263 security scan). Each changed JavaScript
440
+ * or TypeScript file, store.ts included (its class's own method declarations exempt; their bodies are pinned), is held to
441
+ * storeRewrites: a construct the base's copy of that file didn't already have (counted, by the sha256 of its whole
442
+ * text) needs a person. A tree or file that can't be read, or a file that doesn't parse, needs one too; `paths` are the
443
+ * files the verified diff says exist at the candidate, so no bytes is a failed read, never a deletion.
444
+ */
445
+ export function storeRewriteProblem(paths, candidate, base, pinned = PINNED_STORE_METHODS) {
446
+ const scanned = paths.filter(path => /\.(?:[cm]?[jt]sx?)$/.test(path) && !path.startsWith("node_modules/") && !path.includes("/node_modules/"));
447
+ if (scanned.length === 0)
448
+ return null;
449
+ if (candidate === null || base === null)
450
+ return "the changed files can't be read";
451
+ const key = (text) => createHash("sha256").update(text).digest("hex");
452
+ for (const path of scanned) {
453
+ const bytes = candidate(path);
454
+ if (bytes === null)
455
+ return `${path} can't be read`;
456
+ const now = parse(path, bytes);
457
+ if ("problem" in now)
458
+ return now.problem;
459
+ const before = base(path);
460
+ const was = before === null ? null : parse(path, before);
461
+ const kept = new Map();
462
+ // store.ts is scanned too (its own method declarations exempt): nothing added there may reach a pinned method.
463
+ const ownClass = path === "src/store.ts" ? "Store" : null;
464
+ if (was !== null && !("problem" in was))
465
+ for (const one of storeRewrites(was.source, pinned, ownClass))
466
+ kept.set(key(one), (kept.get(key(one)) ?? 0) + 1);
467
+ for (const one of storeRewrites(now.source, pinned, ownClass)) {
468
+ const left = kept.get(key(one)) ?? 0;
469
+ if (left > 0) {
470
+ kept.set(key(one), left - 1);
471
+ continue;
472
+ }
473
+ return `${path} adds \`${one.slice(0, 80)}\`, which could reach a pinned Store method`;
474
+ }
475
+ }
476
+ return null;
477
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The eval oracle's process (eval-oracle.ts): started by the plane's runner from the installed release, outside the
3
+ * fence, once an attempt's whole process tree is gone. It reads one request on stdin (the installed case's folder, the
4
+ * graded files' folder, the time limit), grades, and writes one verdict line on stdout. It never reads anything the
5
+ * build wrote except the named graded files, and never runs the build's code in its own process.
6
+ */
7
+ import { gradeCase } from "./eval-oracle.js";
8
+ const chunks = [];
9
+ let size = 0;
10
+ for await (const chunk of process.stdin) {
11
+ size += chunk.length;
12
+ if (size > 16 * 1024)
13
+ break;
14
+ chunks.push(chunk);
15
+ }
16
+ let verdict;
17
+ try {
18
+ const request = JSON.parse(Buffer.concat(chunks).toString("utf8"));
19
+ // What the graded program may not read beside the installed eval set (the plane's state folder): absolute paths only.
20
+ const deny = Array.isArray(request.deny) ? request.deny.filter((one) => typeof one === "string" && one.startsWith("/")).slice(0, 8) : [];
21
+ if (typeof request.caseDir !== "string" || typeof request.inputDir !== "string" || !Number.isInteger(request.timeoutMs) || request.timeoutMs < 1 || request.timeoutMs > 600_000) {
22
+ verdict = { outcome: "error", why: "the oracle was asked something it can't read" };
23
+ }
24
+ else {
25
+ verdict = await gradeCase({ caseDir: request.caseDir, inputDir: request.inputDir, timeoutMs: request.timeoutMs, deny });
26
+ }
27
+ }
28
+ catch (error) {
29
+ verdict = { outcome: "error", why: `the oracle failed: ${error instanceof Error ? error.message.slice(0, 160) : String(error)}` };
30
+ }
31
+ process.stdout.write(`${JSON.stringify(verdict)}\n`);
@@ -0,0 +1,60 @@
1
+ export type OracleVerdict = {
2
+ outcome: "pass" | "fail" | "error";
3
+ why: string | null;
4
+ };
5
+ /** A case's hidden check (`oracle.mjs`): the files it grades, inputs chosen from a seeded random source, and the
6
+ * answer it computes itself for each. */
7
+ export type CaseOracle = {
8
+ files: readonly string[];
9
+ inputs: (random: () => number) => unknown[];
10
+ expected: (input: unknown) => unknown;
11
+ };
12
+ /** The most a graded program may print, and the most of each graded file the oracle copies. */
13
+ export declare const ORACLE_OUTPUT_BYTES: number;
14
+ /** A random source seeded by the nonce (mulberry32 over its digest): the inputs are fresh for every grading. */
15
+ export declare function seededRandom(nonce: string): () => number;
16
+ /** One value in a canonical form (object keys sorted), so two answers compare by content. */
17
+ export declare function canonical(value: unknown): string;
18
+ /** How the graded program is started: inside the native fence by default; a test may only ever narrow it. */
19
+ export type GradeRunner = (input: {
20
+ cwd: string;
21
+ stdin: string;
22
+ timeoutMs: number;
23
+ deny: readonly string[];
24
+ }) => Promise<{
25
+ code: number;
26
+ stdout: string;
27
+ timedOut: boolean;
28
+ }>;
29
+ /** The default: the installed node, `probe.mjs`, inside the release check's fence — writes only the grading folder, no
30
+ * network (a proxy of its own that allows no host), the installed eval set and whatever else `deny` names unreadable. No
31
+ * fence on this machine: nothing is graded (the attempt is an error, never a pass). */
32
+ export declare const fencedGradeRunner: GradeRunner;
33
+ /** Grade one attempt, in the oracle's own process. `caseDir` must be a case of the installed eval set (`evalsRoot`). */
34
+ export declare function gradeCase(input: {
35
+ caseDir: string;
36
+ inputDir: string;
37
+ timeoutMs: number;
38
+ evalsRoot?: string;
39
+ runner?: GradeRunner;
40
+ deny?: readonly string[];
41
+ }): Promise<OracleVerdict>;
42
+ /** The oracle process's entry file, from the installed release (the sources under a test). */
43
+ export declare function oracleEntry(): {
44
+ file: string;
45
+ args: string[];
46
+ };
47
+ /**
48
+ * Grade in a separate process from the installed release (the plane's runner calls this once the attempt's process
49
+ * tree is gone). Its verdict comes back over its own stdout; anything else — it crashed, timed out, said nothing
50
+ * readable — is an error the plane recorded, never a pass.
51
+ */
52
+ export declare function runOracle(input: {
53
+ caseDir: string;
54
+ inputDir: string;
55
+ timeoutMs: number;
56
+ deny?: readonly string[];
57
+ }, entry?: {
58
+ file: string;
59
+ args: string[];
60
+ }): Promise<OracleVerdict>;