@0xcraft/powershot 1.1.2 → 1.1.4
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 +4 -3
- package/dist/cli/reports.js +4 -6
- package/dist/cli/review-command.js +4 -7
- package/dist/cli/session-command.js +5 -1
- package/dist/github/api.js +198 -0
- package/dist/github/inline-comments.js +3 -154
- package/dist/github/summary-comment.js +149 -0
- package/dist/ground.js +49 -8
- package/dist/langtest.js +93 -1
- package/dist/manifest.js +0 -17
- package/dist/package-smoke.js +4 -0
- package/dist/reinvention.js +109 -0
- package/dist/report/markdown.js +28 -16
- package/dist/report/sarif.js +1 -1
- package/dist/report/summary.js +103 -0
- package/dist/report/terminal.js +18 -11
- package/dist/report/viewer.js +18 -9
- package/dist/selftest.js +594 -22
- package/dist/session.js +6 -2
- package/dist/verifiers/foreign-reinvented.js +72 -26
- package/dist/verifiers/foreign-tokens.js +4 -1
- package/dist/verifiers/reinvented.js +28 -10
- package/docs/architecture.md +2 -1
- package/docs/ci.md +25 -0
- package/examples/github-actions/action.yml +4 -0
- package/package.json +1 -1
package/dist/session.js
CHANGED
|
@@ -105,9 +105,13 @@ export class Session {
|
|
|
105
105
|
verified: findings.filter((f) => f.class === 'verified').length,
|
|
106
106
|
judged: findings.filter((f) => f.class === 'judged').length,
|
|
107
107
|
state: verdict.state,
|
|
108
|
-
notLookedAt: verdict.notLookedAt,
|
|
108
|
+
notLookedAt: [...verdict.notLookedAt],
|
|
109
109
|
coverage: verdict.coverage,
|
|
110
|
-
|
|
110
|
+
verifyOnly: verdict.verifyOnly,
|
|
111
|
+
minSeverity: verdict.minSeverity,
|
|
112
|
+
filesReviewed: verdict.filesReviewed,
|
|
113
|
+
deterministicChecks: verdict.deterministicChecks,
|
|
114
|
+
scopeDetails: verdict.scopeDetails ? [...verdict.scopeDetails] : undefined,
|
|
111
115
|
};
|
|
112
116
|
this.save();
|
|
113
117
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createReinventionScopeResolver, implementationFingerprint } from '#app/reinvention.js';
|
|
2
|
+
import { finding, tokensFor, topLevelDeclarations } from './foreign-tokens.js';
|
|
2
3
|
/** Names too common to mean anything across files, as in the TypeScript version. */
|
|
3
4
|
const GENERIC = new Set([
|
|
4
5
|
'render', 'handler', 'handle', 'create', 'update', 'remove', 'delete', 'insert',
|
|
@@ -14,6 +15,19 @@ const GENERIC = new Set([
|
|
|
14
15
|
* repository this was the single largest source of noise.
|
|
15
16
|
*/
|
|
16
17
|
const TESTISH = /(^|\/)(tests?|spec|__tests__)\/|(^|\/)(test_[^/]+|[^/]+_test|[^/]+\.(test|spec))\.[a-z]+$/;
|
|
18
|
+
function normalized(name) {
|
|
19
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
20
|
+
}
|
|
21
|
+
function declarationIndex(root, pack) {
|
|
22
|
+
const index = new Map();
|
|
23
|
+
for (const [name, node] of topLevelDeclarations(root, pack)) {
|
|
24
|
+
const key = normalized(name);
|
|
25
|
+
const list = index.get(key) ?? [];
|
|
26
|
+
list.push({ name, node, fingerprint: implementationFingerprint(tokensFor(node, pack)) });
|
|
27
|
+
index.set(key, list);
|
|
28
|
+
}
|
|
29
|
+
return index;
|
|
30
|
+
}
|
|
17
31
|
export const foreignReinvented = {
|
|
18
32
|
name: 'reinvented',
|
|
19
33
|
needs: ['syntax'],
|
|
@@ -24,39 +38,71 @@ export const foreignReinvented = {
|
|
|
24
38
|
// Keyed by language as well as name: a Ruby `charge` and a C++ `charge` are two
|
|
25
39
|
// unrelated functions that happen to share a word, and calling that duplication
|
|
26
40
|
// would be nonsense — nothing can be reused across the boundary anyway.
|
|
41
|
+
const scopeFor = createReinventionScopeResolver(g.root);
|
|
27
42
|
const index = new Map();
|
|
43
|
+
const declarations = new Map();
|
|
28
44
|
for (const file of g.foreign) {
|
|
29
|
-
|
|
45
|
+
declarations.set(file.path, {
|
|
46
|
+
current: declarationIndex(file.tree.rootNode, file.pack),
|
|
47
|
+
base: file.beforeTree ? declarationIndex(file.beforeTree.rootNode, file.pack) : undefined,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
for (const file of g.foreign) {
|
|
51
|
+
if (TESTISH.test(file.path) || !file.beforeTree)
|
|
30
52
|
continue;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
const fileDeclarations = declarations.get(file.path);
|
|
54
|
+
for (const [nameKey, baseDeclarations] of fileDeclarations.base ?? []) {
|
|
55
|
+
for (const baseDeclaration of baseDeclarations) {
|
|
56
|
+
const currentDeclaration = (fileDeclarations.current.get(nameKey) ?? []).find((declaration) => declaration.fingerprint === baseDeclaration.fingerprint);
|
|
57
|
+
// The reusable declaration must both predate the change and remain available
|
|
58
|
+
// at the reviewed head. A removed or rewritten helper is not a candidate.
|
|
59
|
+
if (!currentDeclaration)
|
|
60
|
+
continue;
|
|
61
|
+
const key = file.pack.name + '|' + nameKey;
|
|
62
|
+
const list = index.get(key) ?? [];
|
|
63
|
+
list.push({
|
|
64
|
+
file: file.path,
|
|
65
|
+
line: currentDeclaration.node.startPosition.row + 1,
|
|
66
|
+
fingerprint: baseDeclaration.fingerprint,
|
|
67
|
+
scope: scopeFor(file.path),
|
|
68
|
+
});
|
|
69
|
+
index.set(key, list);
|
|
70
|
+
}
|
|
36
71
|
}
|
|
37
72
|
}
|
|
38
73
|
for (const file of g.foreign) {
|
|
39
74
|
if (TESTISH.test(file.path))
|
|
40
75
|
continue;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
const fileDeclarations = declarations.get(file.path);
|
|
77
|
+
for (const [nameKey, currentDeclarations] of fileDeclarations.current) {
|
|
78
|
+
for (const { name, node: decl, fingerprint } of currentDeclarations) {
|
|
79
|
+
const line = decl.startPosition.row + 1;
|
|
80
|
+
if (!file.changed.added.has(line))
|
|
81
|
+
continue;
|
|
82
|
+
if (name.length < 6 || GENERIC.has(name.toLowerCase()))
|
|
83
|
+
continue;
|
|
84
|
+
const existedHere = (fileDeclarations.base?.get(nameKey) ?? []).some((baseDeclaration) => baseDeclaration.fingerprint === fingerprint);
|
|
85
|
+
if (existedHere)
|
|
86
|
+
continue;
|
|
87
|
+
const key = file.pack.name + '|' + nameKey;
|
|
88
|
+
const scope = scopeFor(file.path);
|
|
89
|
+
const match = (index.get(key) ?? []).find((candidate) => candidate.file !== file.path &&
|
|
90
|
+
candidate.scope === scope &&
|
|
91
|
+
candidate.fingerprint === fingerprint);
|
|
92
|
+
if (!match)
|
|
93
|
+
continue;
|
|
94
|
+
findings.push(finding(file, decl, {
|
|
95
|
+
check: 'reinvented',
|
|
96
|
+
severity: 'medium',
|
|
97
|
+
confidence: 'firm',
|
|
98
|
+
title: name + ' repeats the implementation at ' + match.file,
|
|
99
|
+
evidence: {
|
|
100
|
+
oracle: file.pack.name + ' base declaration + token fingerprint',
|
|
101
|
+
detail: 'token-identical implementation already present at ' + match.file + ':' + match.line,
|
|
102
|
+
},
|
|
103
|
+
fix: 'Consider reusing the existing declaration; if the separation is intentional, keep the boundary explicit',
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
60
106
|
}
|
|
61
107
|
}
|
|
62
108
|
return findings;
|
|
@@ -68,8 +68,11 @@ export function finding(file, node, f) {
|
|
|
68
68
|
*/
|
|
69
69
|
export function declaredName(decl, pack) {
|
|
70
70
|
const field = decl.childForFieldName(pack.nodes.declarationName);
|
|
71
|
+
// tree-sitter-kotlin exposes the declaration identifier as a named child but
|
|
72
|
+
// assigns no field name to it. Keep the grammar field authoritative when one is
|
|
73
|
+
// present; otherwise the first identifier inside the declaration is its name.
|
|
71
74
|
if (!field)
|
|
72
|
-
return
|
|
75
|
+
return nodesOfType(decl, pack.nodes.identifier)[0]?.text;
|
|
73
76
|
if (field.childCount === 0)
|
|
74
77
|
return field.text;
|
|
75
78
|
// C and C++ wrap the name in a declarator; look inside that, never wider
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SyntaxKind } from 'ts-morph';
|
|
2
2
|
import { locate, normalizeName, relPath } from '#app/ground.js';
|
|
3
|
+
import { createReinventionScopeResolver, typescriptImplementationFingerprint } from '#app/reinvention.js';
|
|
3
4
|
/**
|
|
4
5
|
* Names too generic to mean anything across files — two `render`s are usually
|
|
5
6
|
* two different things, not a duplication.
|
|
@@ -15,8 +16,10 @@ function declaredNames(sf) {
|
|
|
15
16
|
for (const fn of sf.getFunctions()) {
|
|
16
17
|
const name = fn.getName();
|
|
17
18
|
const id = fn.getNameNode();
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const fingerprint = typescriptImplementationFingerprint(fn);
|
|
20
|
+
if (name && id && fingerprint) {
|
|
21
|
+
out.push({ name, line: fn.getStartLineNumber(), span: locate(sf, id.getStart(), id.getWidth()).span, fingerprint });
|
|
22
|
+
}
|
|
20
23
|
}
|
|
21
24
|
for (const v of sf.getVariableDeclarations()) {
|
|
22
25
|
const init = v.getInitializer();
|
|
@@ -24,7 +27,10 @@ function declaredNames(sf) {
|
|
|
24
27
|
continue;
|
|
25
28
|
if (init.isKind(SyntaxKind.ArrowFunction) || init.isKind(SyntaxKind.FunctionExpression)) {
|
|
26
29
|
const id = v.getNameNode();
|
|
27
|
-
|
|
30
|
+
const fingerprint = typescriptImplementationFingerprint(v);
|
|
31
|
+
if (fingerprint) {
|
|
32
|
+
out.push({ name: v.getName(), line: v.getStartLineNumber(), span: locate(sf, id.getStart(), id.getWidth()).span, fingerprint });
|
|
33
|
+
}
|
|
28
34
|
}
|
|
29
35
|
}
|
|
30
36
|
return out;
|
|
@@ -35,19 +41,28 @@ export const reinvented = {
|
|
|
35
41
|
needs: ['syntax'],
|
|
36
42
|
run(g) {
|
|
37
43
|
const findings = [];
|
|
38
|
-
|
|
44
|
+
const scopeFor = createReinventionScopeResolver(g.root);
|
|
45
|
+
for (const { sf, changed, before } of g.files) {
|
|
39
46
|
const file = relPath(sf, g.root);
|
|
47
|
+
const scope = scopeFor(file);
|
|
40
48
|
// a fixture builder repeated across test files is a deliberate trade, and two
|
|
41
49
|
// tests describing the same scenario naturally share a name
|
|
42
50
|
if (TEST_FILE.test(file))
|
|
43
51
|
continue;
|
|
44
|
-
|
|
52
|
+
const baseDeclarations = before ? declaredNames(before) : [];
|
|
53
|
+
for (const { name, line, span, fingerprint } of declaredNames(sf)) {
|
|
45
54
|
if (!changed.added.has(line))
|
|
46
55
|
continue;
|
|
47
56
|
if (name.length < 6 || GENERIC.has(name) || GENERIC.has(name.toLowerCase()))
|
|
48
57
|
continue;
|
|
49
|
-
const
|
|
50
|
-
|
|
58
|
+
const existedHere = baseDeclarations.some((declaration) => normalizeName(declaration.name) === normalizeName(name) &&
|
|
59
|
+
declaration.fingerprint === fingerprint);
|
|
60
|
+
if (existedHere)
|
|
61
|
+
continue;
|
|
62
|
+
const match = (g.symbolIndex.get(normalizeName(name)) ?? []).find((symbol) => symbol.file !== file &&
|
|
63
|
+
symbol.existedInBase &&
|
|
64
|
+
symbol.scope === scope &&
|
|
65
|
+
symbol.fingerprint === fingerprint);
|
|
51
66
|
if (!match)
|
|
52
67
|
continue;
|
|
53
68
|
findings.push({
|
|
@@ -59,12 +74,15 @@ export const reinvented = {
|
|
|
59
74
|
file,
|
|
60
75
|
line,
|
|
61
76
|
span,
|
|
62
|
-
title: name + '()
|
|
63
|
-
evidence: {
|
|
77
|
+
title: name + '() repeats the implementation at ' + match.file + ':' + match.name,
|
|
78
|
+
evidence: {
|
|
79
|
+
oracle: 'base export + callable token fingerprint',
|
|
80
|
+
detail: 'token-identical implementation already exported from ' + match.file + ':' + match.line,
|
|
81
|
+
},
|
|
64
82
|
// Deliberately not an import statement: the correct specifier depends on the
|
|
65
83
|
// repo's module resolution, and a wrong one would be exactly the kind of
|
|
66
84
|
// confidently-wrong output this tool exists to catch.
|
|
67
|
-
fix: '
|
|
85
|
+
fix: 'Consider reusing ' + match.name + ' from ' + match.file + '; if the separation is intentional, keep the boundary explicit',
|
|
68
86
|
});
|
|
69
87
|
}
|
|
70
88
|
}
|
package/docs/architecture.md
CHANGED
|
@@ -59,13 +59,14 @@ engine. The engine does not depend on a workflow provider or terminal layout.
|
|
|
59
59
|
|---|---|---|
|
|
60
60
|
| `src/cli/` | Argument parsing, command dispatch, report publication, exit mapping | Review algorithms |
|
|
61
61
|
| `src/review.ts` | One review run and its stage orchestration | CLI parsing or presentation |
|
|
62
|
-
| `src/ground.ts` | Change-scoped TypeScript projects, parse trees, manifests,
|
|
62
|
+
| `src/ground.ts` | Change-scoped TypeScript projects, parse trees, manifests, base-aware implementation index | Check selection |
|
|
63
63
|
| `src/plan.ts` | File selection and per-file capability accounting | Finding generation |
|
|
64
64
|
| `src/manifest.ts` | Completion state and the authoritative run record | Rendering |
|
|
65
65
|
| `src/verifiers/` | Deterministic check implementations | Model calls |
|
|
66
66
|
| `src/judges/` | Prompt data, bounded model loop, tool adapter | Git target selection |
|
|
67
67
|
| `src/lang/` | Language-pack data, isolated parser workers, optional language oracles | Cross-run policy |
|
|
68
68
|
| `src/report/` | Pure output adapters | Re-running or reinterpreting a review |
|
|
69
|
+
| `src/github/` | GitHub REST transport and pull-request publication reconciliation | Review decisions or report rendering |
|
|
69
70
|
| `src/bench.ts` | Historical and labelled evaluation | Production command dispatch |
|
|
70
71
|
| `src/session.ts`, `src/cache.ts` | Reuse of completed judge work | Completion decisions |
|
|
71
72
|
|
package/docs/ci.md
CHANGED
|
@@ -41,6 +41,10 @@ name: PowerShot
|
|
|
41
41
|
on:
|
|
42
42
|
pull_request:
|
|
43
43
|
|
|
44
|
+
concurrency:
|
|
45
|
+
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
|
46
|
+
cancel-in-progress: true
|
|
47
|
+
|
|
44
48
|
permissions:
|
|
45
49
|
contents: read
|
|
46
50
|
pull-requests: write
|
|
@@ -77,6 +81,27 @@ and bounded batches, so all declared languages can coexist in one monorepo revie
|
|
|
77
81
|
Set `upload-sarif: 'false'` and omit `security-events: write` when GitHub code scanning
|
|
78
82
|
is unavailable or the workflow should not publish SARIF.
|
|
79
83
|
|
|
84
|
+
`comment: 'true'` maintains one summary through a hidden marker scoped to the caller's
|
|
85
|
+
workflow file and job. Reruns update only that exact `github-actions[bot]` comment, so
|
|
86
|
+
another workflow or job using the same bot identity is left alone. Each candidate also
|
|
87
|
+
records its pull-request head. A new head gets a new candidate, which means an old run
|
|
88
|
+
never patches or retires the current head's comment. The first scoped run replaces the
|
|
89
|
+
newest unmarked legacy `## PowerShot` summary from v1.1.2 or older without claiming the
|
|
90
|
+
ambiguous legacy comment through `PATCH`.
|
|
91
|
+
|
|
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.
|
|
97
|
+
|
|
98
|
+
PowerShot checks the target head throughout reconciliation and removes its own
|
|
99
|
+
just-created candidate if it observes a changed head. Simultaneous same-head runs
|
|
100
|
+
relist and converge on one scoped candidate when they complete. Keep the example's
|
|
101
|
+
`concurrency` block to reduce overlap and canceled stale work. GitHub's issue-comment
|
|
102
|
+
REST API has no atomic create-if-absent operation, and cancellation cannot stop a REST
|
|
103
|
+
request already in flight, so concurrency reduces but cannot eliminate that window.
|
|
104
|
+
|
|
80
105
|
`inline-comments: 'true'` requires `pull-requests: write`. It publishes at most ten
|
|
81
106
|
findings as one review. Only deterministic `verified` findings with `proven`
|
|
82
107
|
confidence, severity `medium` or higher, and a GitHub-confirmed added line qualify.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xcraft/powershot",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
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>",
|