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.
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+ // UNDECLARED-INPUT PREDICATE PROVENANCE. A branch condition IS behaviour, and behaviour must trace to
3
+ // the signed spec. This flags a branch that keys off a FUNCTION PARAMETER the Cell's `in:` never
4
+ // declares — e.g. `if (mode === 'admin')` in a unit whose in: lists only `items`. That is the
5
+ // hidden-mode / undeclared-control-input shape the other checks structurally miss: the branch DOES
6
+ // something (so inertness stays quiet), IS exercised (so coverage is happy), and has no magic constant
7
+ // (so literal-seeding finds nothing) — yet it depends on an input the human never signed.
8
+ //
9
+ // It is the third prong of the pincer: inertness = branch does nothing; literal-seeding = magic-constant
10
+ // gate; THIS = branch keys off an undeclared input.
11
+ //
12
+ // Deliberately conservative (under-flag, never cry wolf):
13
+ // • only SIMPLE identifier params count (destructuring / rest params are skipped — can't map cleanly);
14
+ // • member-property names are NOT reads (`opts.mode` contributes `opts`, never `mode`);
15
+ // • if @babel/parser is absent or the source won't parse, it returns null (no finding), never an error.
16
+ // Default severity is Yellow and always DECLARABLE — the fix is to add the input to `in:` (or a
17
+ // `throws:` / `ensures` case), which is the spec-strengthening loop working. Owner-signed policy
18
+ // (`predicate: declared`) can escalate it to gate-blocking for sensitive Cells.
19
+
20
+ let parser = null;
21
+ try { parser = require('@babel/parser'); } catch (_) { parser = null; }
22
+
23
+ function parseAst(source, opts) {
24
+ if (!parser) return null;
25
+ const plugins = [];
26
+ if (opts.ts) plugins.push('typescript');
27
+ if (opts.jsx) plugins.push('jsx');
28
+ for (const mode of ['module', 'script']) {
29
+ try { return parser.parse(source, { sourceType: mode, errorRecovery: true, plugins }); } catch (_) { /* try next */ }
30
+ }
31
+ return null;
32
+ }
33
+
34
+ // Collect identifier "reads" in an expression subtree, skipping non-computed member property names and
35
+ // object-literal keys — so `opts.mode` contributes `opts` (the input), not `mode` (a field name).
36
+ function readIdents(node, out) {
37
+ if (!node || typeof node.type !== 'string') return;
38
+ switch (node.type) {
39
+ case 'Identifier': out.add(node.name); return;
40
+ case 'MemberExpression':
41
+ case 'OptionalMemberExpression':
42
+ readIdents(node.object, out);
43
+ if (node.computed) readIdents(node.property, out); // a[b] reads b; a.b does not
44
+ return;
45
+ case 'ObjectProperty':
46
+ case 'ObjectMethod':
47
+ case 'Property':
48
+ if (node.computed) readIdents(node.key, out);
49
+ if (node.value) readIdents(node.value, out);
50
+ return;
51
+ default: break;
52
+ }
53
+ for (const k in node) {
54
+ if (k === 'loc' || k === 'start' || k === 'end' || k === 'range' || k === 'comments' || k === 'leadingComments' || k === 'trailingComments') continue;
55
+ const v = node[k];
56
+ if (Array.isArray(v)) { for (const c of v) readIdents(c, out); }
57
+ else if (v && typeof v.type === 'string') readIdents(v, out);
58
+ }
59
+ }
60
+
61
+ // Find the unit's function node by name: `function name(){}`, `const name = (…) =>{}`, `const name = function(){}`.
62
+ function findFn(ast, name) {
63
+ let found = null;
64
+ (function visit(node) {
65
+ if (!node || found || typeof node.type !== 'string') return;
66
+ if (node.type === 'FunctionDeclaration' && node.id && node.id.name === name) { found = node; return; }
67
+ if (node.type === 'VariableDeclarator' && node.id && node.id.name === name && node.init &&
68
+ (node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression')) { found = node.init; return; }
69
+ for (const k in node) {
70
+ if (found) return;
71
+ const v = node[k];
72
+ if (Array.isArray(v)) { for (const c of v) { if (c && typeof c.type === 'string') visit(c); } }
73
+ else if (v && typeof v.type === 'string') visit(v);
74
+ }
75
+ })(ast.program || ast);
76
+ return found;
77
+ }
78
+
79
+ // Simple identifier params (incl. default params `mode = 'user'`). Destructuring / rest are skipped.
80
+ function simpleParams(fn) {
81
+ const names = [];
82
+ for (const p of (fn.params || [])) {
83
+ if (p.type === 'Identifier') names.push(p.name);
84
+ else if (p.type === 'AssignmentPattern' && p.left && p.left.type === 'Identifier') names.push(p.left.name);
85
+ }
86
+ return names;
87
+ }
88
+
89
+ const TEST_OF = { IfStatement: 'test', ConditionalExpression: 'test', WhileStatement: 'test', DoWhileStatement: 'test', SwitchStatement: 'discriminant' };
90
+
91
+ function collectFindings(fn, undeclared, source) {
92
+ const undeclaredSet = new Set(undeclared);
93
+ const findings = [];
94
+ const seen = new Set();
95
+ (function visit(node) {
96
+ if (!node || typeof node.type !== 'string') return;
97
+ const key = TEST_OF[node.type];
98
+ const test = key && node[key];
99
+ if (test) {
100
+ const reads = new Set();
101
+ readIdents(test, reads);
102
+ for (const nm of reads) {
103
+ if (!undeclaredSet.has(nm)) continue;
104
+ const line = (test.loc && test.loc.start.line) || (node.loc && node.loc.start.line) || 0;
105
+ const dk = nm + '@' + line;
106
+ if (seen.has(dk)) continue;
107
+ seen.add(dk);
108
+ let snippet = '';
109
+ try { snippet = source.slice(test.start, test.end).replace(/\s+/g, ' ').trim(); if (snippet.length > 80) snippet = snippet.slice(0, 77) + '…'; } catch (_) {}
110
+ findings.push({ line, name: nm, snippet });
111
+ }
112
+ }
113
+ for (const k in node) {
114
+ if (k === 'loc') continue;
115
+ const v = node[k];
116
+ if (Array.isArray(v)) { for (const c of v) { if (c && typeof c.type === 'string') visit(c); } }
117
+ else if (v && typeof v.type === 'string') visit(v);
118
+ }
119
+ })(fn.body || fn);
120
+ return findings;
121
+ }
122
+
123
+ // Analyze one Cell's unit. `declaredNames` = the names in the spec's `in:` (passed in to avoid a
124
+ // circular require on prove.js). Returns { findings:[{line,name,snippet}], undeclared:[names] } or null.
125
+ function analyzePredicates(source, cell, declaredNames, opts) {
126
+ opts = opts || {};
127
+ if (!parser || !cell || !cell.unitName) return null;
128
+ const file = opts.file || cell.file || '';
129
+ const ast = parseAst(source, { ts: /\.tsx?$/.test(file), jsx: !!opts.jsx || /\.(jsx|tsx)$/.test(file) });
130
+ if (!ast) return null;
131
+ const fn = findFn(ast, cell.unitName);
132
+ if (!fn) return null;
133
+ const params = simpleParams(fn);
134
+ if (!params.length) return null;
135
+ const declared = new Set(declaredNames || []);
136
+ const undeclared = params.filter((p) => !declared.has(p));
137
+ if (!undeclared.length) return null;
138
+ const findings = collectFindings(fn, undeclared, source);
139
+ if (!findings.length) return null;
140
+ return { findings, undeclared };
141
+ }
142
+
143
+ module.exports = { analyzePredicates };