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/CONSTITUTION.md +55 -0
- package/LICENSE +21 -0
- package/README.md +383 -0
- package/bin/yay.js +3550 -0
- package/package.json +55 -0
- package/src/adopt.js +181 -0
- package/src/adversary.js +119 -0
- package/src/analyze.js +270 -0
- package/src/assurance.js +122 -0
- package/src/attest.js +216 -0
- package/src/capability.js +59 -0
- package/src/constitution.js +162 -0
- package/src/coverage.js +77 -0
- package/src/crypto.js +78 -0
- package/src/dashboard.js +463 -0
- package/src/durable.js +152 -0
- package/src/e2e.js +67 -0
- package/src/extract.js +179 -0
- package/src/foundation.js +143 -0
- package/src/gate.js +252 -0
- package/src/grants.js +249 -0
- package/src/history.js +77 -0
- package/src/ids.js +40 -0
- package/src/manifest.js +303 -0
- package/src/map.js +1742 -0
- package/src/mutate.js +192 -0
- package/src/objects.js +31 -0
- package/src/phone.js +188 -0
- package/src/plan.js +141 -0
- package/src/policy.js +0 -0
- package/src/predicate.js +143 -0
- package/src/prove.js +876 -0
- package/src/ratify.js +28 -0
- package/src/record.js +30 -0
- package/src/reverify.js +212 -0
- package/src/roster.js +117 -0
- package/src/signer-page.js +603 -0
- package/src/specdiff.js +69 -0
- package/src/tags.js +67 -0
- package/src/testrun.js +41 -0
- package/src/util.js +234 -0
- package/src/vendor/recovery.js +217 -0
- package/src/vendor/tweetnacl.min.js +1 -0
- package/src/verify.js +560 -0
- package/standard/STANDARD.md +135 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yay-layer",
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
|
+
"description": "A protocol for provable, signed AI code. Approved blueprints first, an inspector that checks the build matches, and it can't ship until it passes.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"verification",
|
|
8
|
+
"provenance",
|
|
9
|
+
"attestation",
|
|
10
|
+
"code-signing",
|
|
11
|
+
"spec-first",
|
|
12
|
+
"vibe-coding"
|
|
13
|
+
],
|
|
14
|
+
"author": "L.J Bergman <contact@yaylayer.com>",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"homepage": "https://yaylayer.com",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/jonas-developer/yay-layer.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/jonas-developer/yay-layer/issues"
|
|
23
|
+
},
|
|
24
|
+
"type": "commonjs",
|
|
25
|
+
"bin": {
|
|
26
|
+
"yay": "bin/yay.js"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"yay": "node bin/yay.js",
|
|
33
|
+
"test": "node test/run.js",
|
|
34
|
+
"test:smoke": "node test/smoke.js",
|
|
35
|
+
"confirm": "node test/confirm.js",
|
|
36
|
+
"example": "node bin/yay.js verify --dir examples"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"bin",
|
|
40
|
+
"src",
|
|
41
|
+
"standard/STANDARD.md",
|
|
42
|
+
"CONSTITUTION.md",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@babel/parser": "^7.29.8",
|
|
48
|
+
"qrcode-terminal": "^0.12.0"
|
|
49
|
+
},
|
|
50
|
+
"optionalDependencies": {
|
|
51
|
+
"@babel/core": "^7.29.7",
|
|
52
|
+
"@babel/plugin-transform-react-jsx": "^7.29.7",
|
|
53
|
+
"@babel/plugin-transform-typescript": "^7.29.7"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/adopt.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// `yay adopt` — retrofit: insert DRAFT, unsigned spec blocks above every named
|
|
3
|
+
// unit (function, method, arrow-prop, class method — at any nesting depth) that
|
|
4
|
+
// doesn't have one yet, using the AST analyzer. Falls back to a shallow top-level
|
|
5
|
+
// regex for files the parser can't handle (rare — severe syntax errors only).
|
|
6
|
+
//
|
|
7
|
+
// Deriving real `intent` prose from behaviour needs an LLM (roadmap); this wires
|
|
8
|
+
// up the structure, ids, indentation and a purity guess.
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { walk, repoRoot, MARK_BEGIN, langOf, commentLeadOf } = require('./util');
|
|
13
|
+
const { analyze, nearestUnitAfter } = require('./analyze');
|
|
14
|
+
const { extractFile } = require('./extract');
|
|
15
|
+
const IDS = require('./ids');
|
|
16
|
+
|
|
17
|
+
const JS_LIKE = /\.(js|jsx|mjs|cjs|ts|tsx)$/;
|
|
18
|
+
const ADOPT_FAMILIES = new Set(['js', 'brace', 'python', 'ruby']); // langs `yay adopt` can scaffold
|
|
19
|
+
// Purity GUESS per language family (drafts only — the human prunes; verify re-checks).
|
|
20
|
+
const EFFECT = /\blocalStorage\b|\bconsole\s*\.|\bfetch\s*\(|\bprocess\s*\.|\bdocument\b|\bwindow\b|\bfs\s*\.|\bMath\.random\b|\bDate\.now\b/;
|
|
21
|
+
const EFFECT_PY = /\bprint\s*\(|\binput\s*\(|\bopen\s*\(|\bos\s*\.|\bsys\s*\.|\bsubprocess\s*\.|\bsocket\s*\.|\brequests\s*\.|\burllib\b|\brandom\s*\.|\btime\s*\.\s*time\s*\(|\bdatetime\b.*\bnow\s*\(|^\s*global\s+[A-Za-z_]/m;
|
|
22
|
+
const EFFECT_RB = /\bputs\b|\bgets\b|\bFile\s*\.|\bIO\s*\.|\b\$std(out|in|err)\b|\brand\b|\bTime\s*\.\s*now\b|\bNet::|\bsystem\s*\(/;
|
|
23
|
+
function effectGuess(lang, body) {
|
|
24
|
+
const l = String(lang || '').toLowerCase();
|
|
25
|
+
const re = /^py(thon)?w?$/.test(l) ? EFFECT_PY : /^(rb|ruby|ex|exs)$/.test(l) ? EFFECT_RB : EFFECT;
|
|
26
|
+
return re.test(body || '');
|
|
27
|
+
}
|
|
28
|
+
const TOP_FN = /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z0-9_$]+)|^(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?\(/;
|
|
29
|
+
|
|
30
|
+
function existingIds(root) {
|
|
31
|
+
const ids = new Set();
|
|
32
|
+
for (const f of walk(root)) {
|
|
33
|
+
let t; try { t = fs.readFileSync(f, 'utf8'); } catch (_) { continue; }
|
|
34
|
+
let m; const re = new RegExp(MARK_BEGIN.source, 'g');
|
|
35
|
+
while ((m = re.exec(t))) ids.add(m[1]);
|
|
36
|
+
}
|
|
37
|
+
return ids;
|
|
38
|
+
}
|
|
39
|
+
// With a per-contributor `shard`, new ids are C-<shard>-<n>, monotonic and never reused (see ids.js)
|
|
40
|
+
// so parallel branches never collide. Without one, the legacy flat C-NNN scheme (single-writer repos).
|
|
41
|
+
function nextId(ids, shard) {
|
|
42
|
+
if (shard) return IDS.nextCellId(shard, ids);
|
|
43
|
+
let n = 1;
|
|
44
|
+
for (;;) { const id = 'C-' + String(n).padStart(3, '0'); if (!ids.has(id)) { ids.add(id); return id; } n++; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function draftBlock(id, name, lang, body, indent, lead) {
|
|
48
|
+
const pure = effectGuess(lang, body) ? 'no' : 'yes';
|
|
49
|
+
const p = indent || '';
|
|
50
|
+
const cc = lead || '//'; // comment lead: '#' for Python, '//' elsewhere
|
|
51
|
+
return [
|
|
52
|
+
`${p}${cc}∷YAY⟨${id}⟩ v0 DERIVED — unconfirmed, not human-reviewed`,
|
|
53
|
+
`${p}${cc} unit: ${name}`,
|
|
54
|
+
`${p}${cc} lang: ${lang}`,
|
|
55
|
+
`${p}${cc} intent: TODO — describe what this does in one sentence [inferred]`,
|
|
56
|
+
`${p}${cc} pure: ${pure}`,
|
|
57
|
+
`${p}${cc} in: TODO`,
|
|
58
|
+
`${p}${cc} out: TODO`,
|
|
59
|
+
`${p}${cc}∷YAY-END⟨${id}⟩`,
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Conservative, keyword-led declaration patterns per body-FAMILY: indent in group 1,
|
|
64
|
+
// unit name in group 2. Same safety stance as manifest's untracked scan — anchored on
|
|
65
|
+
// declaration keywords or an access modifier, never control-flow, so `yay adopt` can't
|
|
66
|
+
// scaffold a bogus unit. Families with no safe pattern here are simply not scaffolded.
|
|
67
|
+
const ADOPT_PATS = {
|
|
68
|
+
python: [/^(\s*)(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)/],
|
|
69
|
+
ruby: [/^(\s*)(?:def|defp)\s+(?:self\.)?([A-Za-z_][A-Za-z0-9_?!]*)/],
|
|
70
|
+
brace: [
|
|
71
|
+
/^(\s*)(?:pub\s+|export\s+|public\s+|private\s+|protected\s+|internal\s+|static\s+|final\s+|open\s+|override\s+|async\s+)*(?:fn|func|fun|function|sub)\s+([A-Za-z_][A-Za-z0-9_]*)/,
|
|
72
|
+
/^(\s*)(?:public|private|protected|internal)(?:\s+(?:static|virtual|override|sealed|abstract|async|partial|new|readonly|unsafe|extern|final))*\s+[A-Za-z_][A-Za-z0-9_<>[\],.?]*\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]*>)?\s*\(/,
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Retrofit a non-JS file: insert a DRAFT block above each un-specced unit, using the
|
|
77
|
+
// language's comment lead. Skips units already governed by a spec block.
|
|
78
|
+
function adoptFileLang(file, ids, dry, fam, shard) {
|
|
79
|
+
const pats = ADOPT_PATS[fam];
|
|
80
|
+
if (!pats) return 0;
|
|
81
|
+
const lead = commentLeadOf(file);
|
|
82
|
+
const langStr = path.extname(file).slice(1);
|
|
83
|
+
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
84
|
+
const covered = new Set();
|
|
85
|
+
for (const c of extractFile(file)) { if (!c.malformed && c.unitName) covered.add(c.unitName); }
|
|
86
|
+
const out = [];
|
|
87
|
+
let added = 0;
|
|
88
|
+
for (let i = 0; i < lines.length; i++) {
|
|
89
|
+
const t = lines[i].trim();
|
|
90
|
+
const isComment = t.startsWith('//') || t.startsWith('#') || t.startsWith('/*') || t.startsWith('*');
|
|
91
|
+
const prev = lines[i - 1] || '';
|
|
92
|
+
if (!isComment && !/∷YAY-END|∷YAY⟨/.test(prev)) {
|
|
93
|
+
for (const pat of pats) {
|
|
94
|
+
const m = lines[i].match(pat);
|
|
95
|
+
if (!m) continue;
|
|
96
|
+
const indent = m[1] || '';
|
|
97
|
+
const name = m[2];
|
|
98
|
+
if (name && !covered.has(name)) {
|
|
99
|
+
out.push(draftBlock(nextId(ids, shard), name, langStr, lines.slice(i, i + 25).join('\n'), indent, lead));
|
|
100
|
+
covered.add(name);
|
|
101
|
+
added++;
|
|
102
|
+
}
|
|
103
|
+
break; // first matching pattern wins for this line
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
out.push(lines[i]);
|
|
107
|
+
}
|
|
108
|
+
if (added && !dry) fs.writeFileSync(file, out.join('\n'));
|
|
109
|
+
return added;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function adoptFile(file, ids, dry, shard) {
|
|
113
|
+
const fam = langOf(file);
|
|
114
|
+
if (fam !== 'js') return adoptFileLang(file, ids, dry, fam, shard); // brace / python / ruby
|
|
115
|
+
const lang = path.extname(file).slice(1);
|
|
116
|
+
const code = fs.readFileSync(file, 'utf8');
|
|
117
|
+
const lines = code.split(/\r?\n/);
|
|
118
|
+
const ana = analyze(code);
|
|
119
|
+
if (!ana.ok) return adoptFileRegex(file, ids, dry, lines, lang, shard); // unparseable (rare)
|
|
120
|
+
|
|
121
|
+
// Units already governed by an existing Cell (skip those).
|
|
122
|
+
const covered = new Set();
|
|
123
|
+
for (const c of extractFile(file)) {
|
|
124
|
+
if (c.malformed) continue;
|
|
125
|
+
const u = nearestUnitAfter(ana.units, c.endLine);
|
|
126
|
+
if (u) covered.add(u.startLine);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const inserts = [];
|
|
130
|
+
for (const u of ana.units) {
|
|
131
|
+
if (covered.has(u.startLine)) continue;
|
|
132
|
+
const indent = (lines[u.startLine - 1].match(/^\s*/) || [''])[0];
|
|
133
|
+
const body = lines.slice(u.startLine - 1, u.endLine).join('\n');
|
|
134
|
+
inserts.push({ line: u.startLine, text: draftBlock(nextId(ids, shard), u.name, lang, body, indent) });
|
|
135
|
+
}
|
|
136
|
+
if (!inserts.length) return 0;
|
|
137
|
+
if (!dry) {
|
|
138
|
+
inserts.sort((a, b) => b.line - a.line); // bottom-to-top keeps line numbers valid
|
|
139
|
+
const out = lines.slice();
|
|
140
|
+
for (const ins of inserts) out.splice(ins.line - 1, 0, ins.text);
|
|
141
|
+
fs.writeFileSync(file, out.join('\n'));
|
|
142
|
+
}
|
|
143
|
+
return inserts.length;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Shallow fallback for files the parser cannot handle.
|
|
147
|
+
function adoptFileRegex(file, ids, dry, lines, lang, shard) {
|
|
148
|
+
const out = [];
|
|
149
|
+
let added = 0;
|
|
150
|
+
for (let i = 0; i < lines.length; i++) {
|
|
151
|
+
const m = lines[i].match(TOP_FN);
|
|
152
|
+
const prev = lines[i - 1] || '';
|
|
153
|
+
if (m && !/∷YAY-END|∷YAY⟨/.test(prev)) {
|
|
154
|
+
const name = m[1] || m[2];
|
|
155
|
+
const indent = (lines[i].match(/^\s*/) || [''])[0];
|
|
156
|
+
out.push(draftBlock(nextId(ids, shard), name, lang, lines.slice(i, i + 25).join('\n'), indent));
|
|
157
|
+
added++;
|
|
158
|
+
}
|
|
159
|
+
out.push(lines[i]);
|
|
160
|
+
}
|
|
161
|
+
if (added && !dry) fs.writeFileSync(file, out.join('\n'));
|
|
162
|
+
return added;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function adopt(targetDir, { dry = false, shard = null, ledgerIds = [] } = {}) {
|
|
166
|
+
const root = repoRoot(targetDir);
|
|
167
|
+
const ids = existingIds(root);
|
|
168
|
+
// Seed with every id the ledger has ever recorded so a deleted-but-once-signed id is never reused.
|
|
169
|
+
for (const id of ledgerIds) ids.add(id);
|
|
170
|
+
const files = walk(path.resolve(targetDir || root)).filter((f) => ADOPT_FAMILIES.has(langOf(f)));
|
|
171
|
+
const report = [];
|
|
172
|
+
let total = 0;
|
|
173
|
+
for (const f of files) {
|
|
174
|
+
let n = 0;
|
|
175
|
+
try { n = adoptFile(f, ids, dry, shard); } catch (_) { continue; }
|
|
176
|
+
if (n) { report.push({ file: path.relative(root, f), added: n }); total += n; }
|
|
177
|
+
}
|
|
178
|
+
return { total, report, dry };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = { adopt };
|
package/src/adversary.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Spec-only adversary — the independent-author layer, done right.
|
|
3
|
+
//
|
|
4
|
+
// The LLM sees ONLY a Cell's spec (never the code) and PROPOSES adversarial INPUTS
|
|
5
|
+
// most likely to violate `ensures`. It does NOT judge. YayLayer's own deterministic
|
|
6
|
+
// checker (the prover's) then runs the REAL function on each proposed input and
|
|
7
|
+
// evaluates the real `ensures`. So a "break" is machine-confirmed — the LLM can't
|
|
8
|
+
// produce a false positive from a bad mock or a wrong expectation; at worst it
|
|
9
|
+
// proposes weak inputs and nothing breaks. Restricted to pure, ensures-bearing Cells
|
|
10
|
+
// (effectful/render Cells are honestly skipped — they need a code-aware/effect test).
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { runSource, parseIn, buildChecker, buildEffectChecker, show, ensuresHint } = require('./prove');
|
|
15
|
+
const plan = require('./plan');
|
|
16
|
+
|
|
17
|
+
const SYSTEM = [
|
|
18
|
+
'You are an adversarial INPUT generator in a verification system.',
|
|
19
|
+
'You are given ONLY the formal SPEC of one function — never its source code.',
|
|
20
|
+
'Propose the input tuples MOST LIKELY to make the function violate its `ensures` postcondition.',
|
|
21
|
+
'- Every tuple MUST be valid per the `in:` declaration (right arity and types). Do not use out-of-type values.',
|
|
22
|
+
'- Hunt domain corners the spec does NOT forbid: zero, negative, empty, boundary, very large, duplicates, unordered, extreme ratios, near-equal, etc.',
|
|
23
|
+
'- You do NOT assert or judge. Return ONLY a JSON array of argument-arrays, e.g. [[0,0],[-1,5],[1000000,1]]. No prose, no code, no comments.',
|
|
24
|
+
].join('\n');
|
|
25
|
+
|
|
26
|
+
// The spec shown to the model — spec fields ONLY, never the code.
|
|
27
|
+
function specText(cell) {
|
|
28
|
+
const s = cell.spec || {};
|
|
29
|
+
const L = ['unit: ' + (cell.unitName || '')];
|
|
30
|
+
if (s.intent) L.push('intent: ' + s.intent);
|
|
31
|
+
if (s.in) L.push('in: ' + s.in);
|
|
32
|
+
if (s.out) L.push('out: ' + s.out);
|
|
33
|
+
if (s.pure) L.push('pure: ' + s.pure);
|
|
34
|
+
if (s.records) L.push('records: ' + s.records + ' (this param is instrumented by the harness — pass {} for it; ensures is checked against its recorded calls/sets)');
|
|
35
|
+
if (s.ensures) L.push('ensures: ' + s.ensures);
|
|
36
|
+
if (s.throws) L.push('throws: ' + s.throws);
|
|
37
|
+
return L.join('\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractJSON(t) {
|
|
41
|
+
t = String(t || '').trim();
|
|
42
|
+
const f = t.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
43
|
+
if (f) t = f[1].trim();
|
|
44
|
+
const a = t.indexOf('['), b = t.lastIndexOf(']');
|
|
45
|
+
if (a >= 0 && b > a) t = t.slice(a, b + 1);
|
|
46
|
+
try { const j = JSON.parse(t); return Array.isArray(j) ? j : []; } catch (_) { return []; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function eligible(cell) {
|
|
50
|
+
const s = cell.spec || {};
|
|
51
|
+
const isLeaf = !(cell.contains && cell.contains.length);
|
|
52
|
+
const pure = /^yes\b/i.test(s.pure || '');
|
|
53
|
+
const records = !!(s.records && String(s.records).trim());
|
|
54
|
+
return isLeaf && cell.unitFound && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(cell.unitName || '') && !!s.ensures && (pure || records);
|
|
55
|
+
}
|
|
56
|
+
function skipReason(cell) {
|
|
57
|
+
const s = cell.spec || {};
|
|
58
|
+
if (cell.contains && cell.contains.length) return 'container Cell (composition, not code)';
|
|
59
|
+
if (!cell.unitFound) return 'no code unit found below the spec';
|
|
60
|
+
if (!s.ensures) return 'no machine-checkable `ensures` to judge against';
|
|
61
|
+
if (!/^yes\b/i.test(s.pure || '') && !(s.records && String(s.records).trim())) return 'has effects — declare the effect surface with `records: <param>` and assert on its trace, so effects become probeable';
|
|
62
|
+
return 'not adversary-testable';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fmtArgs(params, A) { return '(' + A.map((v, i) => (params[i] || ('arg' + i)) + '=' + show(v)).join(', ') + ')'; }
|
|
66
|
+
|
|
67
|
+
// Run the REAL function on the proposed inputs and judge with the real `ensures`.
|
|
68
|
+
function judge(source, cell, tuples) {
|
|
69
|
+
const base = runSource(source, [cell.unitName]);
|
|
70
|
+
if (base.error || typeof base.fns[cell.unitName] !== 'function') return { status: 'skip', reason: 'unit not callable in isolation' };
|
|
71
|
+
const params = parseIn(cell.spec).map((p) => p.name);
|
|
72
|
+
const records = cell.spec.records && String(cell.spec.records).trim();
|
|
73
|
+
let checker;
|
|
74
|
+
try { checker = records ? buildEffectChecker(base.ctx, params, records, cell.spec.ensures) : buildChecker(base.ctx, params, cell.spec.ensures); }
|
|
75
|
+
catch (e) { return { status: 'skip', reason: 'ensures not machine-checkable — ' + ensuresHint(cell.spec.ensures) + ' (strengthen the spec, then re-sign)' }; }
|
|
76
|
+
if (!Array.isArray(tuples) || !tuples.length) return { status: 'survived', note: 'no candidate inputs proposed' };
|
|
77
|
+
const throwsDeclared = !!(cell.spec && cell.spec.throws);
|
|
78
|
+
const fmt = (A) => '(' + params.map((n, i) => (n === records ? n + '=<recorded>' : n + '=' + show(A[i]))).join(', ') + ')';
|
|
79
|
+
for (const A of tuples) {
|
|
80
|
+
if (!Array.isArray(A)) continue;
|
|
81
|
+
const snap = fmt(A); // format BEFORE the checker instruments/mutates the records slot
|
|
82
|
+
let r;
|
|
83
|
+
try { r = checker(base.fns[cell.unitName], A); }
|
|
84
|
+
catch (e) {
|
|
85
|
+
if (throwsDeclared) continue; // may be an expected throw — don't judge without an oracle
|
|
86
|
+
return { status: 'broke', counterexample: 'threw on ' + snap + ' — ' + String((e && e.message) || e).slice(0, 160) };
|
|
87
|
+
}
|
|
88
|
+
if (r && r.ok === false) return { status: 'broke', counterexample: 'ensures failed for ' + snap + (records ? '' : ' → out=' + show(r.out)) };
|
|
89
|
+
}
|
|
90
|
+
return { status: 'survived' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// auth: provider config (resolvePlanAuth). opts.cells = id filter; opts.chat = fake LLM for tests.
|
|
94
|
+
async function adversaryManifest(manifest, auth, opts) {
|
|
95
|
+
opts = opts || {};
|
|
96
|
+
const chat = opts.chat || plan.chat;
|
|
97
|
+
const out = {};
|
|
98
|
+
const byFile = {};
|
|
99
|
+
for (const id of Object.keys(manifest.cells)) {
|
|
100
|
+
const c = manifest.cells[id];
|
|
101
|
+
if (opts.cells && opts.cells.indexOf(id) < 0) continue;
|
|
102
|
+
if (!eligible(c)) { if (opts.cells) out[id] = { status: 'skip', reason: skipReason(c) }; continue; }
|
|
103
|
+
(byFile[c.file] = byFile[c.file] || []).push(c);
|
|
104
|
+
}
|
|
105
|
+
for (const file of Object.keys(byFile)) {
|
|
106
|
+
let source; try { source = fs.readFileSync(path.join(manifest.root, file), 'utf8'); }
|
|
107
|
+
catch (_) { for (const c of byFile[file]) out[c.id] = { status: 'skip', reason: 'file unreadable' }; continue; }
|
|
108
|
+
for (const c of byFile[file]) {
|
|
109
|
+
let tuples;
|
|
110
|
+
try { tuples = extractJSON(await chat(SYSTEM, specText(c), auth)); }
|
|
111
|
+
catch (e) { out[c.id] = { status: 'error', reason: 'LLM error: ' + (e && e.message ? e.message : String(e)) }; continue; }
|
|
112
|
+
out[c.id] = judge(source, c, tuples);
|
|
113
|
+
out[c.id].tried = Array.isArray(tuples) ? tuples.length : 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { adversaryManifest, specText, extractJSON, eligible, skipReason, judge, SYSTEM };
|
package/src/analyze.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// AST-based analyzer for JS / TypeScript / JSX / TSX (@babel/parser with the
|
|
3
|
+
// `estree` plugin). Enumerates every NAMED executable unit —
|
|
4
|
+
// function declaration, function expression assigned to a name, arrow-prop,
|
|
5
|
+
// object method, class method/getter/setter — at ANY nesting depth. This is
|
|
6
|
+
// what lets YayLayer see units inside IIFEs, objects and classes (the old regex
|
|
7
|
+
// only saw top-level `function name(`).
|
|
8
|
+
//
|
|
9
|
+
// A "unit" is a named block that does a job. Anonymous inline callbacks
|
|
10
|
+
// (arr.map(x => …), event handlers) are considered part of their enclosing unit,
|
|
11
|
+
// not separate units. Also flags top-level imperative code that runs at load.
|
|
12
|
+
|
|
13
|
+
const babel = require('@babel/parser');
|
|
14
|
+
const { hasLoadTimeEffect } = require('./util');
|
|
15
|
+
|
|
16
|
+
// One parser for JS, TypeScript, JSX and TSX. The `estree` plugin makes the AST
|
|
17
|
+
// ESTree-shaped (Literal / Property / MethodDefinition) so the walker below is
|
|
18
|
+
// standard. errorRecovery keeps a partial tree on odd syntax instead of failing.
|
|
19
|
+
const PLUGINS = ['estree', 'typescript', 'jsx', 'decorators-legacy'];
|
|
20
|
+
function parse(code) {
|
|
21
|
+
try {
|
|
22
|
+
return babel.parse(code, {
|
|
23
|
+
sourceType: 'unambiguous', errorRecovery: true,
|
|
24
|
+
allowReturnOutsideFunction: true, allowImportExportEverywhere: true,
|
|
25
|
+
plugins: PLUGINS,
|
|
26
|
+
}).program;
|
|
27
|
+
} catch (_) { return null; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function keyName(key) {
|
|
31
|
+
if (!key) return null;
|
|
32
|
+
if (key.type === 'Identifier') return key.name;
|
|
33
|
+
if (key.type === 'Literal') return String(key.value);
|
|
34
|
+
if (key.type === 'PrivateIdentifier') return '#' + key.name;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function memberName(node) {
|
|
38
|
+
if (!node) return null;
|
|
39
|
+
if (node.type === 'Identifier') return node.name;
|
|
40
|
+
if (node.type === 'MemberExpression') {
|
|
41
|
+
const obj = memberName(node.object);
|
|
42
|
+
const prop = node.computed ? null : keyName(node.property);
|
|
43
|
+
return prop ? (obj ? obj + '.' + prop : prop) : obj;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function className(ancestors) {
|
|
48
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
49
|
+
const a = ancestors[i];
|
|
50
|
+
if (a.type === 'ClassDeclaration' || a.type === 'ClassExpression') return a.id ? a.id.name : 'class';
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Derive a name + kind for a function node from its surrounding context.
|
|
56
|
+
function nameFn(node, ancestors) {
|
|
57
|
+
const parent = ancestors[ancestors.length - 1];
|
|
58
|
+
if (node.type === 'FunctionDeclaration' && node.id) return { name: node.id.name, kind: 'function' };
|
|
59
|
+
if (!parent) return null;
|
|
60
|
+
const arrowKind = node.type === 'ArrowFunctionExpression' ? 'arrow' : 'function';
|
|
61
|
+
switch (parent.type) {
|
|
62
|
+
case 'VariableDeclarator':
|
|
63
|
+
return parent.id && parent.id.name ? { name: parent.id.name, kind: arrowKind } : null;
|
|
64
|
+
case 'AssignmentExpression': {
|
|
65
|
+
const n = memberName(parent.left);
|
|
66
|
+
return n ? { name: n, kind: arrowKind } : null;
|
|
67
|
+
}
|
|
68
|
+
case 'Property': {
|
|
69
|
+
const n = keyName(parent.key);
|
|
70
|
+
return n ? { name: n, kind: node.type === 'ArrowFunctionExpression' ? 'arrow' : 'method' } : null;
|
|
71
|
+
}
|
|
72
|
+
case 'MethodDefinition': {
|
|
73
|
+
const n = keyName(parent.key);
|
|
74
|
+
if (!n) return null;
|
|
75
|
+
const cls = className(ancestors);
|
|
76
|
+
const k = parent.kind && parent.kind !== 'method' ? parent.kind : 'method';
|
|
77
|
+
return { name: (cls ? cls + '.' : '') + n, kind: k };
|
|
78
|
+
}
|
|
79
|
+
case 'PropertyDefinition': { // class field: foo = () => {}
|
|
80
|
+
const n = keyName(parent.key);
|
|
81
|
+
if (!n) return null;
|
|
82
|
+
const cls = className(ancestors);
|
|
83
|
+
return { name: (cls ? cls + '.' : '') + n, kind: 'arrow' };
|
|
84
|
+
}
|
|
85
|
+
default:
|
|
86
|
+
return null; // anonymous — belongs to the enclosing unit
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function walk(node, ancestors, visit) {
|
|
91
|
+
if (!node || typeof node.type !== 'string') return;
|
|
92
|
+
visit(node, ancestors);
|
|
93
|
+
const next = ancestors.concat(node);
|
|
94
|
+
for (const key of Object.keys(node)) {
|
|
95
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'range') continue;
|
|
96
|
+
const child = node[key];
|
|
97
|
+
if (Array.isArray(child)) { for (const c of child) walk(c, next, visit); }
|
|
98
|
+
else walk(child, next, visit);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The nearest lexical class / object-literal a unit lives in (for real nesting).
|
|
103
|
+
function containerOf(ancestors) {
|
|
104
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
105
|
+
const a = ancestors[i];
|
|
106
|
+
if (a.type === 'ClassDeclaration' || a.type === 'ClassExpression') return a.id ? a.id.name : 'class';
|
|
107
|
+
if (a.type === 'VariableDeclarator' && a.id && a.id.type === 'Identifier' && a.init && a.init.type === 'ObjectExpression') return a.id.name;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function objectPublicNames(objExpr, out) {
|
|
113
|
+
if (!objExpr || objExpr.type !== 'ObjectExpression') return;
|
|
114
|
+
for (const p of objExpr.properties || []) {
|
|
115
|
+
if (p.type !== 'Property') continue;
|
|
116
|
+
if (p.value && p.value.type === 'Identifier') out.add(p.value.name); // { getDB: getDB }
|
|
117
|
+
const k = keyName(p.key); if (k) out.add(k); // { getDB() {} } / { getDB }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The namespace this module exposes (global.X / window.X / module.exports / return)
|
|
122
|
+
// and the set of unit names that are part of its PUBLIC api.
|
|
123
|
+
function namespaceOf(ast) {
|
|
124
|
+
const objByVar = {};
|
|
125
|
+
walk(ast, [], (node) => {
|
|
126
|
+
if (node.type === 'VariableDeclarator' && node.id && node.id.type === 'Identifier' && node.init && node.init.type === 'ObjectExpression') objByVar[node.id.name] = node.init;
|
|
127
|
+
});
|
|
128
|
+
let name = null; const publicNames = new Set();
|
|
129
|
+
const consider = (exposeName, val) => {
|
|
130
|
+
let obj = null;
|
|
131
|
+
if (val && val.type === 'ObjectExpression') obj = val;
|
|
132
|
+
else if (val && val.type === 'Identifier' && objByVar[val.name]) obj = objByVar[val.name];
|
|
133
|
+
if (!obj) return;
|
|
134
|
+
if (exposeName && !name) name = exposeName;
|
|
135
|
+
objectPublicNames(obj, publicNames);
|
|
136
|
+
};
|
|
137
|
+
walk(ast, [], (node) => {
|
|
138
|
+
if (node.type === 'AssignmentExpression' && node.left && node.left.type === 'MemberExpression' && !node.left.computed) {
|
|
139
|
+
const o = node.left.object; const prop = keyName(node.left.property);
|
|
140
|
+
if (o.type === 'Identifier' && (o.name === 'global' || o.name === 'window' || o.name === 'globalThis') && prop) consider(prop, node.right);
|
|
141
|
+
else if (o.type === 'Identifier' && o.name === 'module' && prop === 'exports') consider(null, node.right);
|
|
142
|
+
} else if (node.type === 'ReturnStatement' && node.argument) {
|
|
143
|
+
consider(null, node.argument);
|
|
144
|
+
} else if (node.type === 'ExportNamedDeclaration') { // ES modules
|
|
145
|
+
const d = node.declaration;
|
|
146
|
+
if (d && d.id && d.id.name) publicNames.add(d.id.name);
|
|
147
|
+
if (d && d.declarations) for (const v of d.declarations) if (v.id && v.id.name) publicNames.add(v.id.name);
|
|
148
|
+
for (const s of node.specifiers || []) if (s.local && s.local.name) publicNames.add(s.local.name);
|
|
149
|
+
} else if (node.type === 'ExportDefaultDeclaration') {
|
|
150
|
+
if (node.declaration && node.declaration.id && node.declaration.id.name) publicNames.add(node.declaration.id.name);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return { name, publicNames };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const FN_TYPES = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression']);
|
|
157
|
+
|
|
158
|
+
function isIIFE(node) {
|
|
159
|
+
if (node.type !== 'ExpressionStatement') return false;
|
|
160
|
+
let e = node.expression;
|
|
161
|
+
while (e && (e.type === 'UnaryExpression' || e.type === 'AwaitExpression')) e = e.argument;
|
|
162
|
+
return !!(e && e.type === 'CallExpression' && e.callee && FN_TYPES.has(e.callee.type));
|
|
163
|
+
}
|
|
164
|
+
function isDirective(node) {
|
|
165
|
+
return node.type === 'ExpressionStatement' && node.expression &&
|
|
166
|
+
node.expression.type === 'Literal' && typeof node.expression.value === 'string';
|
|
167
|
+
}
|
|
168
|
+
function looseTopLevel(ast, code) {
|
|
169
|
+
// Always-skip: definitions, imports/exports, empty. (A function/class DEFINITION that
|
|
170
|
+
// contains fetch is fine — it isn't executed at load; it's handled as a unit elsewhere.)
|
|
171
|
+
const SKIP = new Set(['FunctionDeclaration', 'ClassDeclaration',
|
|
172
|
+
'ImportDeclaration', 'ExportDefaultDeclaration', 'ExportAllDeclaration', 'EmptyStatement']);
|
|
173
|
+
// Wiring that's exempt UNLESS its right-hand side runs an exfil/exec effect at import
|
|
174
|
+
// (`const LEAK = fetch(evil)`, `X = require('cp').execSync(...)`): declarations, named
|
|
175
|
+
// exports (which wrap a declaration), and bare assignments (module.exports = …, X = Y).
|
|
176
|
+
const out = [];
|
|
177
|
+
for (const node of ast.body) {
|
|
178
|
+
if (SKIP.has(node.type) || isDirective(node) || isIIFE(node)) continue;
|
|
179
|
+
const isDecl = node.type === 'VariableDeclaration' || node.type === 'ExportNamedDeclaration';
|
|
180
|
+
const isAssign = node.type === 'ExpressionStatement' && node.expression && node.expression.type === 'AssignmentExpression';
|
|
181
|
+
if (isDecl || isAssign) {
|
|
182
|
+
if (code && node.start != null && node.end != null && hasLoadTimeEffect(code.slice(node.start, node.end), 'js')) out.push(node.loc.start.line);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
out.push(node.loc.start.line);
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Returns { ok, units:[{name,kind,startLine,endLine}], loose:[line,…] }.
|
|
191
|
+
// ok:false means the file could not be parsed (e.g. TypeScript) — caller degrades.
|
|
192
|
+
function calleeName(callee) {
|
|
193
|
+
if (!callee) return null;
|
|
194
|
+
if (callee.type === 'Identifier') return callee.name;
|
|
195
|
+
if (callee.type === 'MemberExpression' && !callee.computed) return keyName(callee.property);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function analyze(code) {
|
|
200
|
+
const ast = parse(code);
|
|
201
|
+
if (!ast) return { ok: false, units: [], loose: [], calls: [], bindings: [], namespace: { name: null, publicNames: new Set() } };
|
|
202
|
+
const units = [];
|
|
203
|
+
const seen = new Set();
|
|
204
|
+
const calls = new Set(); // short callee names used anywhere in the file → for the call graph
|
|
205
|
+
const unitByNode = new Map(); // function AST node → its unit record (for unit-level calls)
|
|
206
|
+
walk(ast, [], (node, ancestors) => {
|
|
207
|
+
if (node.type === 'CallExpression') {
|
|
208
|
+
const cn = calleeName(node.callee);
|
|
209
|
+
// A DIRECT call `foo(...)` (bare identifier) is resolvable to a definition;
|
|
210
|
+
// a member call `obj.foo(...)` is a method and can't be resolved statically.
|
|
211
|
+
const direct = node.callee && node.callee.type === 'Identifier' ? node.callee.name : null;
|
|
212
|
+
if (cn) {
|
|
213
|
+
calls.add(cn);
|
|
214
|
+
// Attribute this call to the innermost named unit that encloses it, so we
|
|
215
|
+
// get a UNIT-level call graph (who calls whom), not just a file-level one.
|
|
216
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
217
|
+
const rec = unitByNode.get(ancestors[i]);
|
|
218
|
+
if (rec) { rec.callsOut.add(cn); if (direct) rec.callsDirect.add(direct); break; }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (!FN_TYPES.has(node.type)) return;
|
|
224
|
+
const named = nameFn(node, ancestors);
|
|
225
|
+
if (!named) return;
|
|
226
|
+
const key = named.name + '@' + node.loc.start.line;
|
|
227
|
+
if (seen.has(key)) return;
|
|
228
|
+
seen.add(key);
|
|
229
|
+
const rec = { name: named.name, kind: named.kind, startLine: node.loc.start.line, endLine: node.loc.end.line, container: containerOf(ancestors), callsOut: new Set(), callsDirect: new Set() };
|
|
230
|
+
units.push(rec);
|
|
231
|
+
unitByNode.set(node, rec); // pre-order: a unit is recorded before we descend into its body
|
|
232
|
+
});
|
|
233
|
+
units.sort((a, b) => a.startLine - b.startLine);
|
|
234
|
+
for (const u of units) { u.callsOut = [...u.callsOut]; u.callsDirect = [...u.callsDirect]; }
|
|
235
|
+
return { ok: true, units, loose: looseTopLevel(ast, code), calls: [...calls], bindings: collectBindings(ast), namespace: namespaceOf(ast) };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Every name BOUND anywhere in the file (function/class ids, variable declarators,
|
|
239
|
+
// params — incl. destructured — imports, catch params). Used to decide whether a
|
|
240
|
+
// direct call resolves to something defined (vs a dangling reference / typo).
|
|
241
|
+
function collectBindings(ast) {
|
|
242
|
+
const b = new Set();
|
|
243
|
+
const addPat = (p) => {
|
|
244
|
+
if (!p) return;
|
|
245
|
+
if (p.type === 'Identifier') b.add(p.name);
|
|
246
|
+
else if (p.type === 'ObjectPattern') (p.properties || []).forEach((pr) => addPat(pr.value || pr.argument));
|
|
247
|
+
else if (p.type === 'ArrayPattern') (p.elements || []).forEach(addPat);
|
|
248
|
+
else if (p.type === 'AssignmentPattern') addPat(p.left);
|
|
249
|
+
else if (p.type === 'RestElement') addPat(p.argument);
|
|
250
|
+
};
|
|
251
|
+
walk(ast, [], (node) => {
|
|
252
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { if (node.id) b.add(node.id.name); (node.params || []).forEach(addPat); }
|
|
253
|
+
else if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') { if (node.id) b.add(node.id.name); }
|
|
254
|
+
else if (node.type === 'VariableDeclarator') addPat(node.id);
|
|
255
|
+
else if (node.type === 'ImportDefaultSpecifier' || node.type === 'ImportNamespaceSpecifier' || node.type === 'ImportSpecifier') { if (node.local) b.add(node.local.name); }
|
|
256
|
+
else if (node.type === 'CatchClause') addPat(node.param);
|
|
257
|
+
});
|
|
258
|
+
return [...b];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// The unit a Cell governs = the nearest unit starting just after the marker.
|
|
262
|
+
function nearestUnitAfter(units, afterLine, gap = 6) {
|
|
263
|
+
for (const u of units) { // units are sorted ascending by startLine
|
|
264
|
+
if (u.startLine > afterLine && u.startLine - afterLine <= gap) return u;
|
|
265
|
+
if (u.startLine > afterLine + gap) break;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = { analyze, nearestUnitAfter };
|