yay-layer 1.0.0-rc.1

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/prove.js ADDED
@@ -0,0 +1,876 @@
1
+ 'use strict';
2
+ // Behavioural verification: does the code actually satisfy its `ensures`?
3
+ //
4
+ // For every leaf Cell that declares `pure: yes` and an `ensures:`, we:
5
+ // 1. load the Cell's file in an isolated VM sandbox (TS types stripped, module
6
+ // syntax neutralised, unknown globals black-holed so pure code still loads),
7
+ // 2. capture the unit function by name (works for function decls, const arrows…),
8
+ // 3. generate inputs from the `in:` types (edge + ordinary values, deterministic),
9
+ // 4. call the real function and evaluate the `ensures` expression against `out`.
10
+ //
11
+ // A single counterexample ⇒ FAIL (Red): the code contradicts its promise. If we
12
+ // genuinely cannot check it (prose ensures, exotic types, code won't load) we say
13
+ // so and leave it UNPROVEN (Yellow / info) — never a fake pass. Test inputs come
14
+ // from the SPEC, never the implementation, so passing means something.
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const vm = require('node:vm');
19
+ const cp = require('child_process');
20
+ const { mutants, deletions, harvestLiterals } = require('./mutate');
21
+ const { makeRecorder } = require('./record');
22
+ const { instrument: instrumentBranches } = require('./coverage');
23
+ const { analyzePredicates } = require('./predicate');
24
+ const { isJsLang, looseTopLevelNonJs, normLangName } = require('./util');
25
+
26
+ function stripTS(code) {
27
+ let m; try { m = require('node:module'); } catch (_) { return code; }
28
+ if (typeof m.stripTypeScriptTypes !== 'function') return code;
29
+ const prev = process.emitWarning; process.emitWarning = () => {};
30
+ try { return m.stripTypeScriptTypes(code, { mode: 'strip' }); }
31
+ catch (_) { return null; }
32
+ finally { process.emitWarning = prev; }
33
+ }
34
+
35
+ // Drop imports and unwrap `export` so the file runs as a plain script in the VM.
36
+ function neutralizeModules(code) {
37
+ return code.split('\n').map((l) => {
38
+ if (/^\s*import\b/.test(l)) return '';
39
+ if (/^\s*export\s+(\{|\*)/.test(l)) return '';
40
+ return l.replace(/^(\s*)export\s+default\s+/, '$1').replace(/^(\s*)export\s+/, '$1');
41
+ }).join('\n');
42
+ }
43
+
44
+ // A permissive stand-in for globals a pure function shouldn't need but a file might
45
+ // touch at load time (document/window/…): every access yields another black hole.
46
+ function blackHole() {
47
+ const bh = new Proxy(function () {}, {
48
+ get: (_t, k) => (k === Symbol.toPrimitive ? () => '' : bh),
49
+ apply: () => bh, construct: () => bh, has: () => true,
50
+ });
51
+ return bh;
52
+ }
53
+
54
+ // A recording "hyperscript": JSX compiles to __h(type, props, ...children) (pragma
55
+ // __h), and this builds a plain { type, props, children } vnode tree — no real React,
56
+ // no DOM — that a component Cell's `ensures` can be checked against. Child components
57
+ // appear as { type:'<Name>' } nodes (shallow render); primitives/strings are leaves.
58
+ function hyperscript(type, props) {
59
+ const kids = [];
60
+ for (let i = 2; i < arguments.length; i++) kids.push(arguments[i]);
61
+ const flat = [];
62
+ (function fl(a) { for (let i = 0; i < a.length; i++) { const c = a[i]; if (Array.isArray(c)) fl(c); else if (c != null && c !== false && c !== true) flat.push(c); } })(kids);
63
+ const t = typeof type === 'function' ? (type.displayName || type.name || 'Component') : (type == null ? '#fragment' : type);
64
+ return { type: t, props: props || {}, children: flat };
65
+ }
66
+ // Deterministic hook shims so a component renders ONCE for given props without a real
67
+ // React runtime. This exercises the initial render (what render-proving checks); it does
68
+ // not drive state transitions, effects, or events (an honest v1 boundary).
69
+ function reactShim() {
70
+ const noop = () => {};
71
+ const hooks = {
72
+ useState: (i) => [typeof i === 'function' ? i() : i, noop],
73
+ useReducer: (_r, i) => [i, noop],
74
+ useRef: (i) => ({ current: i === undefined ? null : i }),
75
+ useMemo: (f) => (typeof f === 'function' ? f() : undefined),
76
+ useCallback: (f) => f,
77
+ useEffect: noop, useLayoutEffect: noop, useContext: () => undefined,
78
+ };
79
+ return { hooks, React: Object.assign({ createElement: hyperscript, Fragment: '#fragment' }, hooks) };
80
+ }
81
+
82
+ function makeSandbox(names, registry, extras) {
83
+ const rs = reactShim();
84
+ const real = {
85
+ Math, JSON, String, Number, Boolean, Array, Object, RegExp, Date, Symbol,
86
+ Error, TypeError, RangeError, // so probes/units can throw with a readable message
87
+ isNaN, isFinite, parseInt, parseFloat, encodeURIComponent, decodeURIComponent,
88
+ NaN, Infinity, undefined,
89
+ console: { log() {}, warn() {}, error() {}, info() {}, debug() {} },
90
+ __ylreg: (obj) => { Object.assign(registry, obj); },
91
+ __mkrec: makeRecorder, // effect recorder factory (for `records:` Cells)
92
+ __h: hyperscript, __Fragment: '#fragment', // JSX pragma targets (for `renders:` Cells)
93
+ React: rs.React, ...rs.hooks, // React.createElement/hooks AND bare hooks (imports are stripped)
94
+ ...(extras || {}), // e.g. __ylcov (a real branch-coverage array the instrumented probes increment)
95
+ };
96
+ const sandbox = new Proxy(real, {
97
+ has: () => true, // tell the VM every identifier is "global" → no ReferenceError
98
+ get: (t, k) => (k in t ? t[k] : blackHole()),
99
+ });
100
+ return vm.createContext(sandbox);
101
+ }
102
+
103
+ // Compile JSX (and, for .tsx, TS types) to plain JS via Babel, with the JSX pragma
104
+ // pointed at our __h/__Fragment. Babel is an OPTIONAL dependency, required lazily and
105
+ // only on the JSX path — if it's absent we return a distinct error so the Cell skips
106
+ // with an honest "install …" note rather than a fake pass. @babel/parser is already a
107
+ // dependency, so this stays in-family and battle-tested on JSX's edge cases.
108
+ function transformJSX(source, file) {
109
+ // Require the plugins as MODULES (resolved relative to this file → yay-layer's own
110
+ // node_modules), never as string names — Babel resolves plugin strings from the cwd,
111
+ // which is the USER's project when `yay verify` runs, where these deps don't live.
112
+ let babel, jsxPlugin, tsPlugin;
113
+ try {
114
+ babel = require('@babel/core');
115
+ jsxPlugin = require('@babel/plugin-transform-react-jsx'); jsxPlugin = jsxPlugin.default || jsxPlugin;
116
+ } catch (_) { return { error: 'jsx-needs-babel' }; }
117
+ const isTS = /\.tsx?$/.test(file || '');
118
+ const isTSX = /\.tsx$/.test(file || '');
119
+ const plugins = [];
120
+ if (isTS) {
121
+ try { tsPlugin = require('@babel/plugin-transform-typescript'); tsPlugin = tsPlugin.default || tsPlugin; }
122
+ catch (_) { return { error: 'jsx-needs-babel' }; }
123
+ plugins.push([tsPlugin, { isTSX, allowDeclareFields: true }]);
124
+ }
125
+ plugins.push([jsxPlugin, { runtime: 'classic', pragma: '__h', pragmaFrag: '__Fragment' }]);
126
+ try {
127
+ const out = babel.transformSync(source, { filename: file || 'component.jsx', babelrc: false, configFile: false, compact: false, plugins });
128
+ return { code: out && out.code != null ? out.code : source };
129
+ } catch (e) {
130
+ return { error: 'jsx-transform: ' + ((e && e.message) || 'failed').split('\n')[0] };
131
+ }
132
+ }
133
+
134
+ // Run a source string in a fresh sandbox; return { fns, ctx } capturing `names`.
135
+ // opts.jsx compiles JSX/TSX first (Babel); otherwise TS types are stripped as before.
136
+ function runSource(source, names, opts) {
137
+ opts = opts || {};
138
+ let stripped;
139
+ if (opts.jsx) {
140
+ const t = transformJSX(source, opts.file);
141
+ if (t.error) return { error: t.error };
142
+ stripped = t.code;
143
+ } else {
144
+ stripped = stripTS(source);
145
+ if (stripped == null) return { error: 'typescript-strip-failed' };
146
+ }
147
+ const code = neutralizeModules(stripped);
148
+ const registry = {};
149
+ const ctx = makeSandbox(names, registry);
150
+ const cap = '\n;try{__ylreg({' + names.map((n) => JSON.stringify(n) + ':(typeof ' + safeIdent(n) + "!=='undefined'?" + safeIdent(n) + ':undefined)').join(',') + '});}catch(e){}';
151
+ try { vm.runInContext(code + cap, ctx, { timeout: 2000 }); }
152
+ catch (e) { return { error: 'load: ' + (e && e.message ? e.message.split('\n')[0] : 'threw') }; }
153
+ return { fns: registry, ctx };
154
+ }
155
+
156
+ // Only plain identifiers can be captured as bare names (skip Class.method units).
157
+ function safeIdent(n) { return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(n) ? n : '__nope_' + Math.abs(hash(n)); }
158
+ function hash(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; return h; }
159
+
160
+ // ── inputs ────────────────────────────────────────────────────────────────
161
+ // Split on TOP-LEVEL commas only, so object/array/generic param types keep their
162
+ // internal commas (`paddle: {x,y,w,h}, step: number` → two params, not six).
163
+ function splitTopLevel(s) {
164
+ const out = []; let depth = 0, cur = '';
165
+ for (const ch of s) {
166
+ if (ch === '{' || ch === '[' || ch === '(' || ch === '<') depth++;
167
+ else if (ch === '}' || ch === ']' || ch === ')' || ch === '>') depth = Math.max(0, depth - 1);
168
+ if (ch === ',' && depth === 0) { out.push(cur); cur = ''; } else cur += ch;
169
+ }
170
+ if (cur.trim()) out.push(cur);
171
+ return out;
172
+ }
173
+ function parseIn(spec) {
174
+ let raw = (spec && spec.in ? String(spec.in) : '').trim();
175
+ raw = raw.replace(/^\(([\s\S]*)\)$/, '$1').trim(); // tolerate `(a:number, b:number)` wrapping
176
+ if (!raw || /^todo$/i.test(raw)) return [];
177
+ return splitTopLevel(raw).map((part) => {
178
+ const i = part.indexOf(':'); // split on the FIRST colon; object types keep theirs
179
+ const name = (i < 0 ? part : part.slice(0, i)).trim();
180
+ const type = (i < 0 ? 'any' : part.slice(i + 1)).trim().toLowerCase().replace(/\s+/g, '');
181
+ return { name, type };
182
+ }).filter((p) => p.name);
183
+ }
184
+ // Same as parseIn but PRESERVES case in the type — needed for component props, whose
185
+ // names are case-sensitive (onClick, className). valuesFor lowercases for primitive
186
+ // lookups internally, so keeping case here only affects object field names.
187
+ function parseInCased(spec) {
188
+ let raw = (spec && spec.in ? String(spec.in) : '').trim();
189
+ raw = raw.replace(/^\(([\s\S]*)\)$/, '$1').trim();
190
+ if (!raw || /^todo$/i.test(raw)) return [];
191
+ return splitTopLevel(raw).map((part) => {
192
+ const i = part.indexOf(':');
193
+ const name = (i < 0 ? part : part.slice(0, i)).trim();
194
+ const type = (i < 0 ? 'any' : part.slice(i + 1)).trim().replace(/\s+/g, '');
195
+ return { name, type };
196
+ }).filter((p) => p.name);
197
+ }
198
+ const POOLS = {
199
+ number: [0, 1, 2, -1, 3, 7, 10, -5, 0.5, 100],
200
+ string: ['', 'a', 'ab', 'abc', 'Hello', '/x', '123', 'a b'],
201
+ boolean: [true, false],
202
+ };
203
+ // Parse an object-shape type `{title:string, featured:boolean}` into fields, PRESERVING
204
+ // field-name case (prop names are case-sensitive). Returns null if it isn't a shape.
205
+ function objectShape(type) {
206
+ const m = String(type).match(/^\{([\s\S]*)\}$/);
207
+ if (!m) return null;
208
+ const fields = splitTopLevel(m[1]).map((f) => { const i = f.indexOf(':'); if (i < 0) return null; return { name: f.slice(0, i).trim(), type: f.slice(i + 1).trim() }; }).filter(Boolean);
209
+ return fields.length ? fields : null;
210
+ }
211
+ function valuesFor(type) {
212
+ const lt = String(type).toLowerCase();
213
+ if (POOLS[lt]) return POOLS[lt];
214
+ const arr = lt.match(/^(number|string|boolean)\[\]$/);
215
+ if (arr) { const b = POOLS[arr[1]]; return [[], [b[1]], [b[1], b[2]], b.slice(0, 3)]; }
216
+ const shape = objectShape(type); // object props → generate objects (cased field names)
217
+ if (shape) {
218
+ const fv = shape.map((f) => valuesFor(f.type));
219
+ if (fv.some((v) => !v)) return null;
220
+ const combos = cartesian(fv, 12);
221
+ return combos.map((tuple) => { const o = {}; shape.forEach((f, i) => { o[f.name] = tuple[i]; }); return o; });
222
+ }
223
+ if (/\[\]$/.test(String(type))) { // any-element array (e.g. object[]) → a few sample arrays
224
+ const base = valuesFor(String(type).replace(/\[\]$/, ''));
225
+ if (!base || !base.length) return null;
226
+ return [[], [base[Math.min(1, base.length - 1)]], base.slice(0, 3)];
227
+ }
228
+ return null; // uncheckable type
229
+ }
230
+ function cartesian(lists, cap) {
231
+ let out = [[]];
232
+ for (const list of lists) {
233
+ const next = [];
234
+ for (const tuple of out) for (const v of list) { next.push(tuple.concat([v])); if (next.length > cap * 4) break; }
235
+ out = next;
236
+ }
237
+ if (out.length > cap) { // deterministic thinning
238
+ const step = out.length / cap; const picked = [];
239
+ for (let i = 0; i < out.length; i += step) picked.push(out[Math.floor(i)]);
240
+ out = picked;
241
+ }
242
+ return out;
243
+ }
244
+
245
+ // ── the ensures expression ──────────────────────────────────────────────────
246
+ // SAFE, unambiguous normalizations only (never anything that could change meaning
247
+ // and mint a false pass): `;` clause-separator → conjunction, and |simple| → Math.abs.
248
+ function normalizeEnsures(ensuresExpr) {
249
+ return String(ensuresExpr || '')
250
+ .replace(/;/g, ' && ')
251
+ .replace(/\|\s*([A-Za-z_$][A-Za-z0-9_$.]*)\s*\|/g, 'Math.abs($1)') // |dx| → Math.abs(dx)
252
+ .replace(/(?:&&\s*)+$/, '')
253
+ .trim() || 'true';
254
+ }
255
+ // A helpful, honest reason when an `ensures` can't be machine-checked — tells the
256
+ // author exactly how to make it checkable, instead of a bare "prose?".
257
+ function ensuresHint(ensuresExpr) {
258
+ const e = String(ensuresExpr || '');
259
+ if (/\b(iff|⇔|<=>)\b/i.test(e)) return 'uses "iff" — write it as JS: `A === B` (boolean equality)';
260
+ if (/(=>|⇒|\bimplies\b)/i.test(e)) return 'uses implication — write it as JS: `(!A || B)`';
261
+ if (/\b(for ?all|every|each|∀)\b/i.test(e)) return 'uses "for all/every" — write it as JS: `arr.every(x => …)`';
262
+ if (/\b(unchanged|not ?mutated|immutab)\b/i.test(e)) return 'talks about mutation — compare a copy: `JSON.stringify(out) === JSON.stringify(fn(...))`';
263
+ if (/\bclamp\b/i.test(e)) return 'uses clamp() — inline it: `Math.max(lo, Math.min(x, hi))`';
264
+ if (!/[<>=!]=?|===|!==|&&|\|\||\.every|\.some|\.includes/.test(e)) return 'reads as prose — write a boolean JS expression over `out` and the inputs';
265
+ return 'not valid JavaScript — write `ensures:` as a boolean expression over `out` and the inputs';
266
+ }
267
+ function buildChecker(ctx, params, ensuresExpr) {
268
+ const expr = normalizeEnsures(ensuresExpr);
269
+ const decl = params.map((p, i) => `var ${p}=A[${i}];`).join(' ');
270
+ const call = `fn(${params.map((_, i) => `A[${i}]`).join(',')})`;
271
+ const src = `(function(fn,A){ ${decl} var out=${call}; return {out:out, ok:!!(${expr})}; })`;
272
+ return vm.runInContext(src, ctx, { timeout: 2000 });
273
+ }
274
+ // Effect-aware checker: instrument the `records:` param with a recorder, run the
275
+ // (side-effecting) function, and evaluate `ensures` against the recorded trace.
276
+ // The ensures may use: `trace` (raw), `calls(name)` → arg-arrays, `sets(name)` →
277
+ // assigned values, `didCall(name)`, `didSet(name, value)` — plus `out` and the args.
278
+ function buildEffectChecker(ctx, params, recordsName, ensuresExpr) {
279
+ const idx = params.indexOf(recordsName);
280
+ if (idx < 0) throw new Error('records: names "' + recordsName + '", which is not a parameter in in:');
281
+ const expr = normalizeEnsures(ensuresExpr);
282
+ const decl = params.map((p, i) => `var ${p}=A[${i}];`).join(' ');
283
+ const call = `fn(${params.map((_, i) => `A[${i}]`).join(',')})`;
284
+ const helpers = 'function calls(n){return trace.filter(function(e){return e.type==="call"&&e.name===n;}).map(function(e){return e.args;});}'
285
+ + 'function sets(n){return trace.filter(function(e){return e.type==="set"&&e.name===n;}).map(function(e){return e.value;});}'
286
+ + 'function didCall(n){return calls(n).length>0;}function didSet(n,v){return sets(n).indexOf(v)>=0;}';
287
+ const src = `(function(fn,A){ var __r=__mkrec(); A[${idx}]=__r.proxy; ${decl} var trace=__r.trace; ${helpers} var out=${call}; return {out:out, trace:trace, ok:!!(${expr})}; })`;
288
+ return vm.runInContext(src, ctx, { timeout: 2000 });
289
+ }
290
+ // Render checker (for `renders:` Cells): run the component with generated props, take
291
+ // the returned vnode tree as `out`, and evaluate `ensures` with tree helpers in scope:
292
+ // text(n) · find(n,type) · findAll(n,type) · has(n,type) · count(n,type)
293
+ // attr(n,name) · hasClass(n,class) · kids(n)
294
+ // (Mirrors buildEffectChecker: a checkable surface + helpers instead of a bare return.)
295
+ function buildRenderChecker(ctx, params, ensuresExpr) {
296
+ const expr = normalizeEnsures(ensuresExpr);
297
+ const decl = params.map((p, i) => `var ${p}=A[${i}];`).join(' ');
298
+ const call = `fn(${params.map((_, i) => `A[${i}]`).join(',')})`;
299
+ const helpers = ''
300
+ + 'function __nodes(n){var acc=[];(function w(x){if(x&&typeof x==="object"&&x.type!==undefined){acc.push(x);(x.children||[]).forEach(w);}})(n);return acc;}'
301
+ + 'function find(n,t){var a=__nodes(n);for(var i=0;i<a.length;i++)if(a[i].type===t)return a[i];return null;}'
302
+ + 'function findAll(n,t){return __nodes(n).filter(function(x){return x.type===t;});}'
303
+ + 'function has(n,t){return findAll(n,t).length>0;}'
304
+ + 'function count(n,t){return findAll(n,t).length;}'
305
+ + 'function attr(n,name){return n&&n.props?n.props[name]:undefined;}'
306
+ + 'function kids(n){return n&&n.children?n.children:[];}'
307
+ + 'function hasClass(n,c){var cn=n&&n.props?String(n.props.className||""):"";return cn.split(/\\s+/).indexOf(c)>=0;}'
308
+ + 'function text(n){var s="";(function w(x){if(x==null||x===false||x===true)return;if(typeof x==="object"&&x.type!==undefined){(x.children||[]).forEach(w);}else{s+=String(x);}})(n);return s;}';
309
+ const src = `(function(fn,A){ ${decl} var out=${call}; ${helpers} return {out:out, ok:!!(${expr})}; })`;
310
+ return vm.runInContext(src, ctx, { timeout: 2000 });
311
+ }
312
+ function show(v) { try { return typeof v === 'string' ? JSON.stringify(v) : JSON.stringify(v) ?? String(v); } catch (_) { return String(v); } }
313
+
314
+ // ── proving adapters ────────────────────────────────────────────────────────
315
+ // Each prover mode is an adapter: it says which Cells it handles (canHandle),
316
+ // how to parse the spec's `in:` (inputs), how to build a checker that exposes a
317
+ // checkable surface + evaluates `ensures` (checker), and how to phrase a
318
+ // counterexample (describe). The core driver owns dispatch, the case-loop, and
319
+ // mutation — so a new framework/language is a new adapter, not a core edit.
320
+ // (`load` is uniform here because all current adapters run in the same JS VM; an
321
+ // out-of-VM adapter, e.g. Python, will carry its own load — see docs/design.)
322
+ function jsLoad(source, names, opts) { return runSource(source, names, { jsx: !!(opts && opts.jsx), file: opts && opts.file }); }
323
+
324
+ const pureCallAdapter = {
325
+ name: 'pure-call', inVM: true, wantsJSX: false,
326
+ canHandle: (cell) => /^yes\b/i.test((cell.spec && cell.spec.pure) || ''),
327
+ load: jsLoad,
328
+ inputs: (cell) => parseIn(cell.spec),
329
+ inputNoun: 'inputs',
330
+ threwVerb: 'on generated inputs',
331
+ checker: (ctx, params, ensures) => buildChecker(ctx, params, ensures),
332
+ describe: (cell, argstr, r, ensures) => `${cell.unitName}(${argstr || ''}) → ${show(r.out)} — violates ensures: ${ensures}`,
333
+ };
334
+
335
+ const renderAdapter = {
336
+ name: 'render', inVM: true, wantsJSX: true,
337
+ canHandle: (cell) => /^yes\b/i.test((cell.spec && cell.spec.renders) || ''),
338
+ load: jsLoad,
339
+ inputs: (cell) => parseInCased(cell.spec), // case-preserved props (onClick/className)
340
+ inputNoun: 'props',
341
+ threwVerb: 'while rendering',
342
+ checker: (ctx, params, ensures) => buildRenderChecker(ctx, params, ensures),
343
+ describe: (cell, argstr, r, ensures) => {
344
+ const rootT = r.out && r.out.type ? '<' + r.out.type + '>' : show(r.out);
345
+ return `${cell.unitName}(${argstr || ''}) → renders ${rootT} — violates ensures: ${ensures}`;
346
+ },
347
+ };
348
+
349
+ // ── Python adapter (out-of-VM) — the first non-JS prover, proving the interface
350
+ // generalises. It runs a Cell's Python function in a subprocess over spec-generated
351
+ // inputs and evaluates the (Python) `ensures` against each. Purely additive: it only
352
+ // claims `lang: python` Cells, which the JS-VM adapters already skip — so no JS/TS/JSX
353
+ // behaviour can change. Python is a lazy/optional runtime dep: absent → honest skip.
354
+ function isPythonLang(l) { return /^(py|python)$/i.test(String(l || '')); }
355
+ let PY_BIN;
356
+ function detectPython() {
357
+ if (PY_BIN !== undefined) return PY_BIN;
358
+ for (const bin of ['python3', 'python']) {
359
+ try { cp.execFileSync(bin, ['-c', 'import sys,json'], { stdio: 'ignore', timeout: 4000 }); PY_BIN = bin; return bin; } catch (_) {}
360
+ }
361
+ PY_BIN = null; return PY_BIN;
362
+ }
363
+ // Harness: read {source, unit, params, inputs, ensures} as JSON on stdin; exec the
364
+ // file, call the unit for each input tuple, and eval the ensures with a SAFE builtin
365
+ // subset (never a fake pass — a raised exception skips that tuple; all-throw ⇒ skip).
366
+ const PY_HARNESS = [
367
+ 'import sys, json',
368
+ 'd = json.loads(sys.stdin.read())',
369
+ 'ns = {}',
370
+ 'try:',
371
+ " exec(d['source'], ns)",
372
+ 'except Exception as e:',
373
+ " print(json.dumps({'error': 'load: ' + str(e)})); sys.exit(0)",
374
+ "fn = ns.get(d['unit'])",
375
+ 'if not callable(fn):',
376
+ " print(json.dumps({'error': 'unit not callable in isolation'})); sys.exit(0)",
377
+ "SAFE = {'len':len,'abs':abs,'all':all,'any':any,'min':min,'max':max,'sum':sum,'sorted':sorted,'range':range,'str':str,'int':int,'float':float,'bool':bool,'round':round,'list':list,'dict':dict,'set':set,'tuple':tuple,'enumerate':enumerate,'zip':zip}",
378
+ 'def ser(v):',
379
+ ' try:',
380
+ ' json.dumps(v); return v',
381
+ ' except Exception:',
382
+ ' return str(v)',
383
+ 'threw = 0; total = 0',
384
+ "for args in d['inputs']:",
385
+ ' total += 1',
386
+ ' try:',
387
+ ' out = fn(*args)',
388
+ " scope = dict(zip(d['params'], args)); scope['out'] = out",
389
+ " ok = bool(eval(d['ensures'], {'__builtins__': {}}, dict(SAFE, **scope)))",
390
+ ' except Exception as e:',
391
+ ' threw += 1; continue',
392
+ ' if not ok:',
393
+ " argstr = ', '.join('%s=%r' % (p, a) for p, a in zip(d['params'], args))",
394
+ " print(json.dumps({'fail': {'argstr': argstr, 'out': ser(out)}})); sys.exit(0)",
395
+ 'if total and threw >= total:',
396
+ " print(json.dumps({'error': 'threw on all generated inputs'})); sys.exit(0)",
397
+ "print(json.dumps({'pass': True, 'cases': total - threw}))",
398
+ ].join('\n');
399
+
400
+ function runPythonBatch(pyBin, source, unit, paramNames, inputs, ensures) {
401
+ const payload = JSON.stringify({ source, unit, params: paramNames, inputs, ensures });
402
+ let out;
403
+ try { out = cp.execFileSync(pyBin, ['-c', PY_HARNESS], { input: payload, timeout: 8000, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 }); }
404
+ catch (e) { return { error: (e && e.message ? String(e.message).split('\n')[0] : 'python failed') }; }
405
+ const line = String(out).trim().split('\n').filter(Boolean).pop() || '';
406
+ try { return JSON.parse(line); } catch (_) { return { error: 'unreadable harness output' }; }
407
+ }
408
+
409
+ const pythonAdapter = {
410
+ name: 'python', inVM: false, wantsJSX: false,
411
+ canHandle: (cell) => isPythonLang(cell.lang || (cell.spec && cell.spec.lang)) && /^yes\b/i.test((cell.spec && cell.spec.pure) || ''),
412
+ load: (source, names) => {
413
+ const pyBin = detectPython();
414
+ if (!pyBin) return { error: 'python-missing' };
415
+ // The harness exec()s the whole module to reach the function — with REAL builtins,
416
+ // no VM sandbox — so top-level imperative code (os.system(…), a bare call) would
417
+ // actually RUN during verify. Refuse to prove such a file: it's flagged Pink by the
418
+ // manifest and gate-blocked anyway; we must not execute it to get there.
419
+ if (looseTopLevelNonJs(String(source).split(/\r?\n/), 'python').length) {
420
+ return { error: 'python-toplevel' };
421
+ }
422
+ const fns = {}; for (const n of names) fns[n] = function () {}; // placeholder — real run is out-of-process in prove()
423
+ return { fns, ctx: { pyBin, source } };
424
+ },
425
+ // Out-of-VM proving in ONE subprocess per Cell; same verdict shape as runCases.
426
+ prove: (cell, base) => {
427
+ const params = parseIn(cell.spec);
428
+ if (params.some((p) => !valuesFor(p.type))) return { status: 'skip', level: 'yellow', reason: 'inputs include a type the prover can\'t generate (' + params.map((p) => p.type).join(', ') + ')' };
429
+ const ensures = String(cell.spec.ensures || '').trim();
430
+ const tuples = cartesian(params.map((p) => valuesFor(p.type)), 40);
431
+ const inputs = tuples.length ? tuples : [[]];
432
+ const r = runPythonBatch(base.ctx.pyBin, base.ctx.source, cell.unitName, params.map((p) => p.name), inputs, ensures);
433
+ if (r.error) return { status: 'skip', level: 'yellow', reason: 'python: ' + r.error };
434
+ if (r.fail) return { status: 'fail', level: 'red', cases: inputs.length, counterexample: `${cell.unitName}(${r.fail.argstr}) → ${show(r.fail.out)} — violates ensures: ${ensures}` };
435
+ return { status: 'pass', level: 'info', cases: (typeof r.cases === 'number' ? r.cases : inputs.length) };
436
+ },
437
+ };
438
+
439
+ // ── Ruby adapter (out-of-VM) — same shape as Python: proves a pure Ruby method against a Ruby
440
+ // `ensures` in a `ruby` subprocess over spec-generated inputs. Optional runtime: absent → skip.
441
+ function isRubyLang(l) { return normLangName(l) === 'ruby'; }
442
+ let RB_BIN;
443
+ function detectRuby() {
444
+ if (RB_BIN !== undefined) return RB_BIN;
445
+ try { cp.execFileSync('ruby', ['-e', 'require "json"'], { stdio: 'ignore', timeout: 4000 }); RB_BIN = 'ruby'; return RB_BIN; } catch (_) {}
446
+ RB_BIN = null; return RB_BIN;
447
+ }
448
+ const RB_HARNESS = [
449
+ 'require "json"',
450
+ 'd = JSON.parse(STDIN.read)',
451
+ 'b = binding',
452
+ 'begin',
453
+ ' eval(d["source"], b)',
454
+ 'rescue Exception => e',
455
+ ' puts JSON.generate({"error" => "load: " + e.message}); exit 0',
456
+ 'end',
457
+ 'fn = (b.eval("method(:" + d["unit"] + ")") rescue nil)',
458
+ 'if fn.nil?',
459
+ ' puts JSON.generate({"error" => "unit not callable in isolation"}); exit 0',
460
+ 'end',
461
+ 'threw = 0; total = 0',
462
+ 'd["inputs"].each do |args|',
463
+ ' total += 1',
464
+ ' begin',
465
+ ' out = fn.call(*args)',
466
+ ' eb = binding',
467
+ ' d["params"].each_with_index { |p, i| eb.local_variable_set(p.to_sym, args[i]) }',
468
+ ' eb.local_variable_set(:out, out)',
469
+ ' ok = eb.eval(d["ensures"]) ? true : false',
470
+ ' rescue Exception => e',
471
+ ' threw += 1; next',
472
+ ' end',
473
+ ' unless ok',
474
+ ' argstr = d["params"].each_with_index.map { |p, i| "#{p}=#{args[i].inspect}" }.join(", ")',
475
+ ' o = ((JSON.generate(out) rescue nil) ? out : out.inspect)',
476
+ ' puts JSON.generate({"fail" => {"argstr" => argstr, "out" => o}}); exit 0',
477
+ ' end',
478
+ 'end',
479
+ 'if total > 0 && threw >= total',
480
+ ' puts JSON.generate({"error" => "threw on all generated inputs"}); exit 0',
481
+ 'end',
482
+ 'puts JSON.generate({"pass" => true, "cases" => total - threw})',
483
+ ].join('\n');
484
+ function runRubyBatch(bin, source, unit, paramNames, inputs, ensures) {
485
+ const payload = JSON.stringify({ source, unit, params: paramNames, inputs, ensures });
486
+ let out;
487
+ try { out = cp.execFileSync(bin, ['-e', RB_HARNESS], { input: payload, timeout: 8000, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 }); }
488
+ catch (e) { return { error: (e && e.message ? String(e.message).split('\n')[0] : 'ruby failed') }; }
489
+ const line = String(out).trim().split('\n').filter(Boolean).pop() || '';
490
+ try { return JSON.parse(line); } catch (_) { return { error: 'unreadable harness output' }; }
491
+ }
492
+
493
+ // ── PHP adapter (out-of-VM) — same shape. Proves a pure PHP function against a PHP `ensures`.
494
+ function isPhpLang(l) { return normLangName(l) === 'php'; }
495
+ let PHP_BIN;
496
+ function detectPhp() {
497
+ if (PHP_BIN !== undefined) return PHP_BIN;
498
+ try { cp.execFileSync('php', ['-r', 'echo json_encode(1);'], { stdio: 'ignore', timeout: 4000 }); PHP_BIN = 'php'; return PHP_BIN; } catch (_) {}
499
+ PHP_BIN = null; return PHP_BIN;
500
+ }
501
+ const PHP_HARNESS = [
502
+ '$d = json_decode(stream_get_contents(STDIN), true);',
503
+ '$unit = $d["unit"]; $params = $d["params"]; $ens = $d["ensures"];',
504
+ 'try { eval("?>" . $d["source"]); } catch (\\Throwable $e) { echo json_encode(["error" => "load: " . $e->getMessage()]); exit(0); }',
505
+ 'if (!function_exists($unit)) { echo json_encode(["error" => "unit not defined in isolation"]); exit(0); }',
506
+ '$threw = 0; $total = 0;',
507
+ 'foreach ($d["inputs"] as $args) {',
508
+ ' $total++;',
509
+ ' try {',
510
+ ' $out = call_user_func_array($unit, $args);',
511
+ ' $scope = ["out" => $out]; foreach ($params as $i => $p) { $scope[$p] = $args[$i]; }',
512
+ ' extract($scope);',
513
+ ' $ok = (bool) eval("return (" . $ens . ");");',
514
+ ' } catch (\\Throwable $e) { $threw++; continue; }',
515
+ ' if (!$ok) {',
516
+ ' $parts = []; foreach ($params as $i => $p) { $parts[] = "$p=" . var_export($args[$i], true); }',
517
+ ' echo json_encode(["fail" => ["argstr" => implode(", ", $parts), "out" => $out]]); exit(0);',
518
+ ' }',
519
+ '}',
520
+ 'if ($total > 0 && $threw >= $total) { echo json_encode(["error" => "threw on all generated inputs"]); exit(0); }',
521
+ 'echo json_encode(["pass" => true, "cases" => $total - $threw]);',
522
+ ].join('\n');
523
+ function runPhpBatch(bin, source, unit, paramNames, inputs, ensures) {
524
+ const payload = JSON.stringify({ source, unit, params: paramNames, inputs, ensures });
525
+ let out;
526
+ try { out = cp.execFileSync(bin, ['-r', PHP_HARNESS], { input: payload, timeout: 8000, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 }); }
527
+ catch (e) { return { error: (e && e.message ? String(e.message).split('\n')[0] : 'php failed') }; }
528
+ const line = String(out).trim().split('\n').filter(Boolean).pop() || '';
529
+ try { return JSON.parse(line); } catch (_) { return { error: 'unreadable harness output' }; }
530
+ }
531
+
532
+ // Shared prove() body for the subprocess (Ruby/PHP) adapters — mirrors the Python one.
533
+ function proveSubprocess(runBatch, langLabel, cell, base) {
534
+ const params = parseIn(cell.spec);
535
+ if (params.some((p) => !valuesFor(p.type))) return { status: 'skip', level: 'yellow', reason: 'inputs include a type the prover can\'t generate (' + params.map((p) => p.type).join(', ') + ')' };
536
+ const ensures = String(cell.spec.ensures || '').trim();
537
+ if (!ensures) return { status: 'skip', level: 'yellow', reason: 'no `ensures:` to machine-check' };
538
+ const tuples = cartesian(params.map((p) => valuesFor(p.type)), 40);
539
+ const inputs = tuples.length ? tuples : [[]];
540
+ const r = runBatch(base.ctx.bin, base.ctx.source, cell.unitName, params.map((p) => p.name), inputs, ensures);
541
+ if (r.error) return { status: 'skip', level: 'yellow', reason: langLabel + ': ' + r.error };
542
+ if (r.fail) return { status: 'fail', level: 'red', cases: inputs.length, counterexample: `${cell.unitName}(${r.fail.argstr}) → ${show(r.fail.out)} — violates ensures: ${ensures}` };
543
+ return { status: 'pass', level: 'info', cases: (typeof r.cases === 'number' ? r.cases : inputs.length) };
544
+ }
545
+
546
+ const rubyAdapter = {
547
+ name: 'ruby', inVM: false, wantsJSX: false,
548
+ canHandle: (cell) => isRubyLang(cell.langName || cell.lang || (cell.spec && cell.spec.lang)) && /^yes\b/i.test((cell.spec && cell.spec.pure) || ''),
549
+ load: (source, names) => {
550
+ const bin = detectRuby();
551
+ if (!bin) return { error: 'ruby-missing' };
552
+ if (looseTopLevelNonJs(String(source).split(/\r?\n/), 'ruby').length) return { error: 'ruby-toplevel' };
553
+ const fns = {}; for (const n of names) fns[n] = function () {};
554
+ return { fns, ctx: { bin, source } };
555
+ },
556
+ prove: (cell, base) => proveSubprocess(runRubyBatch, 'ruby', cell, base),
557
+ };
558
+
559
+ const phpAdapter = {
560
+ name: 'php', inVM: false, wantsJSX: false,
561
+ canHandle: (cell) => isPhpLang(cell.langName || cell.lang || (cell.spec && cell.spec.lang)) && /^yes\b/i.test((cell.spec && cell.spec.pure) || ''),
562
+ load: (source, names) => {
563
+ const bin = detectPhp();
564
+ if (!bin) return { error: 'php-missing' };
565
+ if (looseTopLevelNonJs(String(source).split(/\r?\n/), 'php').length) return { error: 'php-toplevel' };
566
+ const fns = {}; for (const n of names) fns[n] = function () {};
567
+ return { fns, ctx: { bin, source } };
568
+ },
569
+ prove: (cell, base) => proveSubprocess(runPhpBatch, 'php', cell, base),
570
+ };
571
+
572
+ // Registry (order = precedence). The out-of-VM language provers (Python/Ruby/PHP) are claimed
573
+ // before pure-call so a `pure: yes` Cell in those languages routes to its subprocess adapter,
574
+ // not the JS-VM one. The effect/`records:` surface stays an ADVERSARY concern, not a prover mode.
575
+ const ADAPTERS = [renderAdapter, pythonAdapter, rubyAdapter, phpAdapter, pureCallAdapter];
576
+
577
+ // Stage 5 — run one Cell's generated inputs through the adapter's checker. A single
578
+ // counterexample ⇒ Red; can't-check ⇒ honest skip; all cases hold ⇒ proven. Shared
579
+ // across adapters (this is the loop the old proveCell/proveRenderCell each duplicated).
580
+ function runCases(adapter, ctx, cell, fn) {
581
+ const params = adapter.inputs(cell);
582
+ if (params.some((p) => !valuesFor(p.type))) return { status: 'skip', level: 'yellow', reason: adapter.inputNoun + ' include a type the prover can\'t generate (' + params.map((p) => p.type).join(', ') + ')' };
583
+ const ensures = String(cell.spec.ensures || '').trim();
584
+ let checker;
585
+ try { checker = adapter.checker(ctx, params.map((p) => p.name), ensures); }
586
+ catch (_) { return { status: 'skip', level: 'yellow', reason: 'ensures not machine-checkable — ' + ensuresHint(ensures) }; }
587
+ const tuples = cartesian(params.map((p) => valuesFor(p.type)), 40);
588
+ const cases = tuples.length || 1;
589
+ let threw = 0, lastErr = '';
590
+ for (const A of (tuples.length ? tuples : [[]])) {
591
+ let r;
592
+ try { r = checker(fn, A); }
593
+ catch (e) { threw++; lastErr = e && e.message ? e.message.split('\n')[0] : 'threw'; continue; }
594
+ if (!r.ok) {
595
+ const argstr = params.map((p, i) => `${p.name}=${show(A[i])}`).join(', ');
596
+ return { status: 'fail', level: 'red', cases, counterexample: adapter.describe(cell, argstr, r, ensures) };
597
+ }
598
+ }
599
+ if (threw >= cases) return { status: 'skip', level: 'yellow', reason: `threw ${adapter.threwVerb} (${lastErr}) — can't prove` };
600
+ return { status: 'pass', level: 'info', cases: cases - threw };
601
+ }
602
+
603
+ // Mutation testing: corrupt the code one edit at a time, re-run the SAME ensures
604
+ // tests (via the Cell's own adapter), and see how many mutants the ensures kills. A
605
+ // low score means the ensures is too weak to be trusted — it passes even when broken.
606
+ function runMutation(adapter, source, cell, opts) {
607
+ opts = opts || {};
608
+ const params = adapter.inputs(cell);
609
+ const ensures = String(cell.spec.ensures || '');
610
+ const muts = mutants(cell.unitBody || '', 30);
611
+ if (!muts.length) return { total: 0, killed: 0, survived: 0, score: null };
612
+ const tuples = cartesian(params.map((p) => valuesFor(p.type)), 40);
613
+ const inputs = tuples.length ? tuples : [[]];
614
+ const srcLines = source.split(/\r?\n/);
615
+ const start = cell.unitBodyStart || 0;
616
+ const len = (cell.unitBody || '').split('\n').length;
617
+ let killed = 0, survived = 0, survivor = null;
618
+ for (const m of muts) {
619
+ const lines = srcLines.slice();
620
+ lines.splice(start, len, ...m.code.split('\n'));
621
+ const r = adapter.load(lines.join('\n'), [cell.unitName], { jsx: opts.jsx, file: opts.file });
622
+ if (r.error || typeof r.fns[cell.unitName] !== 'function') { killed++; continue; } // mutant broke → detected
623
+ let checker;
624
+ try { checker = adapter.checker(r.ctx, params.map((p) => p.name), ensures); }
625
+ catch (_) { killed++; continue; }
626
+ let dead = false;
627
+ for (const A of inputs) {
628
+ let rr; try { rr = checker(r.fns[cell.unitName], A); } catch (_) { dead = true; break; }
629
+ if (!rr.ok) { dead = true; break; }
630
+ }
631
+ if (dead) killed++; else { survived++; if (!survivor) survivor = m.op; }
632
+ }
633
+ const total = killed + survived;
634
+ return { total, killed, survived, score: total ? killed / total : null, survivor };
635
+ }
636
+
637
+ // INERTNESS check — the dual of mutation testing. Mutation corrupts code and asks
638
+ // "does the ensures notice?"; inertness DELETES a branch and asks "does anything
639
+ // notice?". A branch removable with every spec-derived test still passing is
640
+ // semantically inert under the promise: dead weight, ahead-of-spec scaffolding, or a
641
+ // dormant payload riding under the signature. Route: prune it, spec it, or declare it.
642
+ // Exemptions (both SIGNED — they live inside the spec, so using one to hide a payload
643
+ // means getting a human to sign the declaration):
644
+ // throws: — a deleted guard containing `throw` is exempt when the spec declares throws
645
+ // perf: — declares intentional semantically-invisible code (cache, early-exit);
646
+ // exempts the Cell, with the reason shown.
647
+ function runInertness(adapter, source, cell, opts) {
648
+ opts = opts || {};
649
+ const declaredThrows = String((cell.spec && cell.spec.throws) || '').trim();
650
+ const perf = String((cell.spec && cell.spec.perf) || '').trim();
651
+ if (perf && !/^todo$/i.test(perf)) return { checked: 0, flagged: [], exempt: 'perf: ' + perf };
652
+ const params = adapter.inputs(cell);
653
+ const ensures = String((cell.spec && cell.spec.ensures) || '');
654
+ const dels = deletions(cell.unitBody || '', 20);
655
+ if (!dels.length) return { checked: 0, flagged: [] };
656
+ const tuples = cartesian(params.map((p) => valuesFor(p.type)), 40);
657
+ const inputs = tuples.length ? tuples : [[]];
658
+ const srcLines = source.split(/\r?\n/);
659
+ const start = cell.unitBodyStart || 0;
660
+ const len = (cell.unitBody || '').split('\n').length;
661
+ const flagged = [];
662
+ let checked = 0;
663
+ for (const d of dels) {
664
+ if (/\bthrow\b/.test(d.removed) && declaredThrows && !/^todo$/i.test(declaredThrows)) continue; // declared guard
665
+ checked++;
666
+ const lines = srcLines.slice();
667
+ lines.splice(start, len, ...d.code.split('\n'));
668
+ const r = adapter.load(lines.join('\n'), [cell.unitName], { jsx: opts.jsx, file: opts.file });
669
+ if (r.error || typeof r.fns[cell.unitName] !== 'function') continue; // deletion broke the load → not inert
670
+ let checker;
671
+ try { checker = adapter.checker(r.ctx, params.map((p) => p.name), ensures); }
672
+ catch (_) { continue; }
673
+ let noticed = false;
674
+ for (const A of inputs) {
675
+ let rr; try { rr = checker(r.fns[cell.unitName], A); } catch (_) { noticed = true; break; }
676
+ if (!rr.ok) { noticed = true; break; }
677
+ }
678
+ if (!noticed) {
679
+ const snippet = d.removed.trim().split('\n')[0].slice(0, 80);
680
+ flagged.push({ desc: d.desc, line: (cell.unitBodyStart || 0) + d.line, snippet });
681
+ if (flagged.length >= 5) break; // enough to act on; don't drown the report
682
+ }
683
+ }
684
+ return { checked, flagged };
685
+ }
686
+
687
+ // LITERAL-SEEDED TRIGGER HUNTING — the complement to inertness. Harvest the magic
688
+ // constants a dormant gate compares inputs against, synthesise IN-DOMAIN inputs that
689
+ // fire the gate, and check `ensures`. A hit is a PROVEN spec↔code contradiction → RED
690
+ // (same as any counterexample). Honesty: code-derived inputs live in a RED-ONLY lane —
691
+ // they can convict but never acquit, and never touch the proven-case count. Only builds
692
+ // inputs valid per `in:` (a string literal vs a `number` param is out-of-domain → skip),
693
+ // so every Red it produces is real.
694
+ function cloneVal(v) { try { return v == null ? v : JSON.parse(JSON.stringify(v)); } catch (_) { return v; } }
695
+ // Candidate IN-DOMAIN inputs for a param that satisfy the harvested trigger constraints.
696
+ // Returns a FEW variants (varying the free fields) so a triggered payload whose fake
697
+ // output coincidentally matches one fill is still caught by another — like a fuzzer
698
+ // varying the non-dictionary bytes after hitting a comparison. [] ⇒ can't build (skip).
699
+ function synthSeeds(type, cons) {
700
+ const lt = String(type).toLowerCase();
701
+ const eq = cons.find((c) => c.kind === 'eq');
702
+ if (eq) {
703
+ if (lt === 'string' && typeof eq.value === 'string') return [eq.value];
704
+ if (lt === 'number' && typeof eq.value === 'number') return [eq.value];
705
+ if (lt === 'boolean' && typeof eq.value === 'boolean') return [eq.value];
706
+ return []; // out-of-domain scalar
707
+ }
708
+ const lenC = cons.find((c) => c.kind === 'length');
709
+ const idxC = cons.filter((c) => c.kind === 'index-prop');
710
+ const props = cons.filter((c) => c.kind === 'prop');
711
+ if (lt === 'string' && lenC && typeof lenC.value === 'number' && !idxC.length && !props.length) {
712
+ return (lenC.value >= 0 && lenC.value <= 256) ? ['x'.repeat(lenC.value)] : [];
713
+ }
714
+ if (/\[\]$/.test(String(type)) && (lenC || idxC.length)) {
715
+ const elemType = String(type).replace(/\[\]$/, '');
716
+ let len = lenC ? lenC.value : 1;
717
+ for (const c of idxC) len = Math.max(len, (c.key.index || 0) + 1);
718
+ if (!(len >= 0 && len <= 64)) return [];
719
+ const ev = valuesFor(elemType) || [];
720
+ const fills = (ev.length ? ev.slice(-3) : [null]); // a few distinct element fills (non-degenerate)
721
+ const out = [];
722
+ for (const e of fills) {
723
+ const arr = []; for (let i = 0; i < len; i++) arr.push(cloneVal(e));
724
+ let ok = true;
725
+ for (const c of idxC) { const el = arr[c.key.index]; if (el && typeof el === 'object') el[c.key.prop] = c.value; else { ok = false; break; } }
726
+ if (ok) out.push(arr);
727
+ }
728
+ return out;
729
+ }
730
+ const shape = objectShape(type);
731
+ if (shape && props.length) {
732
+ const sv = valuesFor(type) || [];
733
+ const bases = sv.length ? sv.slice(0, 3) : [{}];
734
+ const out = [];
735
+ for (const b of bases) {
736
+ const o = (b && typeof b === 'object') ? cloneVal(b) : {};
737
+ let ok = true;
738
+ for (const c of props) { if (!shape.some((f) => f.name === c.key)) { ok = false; break; } o[c.key] = c.value; }
739
+ if (ok) out.push(o);
740
+ }
741
+ return out;
742
+ }
743
+ return [];
744
+ }
745
+ function runSeeded(adapter, ctx, cell, fn) {
746
+ const params = adapter.inputs(cell);
747
+ if (!params.length) return null;
748
+ const lits = harvestLiterals(cell.unitBody || '', 30);
749
+ if (!lits.length) return null;
750
+ const byRoot = {};
751
+ for (const c of lits) { if (params.some((p) => p.name === c.root)) (byRoot[c.root] = byRoot[c.root] || []).push(c); }
752
+ const roots = Object.keys(byRoot);
753
+ if (!roots.length) return null;
754
+ const ensures = String(cell.spec.ensures || '').trim();
755
+ let checker;
756
+ try { checker = adapter.checker(ctx, params.map((p) => p.name), ensures); } catch (_) { return null; }
757
+ const baseTuple = (cartesian(params.map((p) => valuesFor(p.type) || [undefined]), 1)[0] || params.map(() => undefined));
758
+ let budget = 24; // cap total seeded runs per Cell (cost)
759
+ for (const root of roots) {
760
+ const pi = params.findIndex((p) => p.name === root);
761
+ for (const seed of synthSeeds(params[pi].type, byRoot[root])) {
762
+ if (budget-- <= 0) return null;
763
+ const A = baseTuple.slice(); A[pi] = seed;
764
+ let r; try { r = checker(fn, A); } catch (_) { continue; } // threw on this input → not a promise violation
765
+ if (r && r.ok === false) {
766
+ const argstr = params.map((p, i) => `${p.name}=${show(A[i])}`).join(', ');
767
+ return { status: 'fail', level: 'red', viaSeed: true, cases: 0, counterexample: `${cell.unitName}(${argstr}) → ${show(r.out)} — violates ensures: ${ensures} (triggered by a constant found in the code)` };
768
+ }
769
+ }
770
+ }
771
+ return null;
772
+ }
773
+
774
+ // BRANCH-EXERCISE HONESTY (in-VM JS only). Instrument the unit's branches, run the SAME
775
+ // spec-derived inputs, and report which branches were exercised. Returns { total, exercised,
776
+ // missed:[{line,kind}] } or null (no branches, or Babel absent → no badge, never an error).
777
+ // Coverage is a HONESTY BADGE, not a verdict: it never turns a pass into a fail here — policy
778
+ // decides whether unexercised branches block the gate (see verify.js `coverage: full`).
779
+ function runCoverageForCell(cell, source, inputs, opts) {
780
+ opts = opts || {};
781
+ const file = opts.file || '';
782
+ const startLine = (cell.unitBodyStart || 0) + 1; // 1-based
783
+ const endLine = startLine + (cell.unitBody || '').split('\n').length; // inclusive-ish
784
+ const inst = instrumentBranches(source, startLine, endLine, {
785
+ ts: /\.tsx?$/.test(file), tsx: /\.tsx$/.test(file), jsx: !!opts.jsx, file,
786
+ });
787
+ if (!inst || !inst.probes.length) return null; // straight-line code or Babel missing → no badge
788
+ const cov = new Array(inst.probes.length).fill(0);
789
+ const registry = {};
790
+ const ctx = makeSandbox([cell.unitName], registry, { __ylcov: cov });
791
+ const code = neutralizeModules(inst.code); // inst.code is already TS/JSX-lowered by the coverage pass
792
+ const cap = '\n;try{__ylreg({' + JSON.stringify(cell.unitName) + ':(typeof ' + safeIdent(cell.unitName) + "!=='undefined'?" + safeIdent(cell.unitName) + ":undefined)});}catch(e){}";
793
+ try { vm.runInContext(code + cap, ctx, { timeout: 2000 }); } catch (_) { return null; }
794
+ const fn = registry[cell.unitName];
795
+ if (typeof fn !== 'function') return null;
796
+ for (const A of inputs) { try { fn.apply(null, A); } catch (_) { /* branch may throw; the hit still counted */ } }
797
+ const exercised = cov.reduce((a, c) => a + (c > 0 ? 1 : 0), 0);
798
+ const missed = inst.probes.map((p, i) => ({ line: p.line, kind: p.kind, hit: cov[i] > 0 })).filter((x) => !x.hit).map((x) => ({ line: x.line, kind: x.kind }));
799
+ return { total: inst.probes.length, exercised, missed };
800
+ }
801
+
802
+ // The driver — dispatch each Cell to an adapter, load once per file, run cases, grade.
803
+ // opts.mutate (default true) also grades each passing Cell's ensures by mutation.
804
+ // opts.adapters overrides the registry (used by tests). Returns { [cellId]: result }.
805
+ function proveManifest(manifest, opts) {
806
+ const mutate = !opts || opts.mutate !== false;
807
+ const adapters = (opts && opts.adapters) || ADAPTERS;
808
+ const out = {};
809
+ const byFile = {};
810
+ for (const id of Object.keys(manifest.cells)) {
811
+ const c = manifest.cells[id];
812
+ const isLeaf = !(c.contains && c.contains.length);
813
+ if (!isLeaf || !c.unitFound || !(c.spec && c.spec.ensures)) continue;
814
+ const adapter = adapters.find((a) => a.canHandle(c));
815
+ if (!adapter) continue; // no adapter handles it → stays Yellow (signed, unproven)
816
+ if (adapter.inVM && c.lang && !isJsLang(c.lang)) continue; // JS-VM adapters skip explicit non-JS langs (missing lang ⇒ JS default)
817
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(c.unitName || '')) { out[id] = { status: 'skip', level: 'info', reason: 'method/qualified units not yet supported' }; continue; }
818
+ (byFile[c.file] = byFile[c.file] || []).push({ cell: c, adapter });
819
+ }
820
+ for (const file of Object.keys(byFile)) {
821
+ const items = byFile[file];
822
+ // Transform JSX when the file is .jsx/.tsx or any of its adapters wants it.
823
+ const wantsJSX = /\.(jsx|tsx)$/i.test(file) || items.some((it) => it.adapter.wantsJSX);
824
+ let source; try { source = fs.readFileSync(path.join(manifest.root, file), 'utf8'); }
825
+ catch (_) { for (const it of items) out[it.cell.id] = { status: 'skip', level: 'info', reason: 'file unreadable' }; continue; }
826
+ const base = items[0].adapter.load(source, items.map((it) => it.cell.unitName), { jsx: wantsJSX, file });
827
+ if (base.error) {
828
+ const reason = base.error === 'jsx-needs-babel'
829
+ ? 'add the optional deps @babel/core + @babel/plugin-transform-react-jsx to machine-prove JSX Cells'
830
+ : base.error === 'python-missing'
831
+ ? 'install Python 3 (python3/python on PATH) to machine-prove Python Cells'
832
+ : base.error === 'python-toplevel'
833
+ ? 'not run: this file has top-level code that would execute on import — wrap it in a Cell (it is also flagged Pink)'
834
+ : 'could not run file (' + base.error + ')';
835
+ for (const it of items) out[it.cell.id] = { status: 'skip', level: 'info', reason }; continue;
836
+ }
837
+ for (const it of items) {
838
+ const fn = base.fns[it.cell.unitName];
839
+ if (typeof fn !== 'function') { out[it.cell.id] = { status: 'skip', level: 'info', reason: 'unit not callable in isolation' }; continue; }
840
+ try {
841
+ let res;
842
+ if (it.adapter.prove) {
843
+ res = it.adapter.prove(it.cell, base, fn); // adapter owns its execution (e.g. out-of-VM) + verdict
844
+ } else {
845
+ res = runCases(it.adapter, base.ctx, it.cell, fn);
846
+ if (res.status === 'pass') {
847
+ // Code-derived trigger hunt: a hit is a proven contradiction → Red (downgrades
848
+ // the pass). Runs even without mutate — it's a security check, not a grade.
849
+ const seeded = runSeeded(it.adapter, base.ctx, it.cell, fn);
850
+ if (seeded) res = seeded;
851
+ else if (mutate) {
852
+ res.mutation = runMutation(it.adapter, source, it.cell, { jsx: wantsJSX, file });
853
+ res.inertness = runInertness(it.adapter, source, it.cell, { jsx: wantsJSX, file });
854
+ // Branch-exercise honesty (pure-call/JS): does the green cover every branch, or only some?
855
+ if (it.adapter.name === 'pure-call') {
856
+ const params = it.adapter.inputs(it.cell);
857
+ const tuples = cartesian(params.map((p) => valuesFor(p.type)), 40);
858
+ res.coverage = runCoverageForCell(it.cell, source, tuples.length ? tuples : [[]], { jsx: wantsJSX, file });
859
+ }
860
+ }
861
+ }
862
+ }
863
+ // Undeclared-input predicate provenance — a STATIC AST check, so it runs on ANY JS unit
864
+ // regardless of proof status (a prose-`ensures` Cell that only reaches Yellow is exactly where a
865
+ // hidden control input can hide). Cheap, and never fails the proof (under-flag on any error).
866
+ if (res && !res.predicate && it.adapter && it.adapter.inVM && (!it.cell.lang || isJsLang(it.cell.lang))) {
867
+ try { const declared = parseIn(it.cell.spec).map((p) => p.name); const pred = analyzePredicates(source, it.cell, declared, { jsx: wantsJSX, file }); if (pred) res.predicate = pred; } catch (_) {}
868
+ }
869
+ out[it.cell.id] = res;
870
+ } catch (e) { out[it.cell.id] = { status: 'skip', level: 'info', reason: 'prover error: ' + (e && e.message) }; }
871
+ }
872
+ }
873
+ return out;
874
+ }
875
+
876
+ module.exports = { proveManifest, runSource, parseIn, parseInCased, valuesFor, buildChecker, buildEffectChecker, buildRenderChecker, transformJSX, show, ensuresHint, normalizeEnsures, ADAPTERS, pureCallAdapter, renderAdapter, pythonAdapter };