@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.
@@ -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) &&
@@ -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
- aggregate file/check counts. Portable gaps and files outside parser coverage stay
94
- visible under a collapsed coverage section without filling the timeline with paths.
95
- The generated `powershot.manifest.json` keeps the per-file accounting for workflows
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.1.4",
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>",