@0xcraft/powershot 1.0.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/LICENSE +202 -0
- package/README.md +306 -0
- package/dist/agents.js +82 -0
- package/dist/bench.js +179 -0
- package/dist/budget.js +59 -0
- package/dist/bundle.js +173 -0
- package/dist/cache.js +155 -0
- package/dist/cli/agent-command.js +27 -0
- package/dist/cli/app.js +31 -0
- package/dist/cli/args.js +76 -0
- package/dist/cli/bench-command.js +89 -0
- package/dist/cli/dismiss-command.js +42 -0
- package/dist/cli/environment.js +32 -0
- package/dist/cli/reports.js +64 -0
- package/dist/cli/review-command.js +268 -0
- package/dist/cli/session-command.js +62 -0
- package/dist/cli.js +7 -0
- package/dist/config.js +130 -0
- package/dist/delegate.js +84 -0
- package/dist/dismissed.js +130 -0
- package/dist/fspolicy.js +62 -0
- package/dist/git.js +238 -0
- package/dist/ground.js +286 -0
- package/dist/judges/judge.js +85 -0
- package/dist/judges/llm.js +234 -0
- package/dist/judges/prompts.js +86 -0
- package/dist/judges/tools.js +125 -0
- package/dist/lang/packs.js +557 -0
- package/dist/lang/pyright.js +108 -0
- package/dist/lang/python-deps.js +174 -0
- package/dist/lang/ruby-deps.js +77 -0
- package/dist/langtest.js +248 -0
- package/dist/manifest.js +209 -0
- package/dist/otel.js +75 -0
- package/dist/package-meta.js +13 -0
- package/dist/package-smoke.js +110 -0
- package/dist/plan.js +134 -0
- package/dist/position.js +94 -0
- package/dist/report/ansi.js +18 -0
- package/dist/report/codequality.js +19 -0
- package/dist/report/compact.js +15 -0
- package/dist/report/highlight.js +54 -0
- package/dist/report/markdown.js +113 -0
- package/dist/report/sarif.js +66 -0
- package/dist/report/terminal.js +170 -0
- package/dist/report/viewer.js +148 -0
- package/dist/review.js +355 -0
- package/dist/scan.js +67 -0
- package/dist/selftest.js +1928 -0
- package/dist/session.js +140 -0
- package/dist/snapshot.js +101 -0
- package/dist/text.js +50 -0
- package/dist/types.js +2 -0
- package/dist/verifiers/assertion-drift.js +137 -0
- package/dist/verifiers/contract-drift.js +140 -0
- package/dist/verifiers/copy-paste-drift.js +106 -0
- package/dist/verifiers/dead-on-arrival.js +92 -0
- package/dist/verifiers/dropped-guard.js +144 -0
- package/dist/verifiers/foreign-contract-drift.js +114 -0
- package/dist/verifiers/foreign-copy-paste-drift.js +83 -0
- package/dist/verifiers/foreign-dropped-guard.js +78 -0
- package/dist/verifiers/foreign-phantom-api.js +36 -0
- package/dist/verifiers/foreign-phantom-config.js +40 -0
- package/dist/verifiers/foreign-phantom-dep.js +82 -0
- package/dist/verifiers/foreign-reinvented.js +65 -0
- package/dist/verifiers/foreign-scope-creep.js +42 -0
- package/dist/verifiers/foreign-swallowed-error.js +36 -0
- package/dist/verifiers/foreign-tests.js +143 -0
- package/dist/verifiers/foreign-tokens.js +94 -0
- package/dist/verifiers/foreign.js +16 -0
- package/dist/verifiers/index.js +38 -0
- package/dist/verifiers/lying-comment.js +90 -0
- package/dist/verifiers/phantom-api.js +88 -0
- package/dist/verifiers/phantom-config.js +93 -0
- package/dist/verifiers/phantom-dep.js +110 -0
- package/dist/verifiers/reinvented.js +74 -0
- package/dist/verifiers/scope-creep.js +77 -0
- package/dist/verifiers/swallowed-error.js +110 -0
- package/dist/verifiers/vacuous-test.js +138 -0
- package/docs/architecture.md +191 -0
- package/docs/assets/cli-preview.svg +68 -0
- package/docs/assets/powershot-logo.png +0 -0
- package/docs/ci.md +151 -0
- package/examples/github-actions/action.yml +23 -0
- package/examples/github-actions/cli.yml +43 -0
- package/examples/gitlab/.gitlab-ci.yml +21 -0
- package/package.json +65 -0
package/dist/review.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { buildGround } from './ground.js';
|
|
2
|
+
import { baseRefOf, collectChanges, statedIntent } from './git.js';
|
|
3
|
+
import { bundle, bundleName, reviewables, uncovered } from './bundle.js';
|
|
4
|
+
import { attachFrames, positionable } from './position.js';
|
|
5
|
+
import { skippedLanguages } from './lang/packs.js';
|
|
6
|
+
import { JudgeCache } from './cache.js';
|
|
7
|
+
import { Dismissals, rememberReport } from './dismissed.js';
|
|
8
|
+
import { renderChanges } from './judges/judge.js';
|
|
9
|
+
import { VERIFIERS } from './verifiers/index.js';
|
|
10
|
+
import { runJudge } from './judges/judge.js';
|
|
11
|
+
import { COMMON, JUDGES } from './judges/prompts.js';
|
|
12
|
+
import { apiKey } from './judges/llm.js';
|
|
13
|
+
import { enabled } from './config.js';
|
|
14
|
+
import { SelectionPlan, capabilitiesOf } from './plan.js';
|
|
15
|
+
import { Budget } from './budget.js';
|
|
16
|
+
import { packFor } from './lang/packs.js';
|
|
17
|
+
import { SEVERITIES } from './types.js';
|
|
18
|
+
import { stripControl, stripPath } from './text.js';
|
|
19
|
+
const packOf = (path) => packFor(path)?.name ?? 'other';
|
|
20
|
+
export function atLeast(severity, min) {
|
|
21
|
+
return SEVERITIES.indexOf(severity) >= SEVERITIES.indexOf(min);
|
|
22
|
+
}
|
|
23
|
+
/** How much two titles say the same thing, as a share of the words they use. */
|
|
24
|
+
export function titleOverlap(a, b) {
|
|
25
|
+
const words = (t) => new Set(t.toLowerCase().match(/[a-z0-9_]{3,}/g) ?? []);
|
|
26
|
+
const left = words(a);
|
|
27
|
+
const right = words(b);
|
|
28
|
+
if (left.size === 0 || right.size === 0)
|
|
29
|
+
return 0;
|
|
30
|
+
let shared = 0;
|
|
31
|
+
for (const w of left)
|
|
32
|
+
if (right.has(w))
|
|
33
|
+
shared++;
|
|
34
|
+
return shared / Math.min(left.size, right.size);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Files this verifier can actually answer for, with unavailable oracles kept per
|
|
38
|
+
* file. `base` is applicability rather than a missing capability: a before/after
|
|
39
|
+
* check has no question to ask about a newly created file.
|
|
40
|
+
*/
|
|
41
|
+
function verifierTargets(v, g, have) {
|
|
42
|
+
if (v.domain === 'typescript') {
|
|
43
|
+
return g.files
|
|
44
|
+
.filter((file) => !v.needs.includes('base') || file.before !== undefined)
|
|
45
|
+
.map((file) => ({
|
|
46
|
+
kind: 'typescript',
|
|
47
|
+
path: file.changed.path,
|
|
48
|
+
file,
|
|
49
|
+
missing: v.needs.filter((need) => {
|
|
50
|
+
if (need === 'types' || need === 'references')
|
|
51
|
+
return !file.typed;
|
|
52
|
+
if (need === 'python-types')
|
|
53
|
+
return true;
|
|
54
|
+
return false;
|
|
55
|
+
}),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
if (v.domain === 'foreign' || v.domain === 'python') {
|
|
59
|
+
return g.foreign
|
|
60
|
+
.filter((file) => v.domain !== 'python' || file.pack.name === 'python')
|
|
61
|
+
.filter((file) => !v.supports || v.supports(file))
|
|
62
|
+
.filter((file) => !v.needs.includes('base') || file.beforeTree !== undefined)
|
|
63
|
+
.map((file) => ({
|
|
64
|
+
kind: 'foreign',
|
|
65
|
+
path: file.path,
|
|
66
|
+
file,
|
|
67
|
+
missing: v.needs.filter((need) => {
|
|
68
|
+
if (need === 'python-types')
|
|
69
|
+
return file.pack.name !== 'python' || !have.has('python-types');
|
|
70
|
+
if (need === 'types' || need === 'references')
|
|
71
|
+
return true;
|
|
72
|
+
return false;
|
|
73
|
+
}),
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
function groundFor(g, targets) {
|
|
79
|
+
const files = targets
|
|
80
|
+
.filter((target) => target.kind === 'typescript' && target.missing.length === 0)
|
|
81
|
+
.map((target) => target.file);
|
|
82
|
+
const foreign = targets
|
|
83
|
+
.filter((target) => target.kind === 'foreign' && target.missing.length === 0)
|
|
84
|
+
.map((target) => target.file);
|
|
85
|
+
return { ...g, files, foreign, typed: files.some((file) => file.typed) };
|
|
86
|
+
}
|
|
87
|
+
function dropNearDuplicates(findings) {
|
|
88
|
+
const kept = [];
|
|
89
|
+
for (const f of findings) {
|
|
90
|
+
const duplicate = kept.some((k) => k.file === f.file && Math.abs(k.line - f.line) <= 2 && titleOverlap(k.title, f.title) >= 0.7);
|
|
91
|
+
if (!duplicate)
|
|
92
|
+
kept.push(f);
|
|
93
|
+
}
|
|
94
|
+
return kept;
|
|
95
|
+
}
|
|
96
|
+
function finalize(findings) {
|
|
97
|
+
// the compiler reports one mistake through several diagnostics
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
findings = findings.filter((f) => {
|
|
100
|
+
const k = f.check + '|' + f.file + '|' + f.line + '|' + f.title;
|
|
101
|
+
if (seen.has(k))
|
|
102
|
+
return false;
|
|
103
|
+
seen.add(k);
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
106
|
+
// Sorted before folding: folding keeps whichever copy comes first, so the other
|
|
107
|
+
// way round a `low` duplicate would evict the `critical` saying the same thing.
|
|
108
|
+
const sorted = findings.slice().sort((a, b) => {
|
|
109
|
+
if (a.class !== b.class)
|
|
110
|
+
return a.class === 'verified' ? -1 : 1;
|
|
111
|
+
const sev = SEVERITIES.indexOf(b.severity) - SEVERITIES.indexOf(a.severity);
|
|
112
|
+
if (sev !== 0)
|
|
113
|
+
return sev;
|
|
114
|
+
return a.file.localeCompare(b.file) || a.line - b.line;
|
|
115
|
+
});
|
|
116
|
+
// Sanitized once here rather than per renderer: a title or a frame line is
|
|
117
|
+
// attacker-controlled text, and every output path prints it somewhere
|
|
118
|
+
return dropNearDuplicates(sorted).map((f, i) => ({
|
|
119
|
+
...f,
|
|
120
|
+
id: 'F' + (i + 1),
|
|
121
|
+
file: stripPath(f.file),
|
|
122
|
+
title: stripControl(f.title),
|
|
123
|
+
fix: f.fix === undefined ? undefined : stripControl(f.fix),
|
|
124
|
+
suggestion: f.suggestion === undefined ? undefined : stripControl(f.suggestion),
|
|
125
|
+
evidence: f.evidence && { oracle: stripControl(f.evidence.oracle), detail: stripControl(f.evidence.detail) },
|
|
126
|
+
frame: f.frame && { ...f.frame, lines: f.frame.lines.map(stripControl) },
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
export async function review(opts) {
|
|
130
|
+
const { root, range, config, verifyOnly, onProgress } = opts;
|
|
131
|
+
const repo = opts.stateRoot ?? root;
|
|
132
|
+
const say = onProgress ?? (() => { });
|
|
133
|
+
const stage = opts.onStage ?? (() => () => { });
|
|
134
|
+
const failures = [];
|
|
135
|
+
let budgetStop;
|
|
136
|
+
const all = opts.changes ?? collectChanges(repo, range);
|
|
137
|
+
const plan = SelectionPlan.build(root, all, config);
|
|
138
|
+
const changed = plan.keep(all);
|
|
139
|
+
if (changed.length === 0) {
|
|
140
|
+
for (const line of plan.summary())
|
|
141
|
+
say('selection ' + line);
|
|
142
|
+
return { findings: [], stats: { files: 0, verified: 0, judged: 0, dismissed: 0 }, failures, plan };
|
|
143
|
+
}
|
|
144
|
+
const skipped = new Map();
|
|
145
|
+
const budget = opts.budget ?? new Budget();
|
|
146
|
+
const manifest = opts.manifest;
|
|
147
|
+
const groundDone = stage('ground');
|
|
148
|
+
const g = await buildGround(root, changed, opts.signal);
|
|
149
|
+
groundDone(g.project.getSourceFiles().length + ' files · ' + g.symbolIndex.size + ' symbols' +
|
|
150
|
+
(g.typed ? '' : ' · no tsconfig, phantom-api disabled'));
|
|
151
|
+
const wanted = (v) => {
|
|
152
|
+
const id = v.id ?? v.name;
|
|
153
|
+
return opts.checks
|
|
154
|
+
? opts.checks.includes(id) || opts.checks.includes(v.name)
|
|
155
|
+
: enabled(config.verifiers, id) || enabled(config.verifiers, v.name);
|
|
156
|
+
};
|
|
157
|
+
const have = capabilitiesOf(g);
|
|
158
|
+
const targets = new Map();
|
|
159
|
+
for (const verifier of VERIFIERS) {
|
|
160
|
+
if (!wanted(verifier))
|
|
161
|
+
continue;
|
|
162
|
+
const files = verifierTargets(verifier, g, have);
|
|
163
|
+
if (files.length > 0)
|
|
164
|
+
targets.set(verifier, files);
|
|
165
|
+
}
|
|
166
|
+
const selectedVerifiers = [...targets.keys()];
|
|
167
|
+
// a file the change touched that no parser produced a tree for was not reviewed,
|
|
168
|
+
// whatever the summary says about the ones that were
|
|
169
|
+
const grounded = new Set([...g.files.map((f) => f.changed.path), ...g.foreign.map((f) => f.path)]);
|
|
170
|
+
for (const c of changed) {
|
|
171
|
+
if (!grounded.has(c.path))
|
|
172
|
+
plan.waive(c.path, 'no parser for this language');
|
|
173
|
+
}
|
|
174
|
+
// Capabilities belong to files, not runs. A typed file beside one excluded from
|
|
175
|
+
// tsconfig must not make the latter look checked, and an old Ruby file must not
|
|
176
|
+
// make a new Python file eligible for a before/after oracle.
|
|
177
|
+
for (const files of targets.values()) {
|
|
178
|
+
for (const file of files)
|
|
179
|
+
plan.limit(file.path, file.missing);
|
|
180
|
+
}
|
|
181
|
+
if (skippedLanguages.length > 0) {
|
|
182
|
+
for (const c of changed) {
|
|
183
|
+
if (!grounded.has(c.path) && skippedLanguages.includes(packOf(c.path))) {
|
|
184
|
+
plan.fail(c.path, 'grammar budget reached');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
failures.push('not reviewed, grammar budget reached: ' + skippedLanguages.join(', '));
|
|
188
|
+
}
|
|
189
|
+
for (const line of plan.summary())
|
|
190
|
+
say('selection ' + line);
|
|
191
|
+
for (const f of plan.of('failed'))
|
|
192
|
+
failures.push('not reviewed: ' + f.path + ' — ' + f.reason);
|
|
193
|
+
const verifyDone = stage('verify');
|
|
194
|
+
const findings = [];
|
|
195
|
+
let ran = 0;
|
|
196
|
+
for (const v of selectedVerifiers) {
|
|
197
|
+
// a check that cannot run must say so rather than return nothing, which reads
|
|
198
|
+
// exactly like a check that ran and was satisfied
|
|
199
|
+
const files = targets.get(v);
|
|
200
|
+
const eligible = files.filter((file) => file.missing.length === 0);
|
|
201
|
+
const missing = [...new Set(files.flatMap((file) => file.missing))];
|
|
202
|
+
if (missing.length > 0 && eligible.length === 0) {
|
|
203
|
+
skipped.set(v.id ?? v.name, missing.join(', '));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
// a scan spends most of its time here, so Ctrl-C has to reach this half
|
|
207
|
+
if (opts.signal?.aborted) {
|
|
208
|
+
failures.push('cancelled during verification');
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
ran++;
|
|
212
|
+
const check = v.id ?? v.name;
|
|
213
|
+
manifest?.ran(check);
|
|
214
|
+
for (const file of eligible)
|
|
215
|
+
plan.checked(file.path, check);
|
|
216
|
+
try {
|
|
217
|
+
findings.push(...v.run(groundFor(g, files)));
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
failures.push(v.name + ': ' + e.message);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
verifyDone(ran + ' checks × ' + reviewables(g).length + ' files · 0 tokens');
|
|
224
|
+
if (skipped.size > 0) {
|
|
225
|
+
const names = [...skipped].map(([n, why]) => n + ' (no ' + why + ')');
|
|
226
|
+
say('skipped ' + names.join(', '));
|
|
227
|
+
}
|
|
228
|
+
// --checks overrides the config rather than filtering it
|
|
229
|
+
const isGated = range.from !== undefined || range.commit !== undefined;
|
|
230
|
+
const judgeCache = opts.cache === false || verifyOnly ? undefined : JudgeCache.open(repo, isGated);
|
|
231
|
+
const wantedJudges = JUDGES.filter((j) => opts.checks ? opts.checks.includes(j.name) : enabled(config.judges, j.name));
|
|
232
|
+
if (!verifyOnly && wantedJudges.length > 0) {
|
|
233
|
+
if (!apiKey(config)) {
|
|
234
|
+
// requested and not run is not the same as not requested: a run that was asked
|
|
235
|
+
// for judges and never reached one must not report the deterministic half as
|
|
236
|
+
// the whole answer
|
|
237
|
+
failures.push('judges requested but no API key set: ' + wantedJudges.map((j) => j.name).join(', '));
|
|
238
|
+
say('judge skipped — no API key set (use --verify-only to silence this)');
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
opts.onCancelable?.(true);
|
|
242
|
+
const intent = statedIntent(repo, range);
|
|
243
|
+
const units = bundle(g, opts.maxBundleLines ?? 1200);
|
|
244
|
+
let cancelled = false;
|
|
245
|
+
if (units.length > 1) {
|
|
246
|
+
say('bundle ' + reviewables(g).length + ' files → ' + units.length + ' review units');
|
|
247
|
+
}
|
|
248
|
+
const missed = uncovered(g, units);
|
|
249
|
+
if (missed.length > 0)
|
|
250
|
+
failures.push('not sent to any judge: ' + missed.slice(0, 5).join(', '));
|
|
251
|
+
for (const spec of wantedJudges) {
|
|
252
|
+
if (cancelled)
|
|
253
|
+
break;
|
|
254
|
+
for (const unit of units) {
|
|
255
|
+
// between units, not mid-flight: `--resume` picks up exactly here
|
|
256
|
+
if (opts.signal?.aborted) {
|
|
257
|
+
cancelled = true;
|
|
258
|
+
failures.push('cancelled before ' + spec.name + ' finished');
|
|
259
|
+
manifest?.unit({ judge: spec.name, unit: bundleName(unit, root), outcome: 'waived', reason: 'cancelled', findings: 0 });
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
const stop = budget.exhausted();
|
|
263
|
+
if (stop) {
|
|
264
|
+
budgetStop = stop;
|
|
265
|
+
manifest?.unit({ judge: spec.name, unit: bundleName(unit, root), outcome: 'waived', reason: stop, findings: 0 });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const label = units.length > 1 ? spec.name + ' · ' + bundleName(unit, root) : spec.name;
|
|
269
|
+
const rendered = renderChanges(unit.files);
|
|
270
|
+
const cached = opts.session?.get(spec.name, bundleName(unit, root), rendered);
|
|
271
|
+
if (cached) {
|
|
272
|
+
findings.push(...cached);
|
|
273
|
+
manifest?.unit({ judge: spec.name, unit: bundleName(unit, root), outcome: 'reused', reason: 'resumed from session', findings: cached.length });
|
|
274
|
+
say('judge ' + label + ' → ' + cached.length + ' findings (resumed)');
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const key = JudgeCache.key({
|
|
278
|
+
judge: spec.name,
|
|
279
|
+
provider: config.provider,
|
|
280
|
+
model: config.model,
|
|
281
|
+
prompt: COMMON + '\n' + spec.brief,
|
|
282
|
+
tools: opts.tools ?? false,
|
|
283
|
+
content: rendered,
|
|
284
|
+
intent: spec.needsIntent ? intent : undefined,
|
|
285
|
+
});
|
|
286
|
+
const remembered = judgeCache?.get(key);
|
|
287
|
+
if (remembered) {
|
|
288
|
+
findings.push(...remembered);
|
|
289
|
+
opts.session?.record(spec.name, bundleName(unit, root), rendered, remembered);
|
|
290
|
+
manifest?.unit({ judge: spec.name, unit: bundleName(unit, root), outcome: 'reused', reason: 'answered before, same question', findings: remembered.length });
|
|
291
|
+
say('judge ' + label + ' → ' + remembered.length + ' findings (cached)');
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const judgeDone = stage('judge');
|
|
295
|
+
try {
|
|
296
|
+
const judged = await runJudge(spec, g, config, { intent, bundle: unit, useTools: opts.tools, budget });
|
|
297
|
+
findings.push(...judged);
|
|
298
|
+
opts.session?.record(spec.name, bundleName(unit, root), rendered, judged);
|
|
299
|
+
judgeCache?.put(key, judged, new Date().toISOString());
|
|
300
|
+
manifest?.unit({ judge: spec.name, unit: bundleName(unit, root), outcome: 'completed', findings: judged.length });
|
|
301
|
+
judgeDone(label + ' → ' + judged.length + ' findings');
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
// keep what the others found, but the run is incomplete from here
|
|
305
|
+
const detail = label + ': ' + e.message;
|
|
306
|
+
failures.push(detail);
|
|
307
|
+
manifest?.unit({ judge: spec.name, unit: bundleName(unit, root), outcome: 'failed', reason: e.message, findings: 0 });
|
|
308
|
+
judgeDone(label + ' failed: ' + e.message);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
judgeCache?.save();
|
|
315
|
+
opts.onCancelable?.(false);
|
|
316
|
+
if (opts.absorbed?.length)
|
|
317
|
+
findings.push(...opts.absorbed);
|
|
318
|
+
const positioned = positionable(findings, g);
|
|
319
|
+
if (positioned.dropped > 0) {
|
|
320
|
+
say('position dropped ' + positioned.dropped + ' judged finding(s) pointing outside the change');
|
|
321
|
+
}
|
|
322
|
+
const framed = attachFrames(positioned.kept, g).filter((f) => atLeast(f.severity, config.minSeverity));
|
|
323
|
+
// the count is still reported: a filter nobody can see is one nobody can trust
|
|
324
|
+
const gated = range.from !== undefined || range.commit !== undefined;
|
|
325
|
+
const base = gated ? baseRefOf(repo, range) : undefined;
|
|
326
|
+
const dismissals = Dismissals.open(repo, base);
|
|
327
|
+
const surviving = opts.showDismissed ? framed : framed.filter((f) => !dismissals.has(f));
|
|
328
|
+
const dismissed = framed.length - surviving.length;
|
|
329
|
+
if (dismissed > 0 && !opts.showDismissed) {
|
|
330
|
+
say('dismissed ' + dismissed + ' finding(s) previously called correct — psh dismiss list');
|
|
331
|
+
}
|
|
332
|
+
const pending = base ? Dismissals.pendingIn(repo, base) : 0;
|
|
333
|
+
if (pending > 0)
|
|
334
|
+
say('dismissed ' + pending + ' new dismissal(s) in this change — not applied to it');
|
|
335
|
+
const kept = finalize(surviving);
|
|
336
|
+
rememberReport(repo, kept); // so `psh dismiss F2` knows which finding F2 was
|
|
337
|
+
// stats describe what is reported, not what was found before filtering
|
|
338
|
+
return {
|
|
339
|
+
findings: kept,
|
|
340
|
+
stats: {
|
|
341
|
+
files: reviewables(g).length,
|
|
342
|
+
verified: kept.filter((f) => f.class === 'verified').length,
|
|
343
|
+
judged: kept.filter((f) => f.class === 'judged').length,
|
|
344
|
+
dismissed,
|
|
345
|
+
},
|
|
346
|
+
failures,
|
|
347
|
+
plan,
|
|
348
|
+
skippedChecks: [...skipped].map(([check, missing]) => ({ check, missing })),
|
|
349
|
+
usage: budget.finish(),
|
|
350
|
+
budgetStop,
|
|
351
|
+
cancelled: opts.signal?.aborted ?? false,
|
|
352
|
+
droppedPosition: positioned.dropped,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
//# sourceMappingURL=review.js.map
|
package/dist/scan.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { decode, lines as splitLines } from './text.js';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { insideRepo, isSymlink, repoPath } from './fspolicy.js';
|
|
5
|
+
import { packFor } from './lang/packs.js';
|
|
6
|
+
const TS_CODE = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
|
|
7
|
+
/**
|
|
8
|
+
* A scan covers every language the tool claims to review, not only the two the
|
|
9
|
+
* TypeScript compiler reads. Walking only JS/TS meant `psh scan` on a Go or Python
|
|
10
|
+
* repository reported a clean tree it had never opened.
|
|
11
|
+
*/
|
|
12
|
+
function isCode(name) {
|
|
13
|
+
return TS_CODE.test(name) || packFor(name) !== undefined;
|
|
14
|
+
}
|
|
15
|
+
const SKIP = new Set(['node_modules', 'dist', 'build', 'coverage', '.git', '.next', 'out']);
|
|
16
|
+
/** No base revision, so the before/after verifiers stay silent by construction. */
|
|
17
|
+
export function scanPaths(root, target) {
|
|
18
|
+
const out = [];
|
|
19
|
+
const walk = (dir) => {
|
|
20
|
+
let entries;
|
|
21
|
+
try {
|
|
22
|
+
entries = readdirSync(dir);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const name of entries) {
|
|
28
|
+
if (SKIP.has(name) || name.startsWith('.'))
|
|
29
|
+
continue;
|
|
30
|
+
const full = join(dir, name);
|
|
31
|
+
// a link inside the repository can point anywhere, and a scan feeds what it
|
|
32
|
+
// reads to the judges — the boundary has to hold here, not only in their tools
|
|
33
|
+
if (isSymlink(full) || !insideRepo(root, full))
|
|
34
|
+
continue;
|
|
35
|
+
const stat = statSync(full, { throwIfNoEntry: false });
|
|
36
|
+
if (!stat)
|
|
37
|
+
continue;
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
walk(full);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!isCode(name))
|
|
43
|
+
continue;
|
|
44
|
+
const lines = splitLines(decode(readFileSync(full))).length;
|
|
45
|
+
out.push({
|
|
46
|
+
path: repoPath(root, full),
|
|
47
|
+
added: new Set(Array.from({ length: lines }, (_, i) => i + 1)),
|
|
48
|
+
before: undefined,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const start = insideRepo(root, target);
|
|
53
|
+
if (!start)
|
|
54
|
+
return out;
|
|
55
|
+
const stat = statSync(start, { throwIfNoEntry: false });
|
|
56
|
+
if (stat?.isFile()) {
|
|
57
|
+
if (isSymlink(start))
|
|
58
|
+
return out;
|
|
59
|
+
const lines = splitLines(decode(readFileSync(start))).length;
|
|
60
|
+
out.push({ path: repoPath(root, start), added: new Set(Array.from({ length: lines }, (_, i) => i + 1)) });
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
walk(start);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=scan.js.map
|