@0xcraft/powershot 1.1.5 → 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 +21 -7
- 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/plan.js +25 -0
- package/dist/report/summary.js +12 -4
- package/dist/review.js +4 -16
- package/dist/selftest.js +90 -8
- package/dist/session.js +1 -0
- package/docs/architecture.md +8 -0
- package/docs/ci.md +9 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,8 +180,8 @@ commands.
|
|
|
180
180
|
flowchart LR
|
|
181
181
|
START["Selected files and checks"] --> ACCOUNT{"Required work accounted for?"}
|
|
182
182
|
ACCOUNT -- "yes" --> DEPTH{"Enriched semantic depth available?"}
|
|
183
|
-
DEPTH -- "yes" --> FULL["full coverage"]
|
|
184
|
-
DEPTH -- "no · portable policy" --> PORTABLE["portable coverage · gaps named"]
|
|
183
|
+
DEPTH -- "yes" --> FULL["full applicable-oracle coverage"]
|
|
184
|
+
DEPTH -- "no · portable policy" --> PORTABLE["portable oracle coverage · gaps named"]
|
|
185
185
|
FULL --> FINDINGS{"Findings?"}
|
|
186
186
|
PORTABLE --> FINDINGS
|
|
187
187
|
FINDINGS -- "no" --> CLEAN["exit 0 · complete and clean"]
|
|
@@ -206,11 +206,11 @@ flowchart LR
|
|
|
206
206
|
| `2` | Command or Git input was invalid |
|
|
207
207
|
| `3` | Review is incomplete; findings may be missing |
|
|
208
208
|
|
|
209
|
-
A clean report names its effective severity threshold, deterministic/model mode,
|
|
210
|
-
|
|
211
|
-
a verdict and keeps the missing work visible. Use
|
|
212
|
-
dispositions, executed and unavailable checks,
|
|
213
|
-
`notLookedAt`.
|
|
209
|
+
A clean report names its effective severity threshold, deterministic/model mode,
|
|
210
|
+
reviewed/changed file ratio, check count, and oracle coverage level. A partial or
|
|
211
|
+
failed review instead says it is not a verdict and keeps the missing work visible. Use
|
|
212
|
+
`--format manifest` to inspect file dispositions, executed and unavailable checks,
|
|
213
|
+
failures, judge units, and `notLookedAt`.
|
|
214
214
|
|
|
215
215
|
## CI integration
|
|
216
216
|
|
|
@@ -298,6 +298,20 @@ psh delegate > /tmp/powershot-brief.md
|
|
|
298
298
|
psh review --verify-only --absorb /tmp/powershot-findings.json
|
|
299
299
|
```
|
|
300
300
|
|
|
301
|
+
Delegation uses the same `SelectionPlan` and target snapshot as a real review. The
|
|
302
|
+
brief names every changed file as selected, policy-waived, or failed, so unsupported
|
|
303
|
+
or oversized files cannot disappear between task creation and absorption. For an
|
|
304
|
+
agent that consumes structured input, request the versioned task directly:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
psh delegate --format json > /tmp/powershot-task.json
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`powershot.delegate/v1` contains the resolved file dispositions, applicable judges,
|
|
311
|
+
bounded review units, changed-line excerpts, intent, and the exact output contract.
|
|
312
|
+
Creating either form is LLM-free and does not create a session. Deterministic checks
|
|
313
|
+
run when the returned finding array is absorbed by `psh review --verify-only`.
|
|
314
|
+
|
|
301
315
|
## Language coverage
|
|
302
316
|
|
|
303
317
|
| Language | Available oracles |
|
package/dist/cli/args.js
CHANGED
|
@@ -6,7 +6,8 @@ psh — code review for machine-written code
|
|
|
6
6
|
psh review --from main --to feat/x branch range
|
|
7
7
|
psh review --commit <hash> a single commit
|
|
8
8
|
psh scan <path> audit existing files, no git history needed
|
|
9
|
-
psh delegate
|
|
9
|
+
psh delegate [--format text|markdown|json]
|
|
10
|
+
emit a selection-accounted task for your existing agent
|
|
10
11
|
psh session list | view <id> runs that can be resumed, or replayed as a page
|
|
11
12
|
psh session diff <a> <b> what one review found that the other did not
|
|
12
13
|
psh dismiss <id> [--reason "..."] record that a finding is correct, and stop showing it
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { Budget, parseLimits } from '#app/budget.js';
|
|
4
4
|
import { loadConfig, policyChanged } from '#app/config.js';
|
|
5
|
-
import { absorbDelegated, delegateBrief } from '#app/delegate.js';
|
|
5
|
+
import { absorbDelegated, buildDelegateTask, delegateBrief, delegateJson } from '#app/delegate.js';
|
|
6
6
|
import { baseRefOf, checkRange, headSha, repoRoot, shaOf } from '#app/git.js';
|
|
7
7
|
import { JUDGES } from '#app/judges/prompts.js';
|
|
8
8
|
import { RunManifest, coverageProblems, hashOf, writeManifest } from '#app/manifest.js';
|
|
@@ -13,6 +13,7 @@ import { progress, stage } from '#app/report/terminal.js';
|
|
|
13
13
|
import { summarizeRun } from '#app/report/summary.js';
|
|
14
14
|
import { review } from '#app/review.js';
|
|
15
15
|
import { scanPaths } from '#app/scan.js';
|
|
16
|
+
import { SelectionPlan } from '#app/plan.js';
|
|
16
17
|
import { Session } from '#app/session.js';
|
|
17
18
|
import { withTargetTree } from '#app/snapshot.js';
|
|
18
19
|
import { SEVERITIES } from '#app/types.js';
|
|
@@ -33,6 +34,10 @@ export async function runReviewCommand(command, values, positionals) {
|
|
|
33
34
|
process.stderr.write('--format must be one of: ' + REPORT_FORMATS.join(', ') + '\n');
|
|
34
35
|
return 2;
|
|
35
36
|
}
|
|
37
|
+
if (command === 'delegate' && !['text', 'markdown', 'json'].includes(values.format)) {
|
|
38
|
+
process.stderr.write('delegate --format must be one of: text, markdown, json\n');
|
|
39
|
+
return 2;
|
|
40
|
+
}
|
|
36
41
|
if ((values.from && !values.to) || (values.to && !values.from)) {
|
|
37
42
|
process.stderr.write('--from and --to must be given together.\n');
|
|
38
43
|
return 2;
|
|
@@ -142,19 +147,32 @@ export async function runReviewCommand(command, values, positionals) {
|
|
|
142
147
|
if (command === 'delegate') {
|
|
143
148
|
const { buildGround } = await import('#app/ground.js');
|
|
144
149
|
const { collectChanges, statedIntent } = await import('#app/git.js');
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
if (changes.length === 0) {
|
|
150
|
+
const all = collectChanges(root, range);
|
|
151
|
+
if (all.length === 0) {
|
|
148
152
|
process.stderr.write('Nothing to review.\n');
|
|
149
153
|
return 0;
|
|
150
154
|
}
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
const task = await withTargetTree(root, range, async (tree) => {
|
|
156
|
+
const plan = SelectionPlan.build(tree, all, config);
|
|
157
|
+
const selected = plan.keep(all);
|
|
158
|
+
const ground = await buildGround(tree, selected, undefined, all);
|
|
159
|
+
plan.accountForGround(selected, ground);
|
|
160
|
+
return buildDelegateTask(ground, config, plan.items(), {
|
|
161
|
+
intent: statedIntent(root, range),
|
|
162
|
+
maxBundleLines: maxBundle,
|
|
163
|
+
checks: requestedChecks,
|
|
164
|
+
target: { from: values.from, to: values.to, commit: values.commit },
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
const rendered = values.format === 'json' ? delegateJson(task) : delegateBrief(task);
|
|
168
|
+
if (values.output !== undefined) {
|
|
169
|
+
writeFileSync(values.output, rendered);
|
|
170
|
+
process.stderr.write(dim(' written to ' + values.output) + '\n');
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
process.stdout.write(rendered);
|
|
174
|
+
}
|
|
175
|
+
return task.state === 'failed' ? 3 : 0;
|
|
158
176
|
}
|
|
159
177
|
const canceller = new AbortController();
|
|
160
178
|
let interrupted = false;
|
|
@@ -46,6 +46,7 @@ export function runSessionCommand(positionals) {
|
|
|
46
46
|
verifyOnly: session.report.verifyOnly,
|
|
47
47
|
minSeverity: session.report.minSeverity,
|
|
48
48
|
filesReviewed: session.report.filesReviewed,
|
|
49
|
+
filesChanged: session.report.filesChanged,
|
|
49
50
|
deterministicChecks: session.report.deterministicChecks,
|
|
50
51
|
scopeDetails: session.report.scopeDetails,
|
|
51
52
|
}));
|
package/dist/delegate.js
CHANGED
|
@@ -2,44 +2,120 @@ import { bundle, bundleName } from './bundle.js';
|
|
|
2
2
|
import { renderChanges } from './judges/judge.js';
|
|
3
3
|
import { COMMON, JUDGES } from './judges/prompts.js';
|
|
4
4
|
import { enabled } from './config.js';
|
|
5
|
-
|
|
5
|
+
import { stripPath } from './text.js';
|
|
6
|
+
export const DELEGATE_SCHEMA = 'powershot.delegate/v1';
|
|
7
|
+
export function buildDelegateTask(g, cfg, files, opts) {
|
|
6
8
|
const units = bundle(g, opts.maxBundleLines ?? 1200);
|
|
7
9
|
const judges = JUDGES.filter((j) => opts.checks ? opts.checks.includes(j.name) : enabled(cfg.judges, j.name));
|
|
10
|
+
return {
|
|
11
|
+
schema: DELEGATE_SCHEMA,
|
|
12
|
+
state: files.some((file) => file.disposition === 'failed') ? 'failed' : 'complete',
|
|
13
|
+
target: opts.target ?? {},
|
|
14
|
+
intent: opts.intent,
|
|
15
|
+
files: files.map(({ path, disposition, reason, bytes, addedLines, language }) => ({
|
|
16
|
+
path,
|
|
17
|
+
disposition,
|
|
18
|
+
reason,
|
|
19
|
+
bytes,
|
|
20
|
+
addedLines,
|
|
21
|
+
language,
|
|
22
|
+
})),
|
|
23
|
+
judges: judges.map((judge) => {
|
|
24
|
+
const usesIntent = judge.needsIntent === true;
|
|
25
|
+
const applicable = !usesIntent || Boolean(opts.intent);
|
|
26
|
+
return {
|
|
27
|
+
name: judge.name,
|
|
28
|
+
brief: judge.brief,
|
|
29
|
+
applicable,
|
|
30
|
+
usesIntent,
|
|
31
|
+
reason: applicable ? undefined : 'no stated intent available',
|
|
32
|
+
};
|
|
33
|
+
}),
|
|
34
|
+
instructions: {
|
|
35
|
+
summary: 'Perform only the judgement work in this task. PowerShot runs its deterministic checks when these findings are absorbed.',
|
|
36
|
+
groundRules: COMMON,
|
|
37
|
+
output: {
|
|
38
|
+
type: 'json-array',
|
|
39
|
+
empty: [],
|
|
40
|
+
example: [{
|
|
41
|
+
file: 'src/a.ts',
|
|
42
|
+
line: 12,
|
|
43
|
+
severity: 'high',
|
|
44
|
+
confidence: 'firm',
|
|
45
|
+
check: 'plausible-logic',
|
|
46
|
+
title: '…',
|
|
47
|
+
why: '…',
|
|
48
|
+
fix: '…',
|
|
49
|
+
}],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
units: units.map((unit, index) => ({
|
|
53
|
+
id: index + 1,
|
|
54
|
+
name: bundleName(unit, g.root),
|
|
55
|
+
files: unit.files.map((file) => file.path),
|
|
56
|
+
changes: renderChanges(unit.files),
|
|
57
|
+
})),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const cell = (value) => stripPath(value).replace(/\|/g, '\\|');
|
|
61
|
+
export function delegateBrief(task) {
|
|
62
|
+
const scope = task.files.length === 0
|
|
63
|
+
? ['_No changed files._']
|
|
64
|
+
: [
|
|
65
|
+
'| File | Disposition | Language | Added | Reason |',
|
|
66
|
+
'|---|---|---|---:|---|',
|
|
67
|
+
...task.files.map((file) => '| ' + cell(file.path) + ' | ' + file.disposition + ' | ' + file.language + ' | ' + file.addedLines +
|
|
68
|
+
' | ' + cell(file.reason ?? '') + ' |'),
|
|
69
|
+
];
|
|
8
70
|
const out = [
|
|
9
71
|
'# PowerShot review brief',
|
|
10
72
|
'',
|
|
11
|
-
|
|
73
|
+
task.instructions.summary,
|
|
12
74
|
'Act as each judge below over each review unit, then reply with a single JSON array',
|
|
13
75
|
'combining every finding. Add nothing outside the array.',
|
|
14
76
|
'',
|
|
77
|
+
'## Scope',
|
|
78
|
+
'',
|
|
79
|
+
'Task state: **' + task.state + '**.',
|
|
80
|
+
'',
|
|
81
|
+
...scope,
|
|
82
|
+
'',
|
|
15
83
|
'## Output contract',
|
|
16
84
|
'',
|
|
17
85
|
'```json',
|
|
18
|
-
|
|
19
|
-
' "check":"plausible-logic","title":"…","why":"…","fix":"…"}]',
|
|
86
|
+
JSON.stringify(task.instructions.output.example, null, 2),
|
|
20
87
|
'```',
|
|
21
88
|
'',
|
|
22
|
-
'Return `
|
|
89
|
+
'Return `' + JSON.stringify(task.instructions.output.empty) +
|
|
90
|
+
'` when a judge finds nothing. An empty array is a success, not a failure.',
|
|
23
91
|
'',
|
|
24
92
|
'## Ground rules',
|
|
25
93
|
'',
|
|
26
|
-
|
|
94
|
+
task.instructions.groundRules,
|
|
27
95
|
'',
|
|
28
96
|
];
|
|
29
|
-
for (const judge of judges) {
|
|
97
|
+
for (const judge of task.judges) {
|
|
30
98
|
out.push('## Judge: ' + judge.name, '', judge.brief, '');
|
|
31
|
-
if (judge.
|
|
32
|
-
out.push(
|
|
33
|
-
? 'This change states that it does the following:\n\n> ' +
|
|
99
|
+
if (judge.usesIntent) {
|
|
100
|
+
out.push(task.intent
|
|
101
|
+
? 'This change states that it does the following:\n\n> ' + task.intent.split('\n').join('\n> ')
|
|
34
102
|
: '_No stated intent available (no commit in range) — skip this judge._', '');
|
|
35
103
|
}
|
|
36
104
|
}
|
|
37
105
|
out.push('## Review units', '');
|
|
38
|
-
units.
|
|
39
|
-
out.push('
|
|
40
|
-
}
|
|
106
|
+
if (task.units.length === 0) {
|
|
107
|
+
out.push('_No judgement units. Every changed file was excluded or failed preparation; inspect Scope._', '');
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
task.units.forEach((unit) => {
|
|
111
|
+
out.push('### Unit ' + unit.id + ' — ' + cell(unit.name), '', '```', unit.changes, '```', '');
|
|
112
|
+
});
|
|
113
|
+
}
|
|
41
114
|
return out.join('\n');
|
|
42
115
|
}
|
|
116
|
+
export function delegateJson(task) {
|
|
117
|
+
return JSON.stringify(task, null, 2) + '\n';
|
|
118
|
+
}
|
|
43
119
|
export function absorbDelegated(json) {
|
|
44
120
|
let parsed;
|
|
45
121
|
try {
|
package/dist/plan.js
CHANGED
|
@@ -92,6 +92,31 @@ export class SelectionPlan {
|
|
|
92
92
|
keep(changed) {
|
|
93
93
|
return changed.filter((c) => this.rows.get(c.path)?.disposition === 'selected');
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Finish the file-level selection after parsers have had one chance to load it.
|
|
97
|
+
*
|
|
98
|
+
* Review and delegation both promise to describe the same change. Keeping this
|
|
99
|
+
* transition on the plan prevents either caller from silently inventing its own
|
|
100
|
+
* meaning for a deleted, unsupported, or unavailable source file.
|
|
101
|
+
*/
|
|
102
|
+
accountForGround(changed, ground) {
|
|
103
|
+
const grounded = new Set([
|
|
104
|
+
...ground.files.map((file) => file.changed.path),
|
|
105
|
+
...ground.foreign.map((file) => file.path),
|
|
106
|
+
]);
|
|
107
|
+
for (const file of changed) {
|
|
108
|
+
if (file.deleted) {
|
|
109
|
+
this.waive(file.path, 'deleted file has no current source to review');
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (grounded.has(file.path))
|
|
113
|
+
continue;
|
|
114
|
+
if (packFor(file.path))
|
|
115
|
+
this.fail(file.path, 'declared language parser unavailable');
|
|
116
|
+
else
|
|
117
|
+
this.waive(file.path, 'no parser for this language');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
95
120
|
items() {
|
|
96
121
|
return [...this.rows.values()];
|
|
97
122
|
}
|
package/dist/report/summary.js
CHANGED
|
@@ -73,6 +73,7 @@ export function summarizeRun(record) {
|
|
|
73
73
|
filesReviewed: hasFiles
|
|
74
74
|
? record.files.filter((file) => file.disposition === 'selected').length
|
|
75
75
|
: undefined,
|
|
76
|
+
filesChanged: hasFiles ? record.files.length : undefined,
|
|
76
77
|
deterministicChecks: hasChecks ? new Set(record.checks.ran).size : undefined,
|
|
77
78
|
scopeDetails: scopeDetails(record),
|
|
78
79
|
};
|
|
@@ -88,13 +89,20 @@ export function noFindingsLabel(summary) {
|
|
|
88
89
|
}
|
|
89
90
|
export function scopeLine(summary) {
|
|
90
91
|
const parts = [];
|
|
91
|
-
if (summary.filesReviewed !== undefined)
|
|
92
|
-
parts.push(
|
|
92
|
+
if (summary.filesReviewed !== undefined) {
|
|
93
|
+
parts.push(summary.filesChanged === undefined
|
|
94
|
+
? plural(summary.filesReviewed, 'file') + ' reviewed'
|
|
95
|
+
: summary.filesReviewed + '/' + summary.filesChanged + ' changed ' +
|
|
96
|
+
(summary.filesChanged === 1 ? 'file' : 'files') + ' reviewed');
|
|
97
|
+
}
|
|
93
98
|
if (summary.deterministicChecks !== undefined) {
|
|
94
99
|
parts.push(plural(summary.deterministicChecks, 'deterministic check'));
|
|
95
100
|
}
|
|
96
|
-
if (summary.coverage !== undefined)
|
|
97
|
-
parts.push(summary.coverage
|
|
101
|
+
if (summary.coverage !== undefined) {
|
|
102
|
+
parts.push(summary.coverage === 'full'
|
|
103
|
+
? 'full applicable-oracle coverage'
|
|
104
|
+
: 'portable oracle coverage');
|
|
105
|
+
}
|
|
98
106
|
return parts.length > 0 ? parts.join(' · ') : undefined;
|
|
99
107
|
}
|
|
100
108
|
export function modeNote(summary, verifyOnly = 'verify-only') {
|
package/dist/review.js
CHANGED
|
@@ -12,7 +12,6 @@ import { apiKey } from './judges/llm.js';
|
|
|
12
12
|
import { enabled } from './config.js';
|
|
13
13
|
import { SelectionPlan, capabilitiesOf } from './plan.js';
|
|
14
14
|
import { Budget } from './budget.js';
|
|
15
|
-
import { packFor } from './lang/packs.js';
|
|
16
15
|
import { SEVERITIES } from './types.js';
|
|
17
16
|
import { stripControl, stripPath } from './text.js';
|
|
18
17
|
export function atLeast(severity, min) {
|
|
@@ -174,21 +173,10 @@ export async function review(opts) {
|
|
|
174
173
|
// Naming a check explicitly is a request for that oracle, even under the portable
|
|
175
174
|
// default. Strict policy makes the same promise for every configured verifier.
|
|
176
175
|
const requireEnrichedOracles = config.coverage === 'strict' || opts.checks !== undefined;
|
|
177
|
-
//
|
|
178
|
-
// whatever the summary says about the ones that were
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (c.deleted) {
|
|
182
|
-
plan.waive(c.path, 'deleted file has no current source to review');
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
if (grounded.has(c.path))
|
|
186
|
-
continue;
|
|
187
|
-
if (packFor(c.path))
|
|
188
|
-
plan.fail(c.path, 'declared language parser unavailable');
|
|
189
|
-
else
|
|
190
|
-
plan.waive(c.path, 'no parser for this language');
|
|
191
|
-
}
|
|
176
|
+
// A file the change touched that no parser produced a tree for was not reviewed,
|
|
177
|
+
// whatever the summary says about the ones that were. Delegation uses this exact
|
|
178
|
+
// transition too, so its task cannot disagree with the review it will feed.
|
|
179
|
+
plan.accountForGround(changed, g);
|
|
192
180
|
// Capabilities belong to files, not runs. A typed file beside one excluded from
|
|
193
181
|
// tsconfig must not make the latter look checked, and an old Ruby file must not
|
|
194
182
|
// make a new Python file eligible for a before/after oracle.
|
package/dist/selftest.js
CHANGED
|
@@ -35,7 +35,7 @@ import { dirname, join, sep } from 'node:path';
|
|
|
35
35
|
import { runTool } from './judges/tools.js';
|
|
36
36
|
import { codeQuality } from './report/codequality.js';
|
|
37
37
|
import { viewer } from './report/viewer.js';
|
|
38
|
-
import { absorbDelegated, delegateBrief } from './delegate.js';
|
|
38
|
+
import { absorbDelegated, buildDelegateTask, delegateBrief } from './delegate.js';
|
|
39
39
|
import { TARGETS, findTarget } from './agents.js';
|
|
40
40
|
import { PACKS, packFor, parseIsolated } from './lang/packs.js';
|
|
41
41
|
import { isPhantom, pythonManifest, localModules } from './lang/python-deps.js';
|
|
@@ -1175,9 +1175,14 @@ check('portable coverage is a verdict, but never masquerades as full semantic co
|
|
|
1175
1175
|
filesReviewed: 1, deterministicChecks: 1, scopeDetails: unavailable,
|
|
1176
1176
|
});
|
|
1177
1177
|
assert.match(out, /No medium-or-higher deterministic findings\./);
|
|
1178
|
-
assert.match(out, /1 file reviewed · 1 deterministic check · portable coverage/);
|
|
1178
|
+
assert.match(out, /1 file reviewed · 1 deterministic check · portable oracle coverage/);
|
|
1179
1179
|
assert.match(out, /1 reviewed file lacked type information and a reference graph/);
|
|
1180
1180
|
assert.doesNotMatch(out, /not a verdict/);
|
|
1181
|
+
const full = terminal([], {
|
|
1182
|
+
subtitle: 'workspace', verified: 0, judged: 0, state: 'complete', notLookedAt: [],
|
|
1183
|
+
coverage: 'full', filesReviewed: 2, filesChanged: 2, deterministicChecks: 3,
|
|
1184
|
+
});
|
|
1185
|
+
assert.match(full, /2\/2 changed files reviewed · 3 deterministic checks · full applicable-oracle coverage/);
|
|
1181
1186
|
const selected = Array.from({ length: 20 }, (_, index) => ({
|
|
1182
1187
|
path: 'web/file-' + index + '.ts',
|
|
1183
1188
|
disposition: 'selected',
|
|
@@ -1202,7 +1207,7 @@ check('portable coverage is a verdict, but never masquerades as full semantic co
|
|
|
1202
1207
|
},
|
|
1203
1208
|
}));
|
|
1204
1209
|
assert.match(md, /✅ \*\*No medium-or-higher deterministic findings\*\*/);
|
|
1205
|
-
assert.match(md, /20 files reviewed · 19 deterministic checks · portable coverage/);
|
|
1210
|
+
assert.match(md, /20\/36 changed files reviewed · 19 deterministic checks · portable oracle coverage/);
|
|
1206
1211
|
assert.match(md, /Model review was disabled \(`verify-only`\)\./);
|
|
1207
1212
|
assert.match(md, /<summary>Coverage details<\/summary>/);
|
|
1208
1213
|
const rendered = md.replace(/\\/g, '');
|
|
@@ -1958,6 +1963,16 @@ check('the public action persists judge answers and publishes only a verdict', (
|
|
|
1958
1963
|
assert.match(action, /m\.coverage === "full" \|\| m\.coverage === "portable" \? m\.coverage : "unknown"/);
|
|
1959
1964
|
assert.match(action, /Approve a clean review[\s\S]+steps\.review\.outputs\.coverage == 'full'/);
|
|
1960
1965
|
});
|
|
1966
|
+
check('the public action keeps every pull request write inside the base repository', () => {
|
|
1967
|
+
const action = readFileSync(join(process.cwd(), 'action.yml'), 'utf8');
|
|
1968
|
+
for (const name of ['Post inline comments', 'Comment on the pull request', 'Approve a clean review']) {
|
|
1969
|
+
const start = action.indexOf(' - name: ' + name);
|
|
1970
|
+
const end = action.indexOf('\n - name: ', start + 1);
|
|
1971
|
+
const step = action.slice(start, end === -1 ? undefined : end);
|
|
1972
|
+
assert.ok(start >= 0, name + ' step is missing');
|
|
1973
|
+
assert.match(step, /github\.event\.pull_request\.head\.repo\.full_name == github\.repository/, name);
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1961
1976
|
check('published CI examples preserve one verdict and its exit status', () => {
|
|
1962
1977
|
const action = readFileSync(join(process.cwd(), 'examples', 'github-actions', 'action.yml'), 'utf8');
|
|
1963
1978
|
const github = readFileSync(join(process.cwd(), 'examples', 'github-actions', 'cli.yml'), 'utf8');
|
|
@@ -1997,10 +2012,11 @@ check('the viewer labels a complete portable session', () => {
|
|
|
1997
2012
|
const html = viewer([], {
|
|
1998
2013
|
id: 'portable', target: 'workspace', started: '2026-01-01T10:00:00Z',
|
|
1999
2014
|
state: 'complete', notLookedAt: [], coverage: 'portable',
|
|
2000
|
-
verifyOnly: true, minSeverity: 'medium', filesReviewed: 1,
|
|
2015
|
+
verifyOnly: true, minSeverity: 'medium', filesReviewed: 1, filesChanged: 3,
|
|
2016
|
+
deterministicChecks: 2,
|
|
2001
2017
|
scopeDetails: ['1 reviewed file lacked type information'],
|
|
2002
2018
|
});
|
|
2003
|
-
assert.match(html, /1
|
|
2019
|
+
assert.match(html, /1\/3 changed files reviewed · 2 deterministic checks · portable oracle coverage/);
|
|
2004
2020
|
assert.match(html, /No medium-or-higher deterministic findings\./);
|
|
2005
2021
|
assert.match(html, /<summary>Coverage details<\/summary>/);
|
|
2006
2022
|
});
|
|
@@ -2033,9 +2049,11 @@ check('delegate --checks selects only the requested judging brief', () => {
|
|
|
2033
2049
|
provider: 'anthropic', model: 'm', verifiers: ['*'], judges: ['*'],
|
|
2034
2050
|
minSeverity: 'low', ignore: [], coverage: 'portable', promptCache: true,
|
|
2035
2051
|
};
|
|
2036
|
-
const
|
|
2037
|
-
|
|
2038
|
-
|
|
2052
|
+
const task = buildDelegateTask(ground([{ path: 'a.ts', after: 'export const a = 1\n' }]), cfg, [{
|
|
2053
|
+
path: 'a.ts', disposition: 'selected', bytes: 19, addedLines: 1,
|
|
2054
|
+
language: 'typescript', checks: [],
|
|
2055
|
+
}], { checks: ['intent'], intent: 'add a' });
|
|
2056
|
+
const brief = delegateBrief(task);
|
|
2039
2057
|
assert.match(brief, /## Judge: intent/);
|
|
2040
2058
|
assert.doesNotMatch(brief, /## Judge: plausible-logic/);
|
|
2041
2059
|
});
|
|
@@ -2190,6 +2208,17 @@ check('a finding is matched past a line that moved under it', () => {
|
|
|
2190
2208
|
assert.equal(Session.compare(at(12), at(22)).remaining.length, 1);
|
|
2191
2209
|
rmSync(dir, { recursive: true, force: true });
|
|
2192
2210
|
});
|
|
2211
|
+
check('a finished session preserves the reviewed and changed file counts', () => {
|
|
2212
|
+
const dir = mkdtempSync(join(tmpdir(), 'psh-scope-session-'));
|
|
2213
|
+
const session = Session.create(dir, 'workspace');
|
|
2214
|
+
session.saveReport([], {
|
|
2215
|
+
state: 'complete', notLookedAt: [], filesReviewed: 2, filesChanged: 5,
|
|
2216
|
+
});
|
|
2217
|
+
const reopened = Session.open(dir, session.id);
|
|
2218
|
+
assert.equal(reopened?.report?.filesReviewed, 2);
|
|
2219
|
+
assert.equal(reopened?.report?.filesChanged, 5);
|
|
2220
|
+
rmSync(dir, { recursive: true, force: true });
|
|
2221
|
+
});
|
|
2193
2222
|
check('partial sessions cannot be compared or rendered as clean', () => {
|
|
2194
2223
|
const dir = mkdtempSync(join(tmpdir(), 'psh-partial-session-'));
|
|
2195
2224
|
const partial = Session.create(dir, 'workspace');
|
|
@@ -3475,6 +3504,7 @@ check('CLI rejects selections that would otherwise run nothing and report clean'
|
|
|
3475
3504
|
assert.equal(status(['scan', 'example.rb', '--verify-only', '--checks', 'plausible-logic', '--format', 'manifest']), 2);
|
|
3476
3505
|
assert.equal(status(['delegate', '--checks', 'phantom-api']), 2);
|
|
3477
3506
|
assert.equal(status(['delegate']), 0);
|
|
3507
|
+
assert.equal(status(['delegate', '--format', 'manifest']), 2);
|
|
3478
3508
|
assert.equal(existsSync(join(dir, '.powershot', 'sessions')), false, 'delegate must not create an unused session');
|
|
3479
3509
|
assert.equal(status(['scan', 'example.rb', '--format', 'unknown']), 2);
|
|
3480
3510
|
assert.equal(status(['scan', 'example.rb', '--report', 'unknown=report.txt']), 2);
|
|
@@ -3489,6 +3519,58 @@ check('CLI rejects selections that would otherwise run nothing and report clean'
|
|
|
3489
3519
|
rmSync(dir, { recursive: true, force: true });
|
|
3490
3520
|
}
|
|
3491
3521
|
});
|
|
3522
|
+
check('delegate JSON uses the review selection contract and names excluded files', () => {
|
|
3523
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'psh-delegate-plan-')));
|
|
3524
|
+
const cli = join(process.cwd(), 'dist', 'cli.js');
|
|
3525
|
+
try {
|
|
3526
|
+
execFileSync('git', ['init', '-q', '.'], { cwd: dir });
|
|
3527
|
+
writeFileSync(join(dir, 'source.rs'), 'pub fn value() -> i32 { 1 }\n');
|
|
3528
|
+
writeFileSync(join(dir, 'notes.md'), '# Notes\n');
|
|
3529
|
+
writeFileSync(join(dir, 'large.ts'), 'x'.repeat(512 * 1024 + 1));
|
|
3530
|
+
const raw = execFileSync(process.execPath, [cli, 'delegate', '--format', 'json'], {
|
|
3531
|
+
cwd: dir,
|
|
3532
|
+
env: { ...process.env, CI: 'true' },
|
|
3533
|
+
encoding: 'utf8',
|
|
3534
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
3535
|
+
});
|
|
3536
|
+
const task = JSON.parse(raw);
|
|
3537
|
+
const files = new Map(task.files.map((file) => [file.path, file]));
|
|
3538
|
+
assert.equal(task.schema, 'powershot.delegate/v1');
|
|
3539
|
+
assert.equal(task.state, 'complete');
|
|
3540
|
+
assert.equal(files.get('source.rs')?.disposition, 'selected');
|
|
3541
|
+
assert.equal(files.get('notes.md')?.disposition, 'waived');
|
|
3542
|
+
assert.match(files.get('notes.md')?.reason ?? '', /no parser/);
|
|
3543
|
+
assert.equal(files.get('large.ts')?.disposition, 'waived');
|
|
3544
|
+
assert.match(files.get('large.ts')?.reason ?? '', /over 512KB/);
|
|
3545
|
+
assert.ok(task.judges.some((judge) => judge.name === 'plausible-logic' && judge.applicable));
|
|
3546
|
+
assert.deepEqual([...new Set(task.units.flatMap((unit) => unit.files))], ['source.rs']);
|
|
3547
|
+
assert.match(task.units[0]?.changes ?? '', /pub fn value/);
|
|
3548
|
+
const markdown = execFileSync(process.execPath, [cli, 'delegate'], {
|
|
3549
|
+
cwd: dir,
|
|
3550
|
+
env: { ...process.env, CI: 'true' },
|
|
3551
|
+
encoding: 'utf8',
|
|
3552
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
3553
|
+
});
|
|
3554
|
+
assert.match(markdown, /## Scope/);
|
|
3555
|
+
assert.match(markdown, /source\.rs.*selected/);
|
|
3556
|
+
assert.match(markdown, /notes\.md.*no parser/);
|
|
3557
|
+
assert.match(markdown, /large\.ts.*over 512KB/);
|
|
3558
|
+
assert.ok(markdown.length < 100_000, 'an oversized source leaked into the delegate brief');
|
|
3559
|
+
assert.equal(existsSync(join(dir, '.powershot', 'sessions')), false, 'delegate must stay LLM-free');
|
|
3560
|
+
rmSync(join(dir, 'source.rs'));
|
|
3561
|
+
rmSync(join(dir, 'large.ts'));
|
|
3562
|
+
const waivedOnly = JSON.parse(execFileSync(process.execPath, [cli, 'delegate', '--format', 'json'], { cwd: dir, env: { ...process.env, CI: 'true' }, encoding: 'utf8' }));
|
|
3563
|
+
assert.equal(waivedOnly.state, 'complete');
|
|
3564
|
+
assert.deepEqual(waivedOnly.files, [{
|
|
3565
|
+
path: 'notes.md', disposition: 'waived', bytes: 8, addedLines: 2, language: 'other',
|
|
3566
|
+
reason: 'no parser for this language',
|
|
3567
|
+
}]);
|
|
3568
|
+
assert.deepEqual(waivedOnly.units, []);
|
|
3569
|
+
}
|
|
3570
|
+
finally {
|
|
3571
|
+
rmSync(dir, { recursive: true, force: true });
|
|
3572
|
+
}
|
|
3573
|
+
});
|
|
3492
3574
|
check('a budget stop is partial and the manifest names what was not reviewed', () => {
|
|
3493
3575
|
const b = new Budget({ requests: 1 }, 0);
|
|
3494
3576
|
b.spend({ requests: 1 });
|
package/dist/session.js
CHANGED
|
@@ -110,6 +110,7 @@ export class Session {
|
|
|
110
110
|
verifyOnly: verdict.verifyOnly,
|
|
111
111
|
minSeverity: verdict.minSeverity,
|
|
112
112
|
filesReviewed: verdict.filesReviewed,
|
|
113
|
+
filesChanged: verdict.filesChanged,
|
|
113
114
|
deterministicChecks: verdict.deterministicChecks,
|
|
114
115
|
scopeDetails: verdict.scopeDetails ? [...verdict.scopeDetails] : undefined,
|
|
115
116
|
};
|
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
|
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>",
|