@0xcraft/powershot 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -9
- package/dist/cli/args.js +2 -1
- package/dist/cli/review-command.js +30 -12
- package/dist/cli/session-command.js +1 -0
- package/dist/delegate.js +89 -13
- package/dist/git.js +5 -3
- package/dist/ground.js +19 -12
- package/dist/lang/packs.js +170 -8
- package/dist/langtest.js +1056 -6
- package/dist/plan.js +25 -0
- package/dist/reinvention.js +227 -2
- package/dist/report/sarif.js +2 -2
- package/dist/report/summary.js +12 -4
- package/dist/review.js +5 -13
- package/dist/selftest.js +507 -17
- package/dist/session.js +1 -0
- package/dist/verifiers/dropped-guard.js +188 -84
- package/dist/verifiers/foreign-dropped-guard.js +174 -50
- package/dist/verifiers/foreign-reinvented.js +22 -7
- package/dist/verifiers/foreign-tokens.js +228 -19
- package/dist/verifiers/guard-diff.js +138 -0
- package/dist/verifiers/reinvented.js +7 -5
- package/docs/architecture.md +33 -0
- package/docs/ci.md +9 -4
- package/package.json +1 -1
|
@@ -1,14 +1,28 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
1
2
|
import { nodesOfType, walk } from '#app/lang/packs.js';
|
|
3
|
+
import { implementationFingerprint } from '#app/reinvention.js';
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
* and
|
|
5
|
-
*
|
|
5
|
+
* Some wasm grammars leave meaningful text outside child nodes: Rust/Go string
|
|
6
|
+
* contents and Kotlin's nullable `?` are examples. Keep those enclosing nodes atomic
|
|
7
|
+
* so a value or contract change cannot disappear from the fingerprint.
|
|
6
8
|
*/
|
|
7
|
-
const ATOMIC = /string|char|raw_|heredoc|interpolat/;
|
|
9
|
+
const ATOMIC = /string|char|raw_|heredoc|interpolat|nullable_type/;
|
|
8
10
|
/** The token stream: what a compiler sees, minus layout and comments. */
|
|
9
|
-
|
|
11
|
+
function nodeKey(node) {
|
|
12
|
+
return [
|
|
13
|
+
node.type,
|
|
14
|
+
node.startPosition.row,
|
|
15
|
+
node.startPosition.column,
|
|
16
|
+
node.endPosition.row,
|
|
17
|
+
node.endPosition.column,
|
|
18
|
+
].join(':');
|
|
19
|
+
}
|
|
20
|
+
export function tokensFor(root, pack, exclude) {
|
|
10
21
|
const out = [];
|
|
22
|
+
const excluded = new Set((exclude === undefined ? [] : Array.isArray(exclude) ? exclude : [exclude]).map(nodeKey));
|
|
11
23
|
const visit = (n) => {
|
|
24
|
+
if (excluded.has(nodeKey(n)))
|
|
25
|
+
return;
|
|
12
26
|
if (pack.nodes.comment.includes(n.type))
|
|
13
27
|
return;
|
|
14
28
|
if (ATOMIC.test(n.type)) {
|
|
@@ -29,6 +43,13 @@ export function tokensFor(root, pack) {
|
|
|
29
43
|
visit(root);
|
|
30
44
|
return out;
|
|
31
45
|
}
|
|
46
|
+
/** Tokens that affect compilation, including comment-shaped file constraints. */
|
|
47
|
+
export function sourceTokensFor(root, pack, exclude) {
|
|
48
|
+
return [
|
|
49
|
+
...(pack.fileConstraints?.(root) ?? []).map((text) => ({ type: '__file_constraint__', text })),
|
|
50
|
+
...tokensFor(root, pack, exclude),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
32
53
|
export function commentText(root, pack) {
|
|
33
54
|
const parts = [];
|
|
34
55
|
walk(root, (n) => {
|
|
@@ -52,6 +73,16 @@ export function documentsSomething(root, pack) {
|
|
|
52
73
|
export function same(a, b) {
|
|
53
74
|
return a.length === b.length && a.every((t, i) => t.type === b[i].type && t.text === b[i].text);
|
|
54
75
|
}
|
|
76
|
+
/** Layout-insensitive identity of a file's declared package/module, when it has one. */
|
|
77
|
+
export function fileScopeIdentity(root, pack) {
|
|
78
|
+
const tokens = root.namedChildren
|
|
79
|
+
.filter((child) => pack.nodes.fileScope.includes(child.type))
|
|
80
|
+
.flatMap((child) => tokensFor(child, pack));
|
|
81
|
+
const constraints = pack.fileConstraints?.(root) ?? [];
|
|
82
|
+
return tokens.length === 0 && constraints.length === 0
|
|
83
|
+
? undefined
|
|
84
|
+
: 'file:' + JSON.stringify({ tokens, constraints });
|
|
85
|
+
}
|
|
55
86
|
export function finding(file, node, f) {
|
|
56
87
|
return {
|
|
57
88
|
id: '',
|
|
@@ -78,20 +109,198 @@ export function declaredName(decl, pack) {
|
|
|
78
109
|
// C and C++ wrap the name in a declarator; look inside that, never wider
|
|
79
110
|
return nodesOfType(field, pack.nodes.identifier)[0]?.text;
|
|
80
111
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
112
|
+
export function reusableDeclarations(root, pack, bindingPath = '') {
|
|
113
|
+
const raw = [];
|
|
114
|
+
const contexts = [];
|
|
115
|
+
const bindings = [];
|
|
116
|
+
const namesIn = (tokens) => [
|
|
117
|
+
...new Set(tokens.filter((token) => pack.nodes.bindingIdentifier.includes(token.type)).map((token) => token.text)),
|
|
118
|
+
];
|
|
119
|
+
const addBinding = (scope, node, tokens, indexNames) => {
|
|
120
|
+
if (indexNames.length > 0) {
|
|
121
|
+
bindings.push({
|
|
122
|
+
node,
|
|
123
|
+
tokens,
|
|
124
|
+
scope,
|
|
125
|
+
indexNames,
|
|
126
|
+
references: namesIn(tokens),
|
|
127
|
+
fingerprint: implementationFingerprint([
|
|
128
|
+
{ type: '__binding_scope__', text: JSON.stringify(scope) },
|
|
129
|
+
...tokens,
|
|
130
|
+
]),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const visit = (container, scope, fingerprintRoot) => {
|
|
135
|
+
let prefix = [];
|
|
136
|
+
let blocked = false;
|
|
137
|
+
for (const child of container.namedChildren) {
|
|
138
|
+
if (pack.nodes.comment.includes(child.type))
|
|
139
|
+
continue;
|
|
140
|
+
if (pack.nodes.reusablePrefix.includes(child.type)) {
|
|
141
|
+
prefix.push(child);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (pack.nodes.reusableBlocker.includes(child.type)) {
|
|
145
|
+
blocked = true;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (pack.nodes.bindingContext.includes(child.type)) {
|
|
149
|
+
contexts.push({ scope, node: child, tokens: tokensFor(child, pack) });
|
|
150
|
+
prefix = [];
|
|
151
|
+
blocked = false;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (pack.nodes.reusableDeclaration.includes(child.type)) {
|
|
155
|
+
if (blocked || (pack.reusableAcrossFiles && !pack.reusableAcrossFiles(child))) {
|
|
156
|
+
prefix = [];
|
|
157
|
+
blocked = false;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const name = declaredName(child, pack);
|
|
161
|
+
if (!name) {
|
|
162
|
+
if (pack.nodes.bindingDeclaration.includes(child.type)) {
|
|
163
|
+
const bindingTokens = tokensFor(child, pack);
|
|
164
|
+
addBinding(scope, child, bindingTokens, namesIn(bindingTokens));
|
|
165
|
+
}
|
|
166
|
+
prefix = [];
|
|
167
|
+
blocked = false;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const ownTokens = [
|
|
171
|
+
...prefix.flatMap((node) => tokensFor(node, pack)),
|
|
172
|
+
...tokensFor(fingerprintRoot ?? child, pack),
|
|
173
|
+
];
|
|
174
|
+
raw.push({
|
|
175
|
+
scope,
|
|
176
|
+
name,
|
|
177
|
+
node: child,
|
|
178
|
+
tokens: ownTokens,
|
|
179
|
+
});
|
|
180
|
+
addBinding(scope, child, ownTokens, [name]);
|
|
181
|
+
prefix = [];
|
|
182
|
+
blocked = false;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (pack.nodes.bindingDeclaration.includes(child.type)) {
|
|
186
|
+
const bindingTokens = tokensFor(child, pack);
|
|
187
|
+
addBinding(scope, child, bindingTokens, namesIn(bindingTokens));
|
|
188
|
+
prefix = [];
|
|
189
|
+
blocked = false;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!pack.nodes.reusableContainer.includes(child.type)) {
|
|
193
|
+
prefix = [];
|
|
194
|
+
blocked = false;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
let nextScope = scope;
|
|
198
|
+
if (pack.nodes.reusableScope.includes(child.type)) {
|
|
199
|
+
const name = child.childForFieldName('name')?.text;
|
|
200
|
+
// An anonymous namespace cannot provide a cross-file reuse candidate.
|
|
201
|
+
if (!name)
|
|
202
|
+
continue;
|
|
203
|
+
nextScope = [...scope, name];
|
|
204
|
+
}
|
|
205
|
+
visit(child, nextScope, fingerprintRoot ?? (pack.nodes.reusableWrapper.includes(child.type) ? child : undefined));
|
|
206
|
+
prefix = [];
|
|
207
|
+
blocked = false;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const fileScope = fileScopeIdentity(root, pack);
|
|
211
|
+
visit(root, fileScope ? [fileScope] : []);
|
|
212
|
+
const scopeKey = (scope) => JSON.stringify(scope);
|
|
213
|
+
const scopePrefixes = (scope) => Array.from({ length: scope.length + 1 }, (_, length) => scopeKey(scope.slice(0, length)));
|
|
214
|
+
const byName = new Map();
|
|
215
|
+
for (const binding of bindings) {
|
|
216
|
+
for (const name of binding.indexNames) {
|
|
217
|
+
const byScope = byName.get(name) ?? new Map();
|
|
218
|
+
const scoped = byScope.get(scopeKey(binding.scope)) ?? [];
|
|
219
|
+
scoped.push(binding);
|
|
220
|
+
byScope.set(scopeKey(binding.scope), scoped);
|
|
221
|
+
byName.set(name, byScope);
|
|
222
|
+
}
|
|
94
223
|
}
|
|
95
|
-
|
|
224
|
+
const needsBindingDirectory = (tokens) => {
|
|
225
|
+
const text = tokens.map((token) => token.text).join(' ');
|
|
226
|
+
return /\brequire_relative\b|\b(?:require|include)(?:_once)?\b|\bfrom\s+\.+|["']\.\.?\/|#\s*include\s*"|\buse\s+(?:self|super)\s*::|\bmod\s+[A-Za-z_]\w*\s*;/.test(text);
|
|
227
|
+
};
|
|
228
|
+
const contextsByScope = new Map();
|
|
229
|
+
for (const context of contexts) {
|
|
230
|
+
const scoped = contextsByScope.get(scopeKey(context.scope)) ?? [];
|
|
231
|
+
scoped.push(context);
|
|
232
|
+
contextsByScope.set(scopeKey(context.scope), scoped);
|
|
233
|
+
}
|
|
234
|
+
const contextCache = new Map();
|
|
235
|
+
const contextFor = (scope) => {
|
|
236
|
+
const key = scopeKey(scope);
|
|
237
|
+
const known = contextCache.get(key);
|
|
238
|
+
if (known)
|
|
239
|
+
return known;
|
|
240
|
+
const visibleContexts = scopePrefixes(scope)
|
|
241
|
+
.flatMap((prefix) => contextsByScope.get(prefix) ?? [])
|
|
242
|
+
.sort((left, right) => left.node.startPosition.row - right.node.startPosition.row ||
|
|
243
|
+
left.node.startPosition.column - right.node.startPosition.column);
|
|
244
|
+
const ordered = visibleContexts.flatMap((context) => [
|
|
245
|
+
{ type: '__binding_scope__', text: JSON.stringify(context.scope) },
|
|
246
|
+
...context.tokens,
|
|
247
|
+
]);
|
|
248
|
+
const value = {
|
|
249
|
+
tokens: ordered.length === 0
|
|
250
|
+
? []
|
|
251
|
+
: [{ type: '__binding_context__', text: implementationFingerprint(ordered) }],
|
|
252
|
+
needsDirectory: visibleContexts.some((context) => needsBindingDirectory(context.tokens)),
|
|
253
|
+
};
|
|
254
|
+
contextCache.set(key, value);
|
|
255
|
+
return value;
|
|
256
|
+
};
|
|
257
|
+
const identity = (node) => [
|
|
258
|
+
node.type,
|
|
259
|
+
node.startPosition.row,
|
|
260
|
+
node.startPosition.column,
|
|
261
|
+
node.endPosition.row,
|
|
262
|
+
node.endPosition.column,
|
|
263
|
+
].join(':');
|
|
264
|
+
return raw.map((declaration) => {
|
|
265
|
+
const context = contextFor(declaration.scope);
|
|
266
|
+
const selected = new Map();
|
|
267
|
+
const pending = namesIn(declaration.tokens);
|
|
268
|
+
const visitedNames = new Set();
|
|
269
|
+
const target = identity(declaration.node);
|
|
270
|
+
while (pending.length > 0) {
|
|
271
|
+
const name = pending.pop();
|
|
272
|
+
if (visitedNames.has(name))
|
|
273
|
+
continue;
|
|
274
|
+
visitedNames.add(name);
|
|
275
|
+
const byScope = byName.get(name);
|
|
276
|
+
for (const binding of scopePrefixes(declaration.scope).flatMap((prefix) => byScope?.get(prefix) ?? [])) {
|
|
277
|
+
const key = identity(binding.node);
|
|
278
|
+
if (key === target || selected.has(key))
|
|
279
|
+
continue;
|
|
280
|
+
selected.set(key, binding);
|
|
281
|
+
// More than this is a generated binding graph, not a helper a human can
|
|
282
|
+
// meaningfully reuse. Abstain instead of doing quadratic work on it.
|
|
283
|
+
if (selected.size > 512)
|
|
284
|
+
return [];
|
|
285
|
+
for (const referenced of binding.references) {
|
|
286
|
+
if (!visitedNames.has(referenced))
|
|
287
|
+
pending.push(referenced);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const bindingTokens = [...selected.values()]
|
|
292
|
+
.sort((left, right) => left.node.startPosition.row - right.node.startPosition.row ||
|
|
293
|
+
left.node.startPosition.column - right.node.startPosition.column)
|
|
294
|
+
.map((binding) => ({ type: '__binding__', text: binding.fingerprint }));
|
|
295
|
+
const directoryToken = bindingPath !== '' && (context.needsDirectory ||
|
|
296
|
+
needsBindingDirectory(declaration.tokens) ||
|
|
297
|
+
[...selected.values()].some((binding) => needsBindingDirectory(binding.tokens)))
|
|
298
|
+
? [{ type: '__binding_directory__', text: posix.dirname(bindingPath.replaceAll('\\', '/')) }]
|
|
299
|
+
: [];
|
|
300
|
+
return [{
|
|
301
|
+
...declaration,
|
|
302
|
+
tokens: [...directoryToken, ...context.tokens, ...bindingTokens, ...declaration.tokens],
|
|
303
|
+
}];
|
|
304
|
+
}).flat();
|
|
96
305
|
}
|
|
97
306
|
//# sourceMappingURL=foreign-tokens.js.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { packFor } from '#app/lang/packs.js';
|
|
2
|
+
import { implementationFingerprint, typescriptSourceFingerprint } from '#app/reinvention.js';
|
|
3
|
+
import { sourceTokensFor } from './foreign-tokens.js';
|
|
4
|
+
const NATIVE_SOURCE = /\.[cm]?[jt]sx?$/i;
|
|
5
|
+
const EXECUTABLE_CHANGES = new WeakMap();
|
|
6
|
+
function executableChanges(g) {
|
|
7
|
+
const known = EXECUTABLE_CHANGES.get(g);
|
|
8
|
+
if (known)
|
|
9
|
+
return known;
|
|
10
|
+
const changed = new Set();
|
|
11
|
+
const represented = new Set();
|
|
12
|
+
for (const file of g.files) {
|
|
13
|
+
represented.add(file.changed.path);
|
|
14
|
+
const after = typescriptSourceFingerprint(file.sf.getFullText());
|
|
15
|
+
const before = typescriptSourceFingerprint(file.before?.getFullText() ?? '');
|
|
16
|
+
if (!after || !before || after !== before || file.changed.beforePath !== undefined) {
|
|
17
|
+
changed.add(file.changed.path);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
for (const file of g.foreign) {
|
|
21
|
+
represented.add(file.path);
|
|
22
|
+
const after = implementationFingerprint(sourceTokensFor(file.tree.rootNode, file.pack));
|
|
23
|
+
const before = implementationFingerprint(file.beforeTree ? sourceTokensFor(file.beforeTree.rootNode, file.pack) : []);
|
|
24
|
+
if (after !== before || file.changed.beforePath !== undefined)
|
|
25
|
+
changed.add(file.path);
|
|
26
|
+
}
|
|
27
|
+
const supported = (path) => path !== undefined && (NATIVE_SOURCE.test(path) || packFor(path) !== undefined);
|
|
28
|
+
for (const file of g.inventory ?? g.changed) {
|
|
29
|
+
if (represented.has(file.path))
|
|
30
|
+
continue;
|
|
31
|
+
if (supported(file.path) || supported(file.beforePath))
|
|
32
|
+
changed.add(file.path);
|
|
33
|
+
}
|
|
34
|
+
EXECUTABLE_CHANGES.set(g, changed);
|
|
35
|
+
return changed;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A guard transfer into another changed file is outside a local syntax proof. Keep a
|
|
39
|
+
* HIGH/proven result only when every other supported source file is token-stable.
|
|
40
|
+
*/
|
|
41
|
+
export function hasOtherExecutableChange(g, currentPath) {
|
|
42
|
+
const changed = executableChanges(g);
|
|
43
|
+
return changed.size > (changed.has(currentPath) ? 1 : 0);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Return the guards removed by an otherwise token-identical block rewrite.
|
|
47
|
+
*
|
|
48
|
+
* This is deliberately a narrow proof. A helper extraction, inserted validation call,
|
|
49
|
+
* changed continuation, or moved statement makes the block ambiguous and produces no
|
|
50
|
+
* deterministic finding. Those changes need semantic judgement; a pre/post syntax
|
|
51
|
+
* oracle cannot honestly call them lost guards.
|
|
52
|
+
*/
|
|
53
|
+
function guardOnlyDeletion(before, after) {
|
|
54
|
+
const removed = [];
|
|
55
|
+
let left = 0;
|
|
56
|
+
let right = 0;
|
|
57
|
+
while (left < before.entries.length && right < after.entries.length) {
|
|
58
|
+
const old = before.entries[left];
|
|
59
|
+
const current = after.entries[right];
|
|
60
|
+
if (old.fingerprint === current.fingerprint) {
|
|
61
|
+
left++;
|
|
62
|
+
right++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (old.guard !== undefined) {
|
|
66
|
+
// A guard only protects a continuation in its own block. If it is the last
|
|
67
|
+
// meaningful statement, deleting it is not the failure this check promises.
|
|
68
|
+
if (before.entries.slice(left + 1).some((entry) => entry.guard === undefined)) {
|
|
69
|
+
removed.push({ id: old.id, ...old.guard });
|
|
70
|
+
}
|
|
71
|
+
left++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (right !== after.entries.length)
|
|
77
|
+
return undefined;
|
|
78
|
+
while (left < before.entries.length) {
|
|
79
|
+
const old = before.entries[left];
|
|
80
|
+
if (old.guard === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (before.entries.slice(left + 1).some((entry) => entry.guard === undefined)) {
|
|
83
|
+
removed.push({ id: old.id, ...old.guard });
|
|
84
|
+
}
|
|
85
|
+
left++;
|
|
86
|
+
}
|
|
87
|
+
return removed.length > 0 ? removed : undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Guards whose removal is the only executable change in a uniquely matched block.
|
|
91
|
+
* Ambiguous duplicate blocks are skipped rather than paired by traversal order.
|
|
92
|
+
*/
|
|
93
|
+
export function provenGuardRemovals(before, after) {
|
|
94
|
+
// A name is not a callable contract. Changing a parameter, receiver, owner, or
|
|
95
|
+
// return type can make a formerly necessary guard obsolete.
|
|
96
|
+
if (before.identity !== after.identity)
|
|
97
|
+
return [];
|
|
98
|
+
const uniqueByPath = (blocks) => {
|
|
99
|
+
const out = new Map();
|
|
100
|
+
for (const block of blocks) {
|
|
101
|
+
if (out.has(block.path))
|
|
102
|
+
out.set(block.path, undefined);
|
|
103
|
+
else
|
|
104
|
+
out.set(block.path, block);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
};
|
|
108
|
+
const current = uniqueByPath(after.blocks);
|
|
109
|
+
const remainingGuards = new Set(after.blocks.flatMap((block) => block.entries.flatMap((entry) => entry.guard === undefined ? [] : [entry.guard.key])));
|
|
110
|
+
const structurallyRemoved = [];
|
|
111
|
+
for (const [path, previous] of uniqueByPath(before.blocks)) {
|
|
112
|
+
const now = current.get(path);
|
|
113
|
+
if (!previous || !now)
|
|
114
|
+
continue;
|
|
115
|
+
structurallyRemoved.push(...(guardOnlyDeletion(previous, now) ?? []));
|
|
116
|
+
}
|
|
117
|
+
if (structurallyRemoved.length === 0)
|
|
118
|
+
return [];
|
|
119
|
+
// A block-local diff is not enough: another branch or sibling statement may have
|
|
120
|
+
// changed the guarantee that made the guard obsolete. Remove the candidate guard
|
|
121
|
+
// nodes from the old callable and require every remaining compiler-visible token
|
|
122
|
+
// to equal the new callable. This is the fact behind the `proven` confidence.
|
|
123
|
+
const omitted = new Set(structurallyRemoved.map((guard) => guard.id));
|
|
124
|
+
const previousResidual = before.residualFingerprint(omitted);
|
|
125
|
+
const currentResidual = after.residualFingerprint(new Set());
|
|
126
|
+
if (!previousResidual || previousResidual !== currentResidual)
|
|
127
|
+
return [];
|
|
128
|
+
const reportable = new Map();
|
|
129
|
+
for (const guard of structurallyRemoved) {
|
|
130
|
+
// A syntactically identical guard elsewhere in the callable may dominate the
|
|
131
|
+
// continuation. Without a control-flow graph, abstaining is the only proof-safe
|
|
132
|
+
// answer; false negatives are preferable to a false HIGH/proven defect.
|
|
133
|
+
if (!remainingGuards.has(guard.key))
|
|
134
|
+
reportable.set(guard.key, guard.label);
|
|
135
|
+
}
|
|
136
|
+
return [...reportable.values()];
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=guard-diff.js.map
|
|
@@ -11,12 +11,12 @@ const GENERIC = new Set([
|
|
|
11
11
|
'handler', 'handle', 'create', 'update', 'remove', 'delete', 'list', 'find',
|
|
12
12
|
'parse', 'format', 'load', 'save', 'toString', 'default', 'config', 'options',
|
|
13
13
|
]);
|
|
14
|
-
function declaredNames(sf) {
|
|
14
|
+
function declaredNames(sf, bindingPath) {
|
|
15
15
|
const out = [];
|
|
16
16
|
for (const fn of sf.getFunctions()) {
|
|
17
17
|
const name = fn.getName();
|
|
18
18
|
const id = fn.getNameNode();
|
|
19
|
-
const fingerprint = typescriptImplementationFingerprint(fn);
|
|
19
|
+
const fingerprint = typescriptImplementationFingerprint(fn, bindingPath);
|
|
20
20
|
if (name && id && fingerprint) {
|
|
21
21
|
out.push({ name, line: fn.getStartLineNumber(), span: locate(sf, id.getStart(), id.getWidth()).span, fingerprint });
|
|
22
22
|
}
|
|
@@ -27,7 +27,7 @@ function declaredNames(sf) {
|
|
|
27
27
|
continue;
|
|
28
28
|
if (init.isKind(SyntaxKind.ArrowFunction) || init.isKind(SyntaxKind.FunctionExpression)) {
|
|
29
29
|
const id = v.getNameNode();
|
|
30
|
-
const fingerprint = typescriptImplementationFingerprint(v);
|
|
30
|
+
const fingerprint = typescriptImplementationFingerprint(v, bindingPath);
|
|
31
31
|
if (fingerprint) {
|
|
32
32
|
out.push({ name: v.getName(), line: v.getStartLineNumber(), span: locate(sf, id.getStart(), id.getWidth()).span, fingerprint });
|
|
33
33
|
}
|
|
@@ -49,10 +49,12 @@ export const reinvented = {
|
|
|
49
49
|
// tests describing the same scenario naturally share a name
|
|
50
50
|
if (TEST_FILE.test(file))
|
|
51
51
|
continue;
|
|
52
|
-
const baseDeclarations = before ? declaredNames(before) : [];
|
|
53
|
-
for (const { name, line, span, fingerprint } of declaredNames(sf)) {
|
|
52
|
+
const baseDeclarations = before ? declaredNames(before, changed.beforePath ?? file) : [];
|
|
53
|
+
for (const { name, line, span, fingerprint } of declaredNames(sf, file)) {
|
|
54
54
|
if (!changed.added.has(line))
|
|
55
55
|
continue;
|
|
56
|
+
if (changed.beforePath && changed.beforePath !== changed.path)
|
|
57
|
+
continue;
|
|
56
58
|
if (name.length < 6 || GENERIC.has(name) || GENERIC.has(name.toLowerCase()))
|
|
57
59
|
continue;
|
|
58
60
|
const existedHere = baseDeclarations.some((declaration) => normalizeName(declaration.name) === normalizeName(name) &&
|
package/docs/architecture.md
CHANGED
|
@@ -121,6 +121,14 @@ A branch or commit review reads source from the target revision, not from whatev
|
|
|
121
121
|
currently present in the working directory. Grounding, verification, bundling, and
|
|
122
122
|
positioning all receive the same tree.
|
|
123
123
|
|
|
124
|
+
### Delegation shares file selection
|
|
125
|
+
|
|
126
|
+
`psh delegate` builds the same target snapshot, `SelectionPlan`, and parser ground as
|
|
127
|
+
`psh review`, then stops before deterministic verification, model calls, sessions, or
|
|
128
|
+
manifests. Its Markdown brief and `powershot.delegate/v1` JSON are renderings of one
|
|
129
|
+
task object. Both therefore expose the same selected, waived, and failed files and the
|
|
130
|
+
same bounded judge units; an output adapter cannot silently choose a different scope.
|
|
131
|
+
|
|
124
132
|
### Capabilities belong to files
|
|
125
133
|
|
|
126
134
|
A run can contain a typed TypeScript file beside a Python file or a TypeScript file
|
|
@@ -151,6 +159,31 @@ Python dependency grounding follows the same rule. Local modules are discovered
|
|
|
151
159
|
direct entries on each changed file's ancestor chain and conventional `src`, `lib`, or
|
|
152
160
|
`python` roots. It never recursively crawls an unrelated monorepo tree.
|
|
153
161
|
|
|
162
|
+
### Proven findings have one proof shape
|
|
163
|
+
|
|
164
|
+
A syntax oracle abstains when a refactor falls outside the exact fact it can prove.
|
|
165
|
+
`dropped-guard` pairs stable module bindings, owner, bodyless callable contract, and
|
|
166
|
+
structural block path. It then removes only the candidate guard nodes from the old file
|
|
167
|
+
and requires every remaining compiler-visible token to equal the new file. Any other
|
|
168
|
+
executable change in a supported source file also makes the check abstain, because the
|
|
169
|
+
guard may have moved across that boundary. An import, type, ancestor condition, sibling
|
|
170
|
+
statement, helper extraction, or moved continuation therefore cannot become a
|
|
171
|
+
HIGH/proven defect. The check also abstains when the same token-normalized guard remains
|
|
172
|
+
elsewhere in the callable. The changed-source index is built once from the complete diff
|
|
173
|
+
inventory, including renamed, deleted, and policy-waived paths, so the proof stays linear
|
|
174
|
+
in a large monorepo instead of rescanning the change for every callable.
|
|
175
|
+
|
|
176
|
+
`reinvented` follows only language-declared module wrappers, includes decorators and
|
|
177
|
+
templates in the token fingerprint, and keeps package and namespace identity separate.
|
|
178
|
+
Its syntax evidence does not claim semantic accessibility: it requires matching declared
|
|
179
|
+
visibility, wrappers, imports, referenced module bindings, conditional compilation, and
|
|
180
|
+
relative binding directory before calling a declaration plausibly reusable.
|
|
181
|
+
Go also requires the same source directory because its import path, not its package
|
|
182
|
+
clause alone, defines cross-file reuse. Anonymous namespaces and file-private
|
|
183
|
+
declarations are excluded. Type bodies, receiver implementations, and nested test
|
|
184
|
+
support are not treated as interchangeable module-level alternatives. Ambiguous
|
|
185
|
+
identities stay available to the optional judge instead of becoming verified facts.
|
|
186
|
+
|
|
154
187
|
### Grammar memory is isolated by language
|
|
155
188
|
|
|
156
189
|
Tree-sitter WASM compilation outlives its JavaScript parser objects. Keeping every
|
package/docs/ci.md
CHANGED
|
@@ -90,10 +90,15 @@ newest unmarked legacy `## PowerShot` summary from v1.1.2 or older without claim
|
|
|
90
90
|
ambiguous legacy comment through `PATCH`.
|
|
91
91
|
|
|
92
92
|
The comment leads with the verdict, effective severity threshold, review mode, and
|
|
93
|
-
|
|
94
|
-
visible under a collapsed coverage section without filling the timeline
|
|
95
|
-
The generated `powershot.manifest.json` keeps the per-file accounting for
|
|
96
|
-
that want to persist it as an artifact.
|
|
93
|
+
reviewed/changed file ratio and check count. Portable gaps and files outside parser
|
|
94
|
+
coverage stay visible under a collapsed coverage section without filling the timeline
|
|
95
|
+
with paths. The generated `powershot.manifest.json` keeps the per-file accounting for
|
|
96
|
+
workflows that want to persist it as an artifact.
|
|
97
|
+
|
|
98
|
+
For a `pull_request` from a fork, GitHub gives the workflow token read-only pull-request
|
|
99
|
+
permissions. PowerShot still runs and writes the complete report to the job summary,
|
|
100
|
+
but skips summary comments, inline comments, and approval because those operations
|
|
101
|
+
require a write-capable token.
|
|
97
102
|
|
|
98
103
|
PowerShot checks the target head throughout reconciliation and removes its own
|
|
99
104
|
just-created candidate if it observes a changed head. Simultaneous same-head runs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xcraft/powershot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Oracle-first code review for machine-written code, with deterministic verification and CI-ready reports.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "aglumova <alina.glumova@gmail.com>",
|