@0xcraft/powershot 1.1.4 → 1.1.5

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.
@@ -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
- * In tree-sitter-rust and -go a `string_literal` has exactly two children, the quotes,
4
- * and the text between them belongs to no node so walking to leaves loses it, and
5
- * two files differing only in what their strings say tokenize identically. Atomic.
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
- export function tokensFor(root, pack) {
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
- /** Module level only: two classes sharing a method name is polymorphism. */
82
- export function topLevelDeclarations(root, pack) {
83
- const all = nodesOfType(root, pack.nodes.declaration);
84
- const out = new Map();
85
- for (const decl of all) {
86
- const nested = all.some((other) => other !== decl &&
87
- other.startPosition.row <= decl.startPosition.row &&
88
- other.endPosition.row >= decl.endPosition.row);
89
- if (nested)
90
- continue;
91
- const name = declaredName(decl, pack);
92
- if (name)
93
- out.set(name, decl);
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
- return out;
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) &&
@@ -151,6 +151,31 @@ Python dependency grounding follows the same rule. Local modules are discovered
151
151
  direct entries on each changed file's ancestor chain and conventional `src`, `lib`, or
152
152
  `python` roots. It never recursively crawls an unrelated monorepo tree.
153
153
 
154
+ ### Proven findings have one proof shape
155
+
156
+ A syntax oracle abstains when a refactor falls outside the exact fact it can prove.
157
+ `dropped-guard` pairs stable module bindings, owner, bodyless callable contract, and
158
+ structural block path. It then removes only the candidate guard nodes from the old file
159
+ and requires every remaining compiler-visible token to equal the new file. Any other
160
+ executable change in a supported source file also makes the check abstain, because the
161
+ guard may have moved across that boundary. An import, type, ancestor condition, sibling
162
+ statement, helper extraction, or moved continuation therefore cannot become a
163
+ HIGH/proven defect. The check also abstains when the same token-normalized guard remains
164
+ elsewhere in the callable. The changed-source index is built once from the complete diff
165
+ inventory, including renamed, deleted, and policy-waived paths, so the proof stays linear
166
+ in a large monorepo instead of rescanning the change for every callable.
167
+
168
+ `reinvented` follows only language-declared module wrappers, includes decorators and
169
+ templates in the token fingerprint, and keeps package and namespace identity separate.
170
+ Its syntax evidence does not claim semantic accessibility: it requires matching declared
171
+ visibility, wrappers, imports, referenced module bindings, conditional compilation, and
172
+ relative binding directory before calling a declaration plausibly reusable.
173
+ Go also requires the same source directory because its import path, not its package
174
+ clause alone, defines cross-file reuse. Anonymous namespaces and file-private
175
+ declarations are excluded. Type bodies, receiver implementations, and nested test
176
+ support are not treated as interchangeable module-level alternatives. Ambiguous
177
+ identities stay available to the optional judge instead of becoming verified facts.
178
+
154
179
  ### Grammar memory is isolated by language
155
180
 
156
181
  Tree-sitter WASM compilation outlives its JavaScript parser objects. Keeping every
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xcraft/powershot",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
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>",