@0xcraft/powershot 1.1.2 → 1.1.3
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 +3 -2
- 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/manifest.js +0 -17
- package/dist/package-smoke.js +4 -0
- package/dist/report/markdown.js +28 -16
- 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 +431 -18
- package/dist/session.js +6 -2
- package/docs/architecture.md +1 -0
- package/docs/ci.md +25 -0
- package/examples/github-actions/action.yml +4 -0
- package/package.json +1 -1
package/dist/report/markdown.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { modeNote, noFindingsLabel, scopeLine, } from './summary.js';
|
|
2
2
|
const MARK = { verified: '▣', judged: '▚' };
|
|
3
3
|
/** Untrusted prose encoded as literal CommonMark text. */
|
|
4
4
|
function text(s) {
|
|
@@ -51,6 +51,26 @@ function group(findings) {
|
|
|
51
51
|
list.sort((a, b) => a.line - b.line);
|
|
52
52
|
return out;
|
|
53
53
|
}
|
|
54
|
+
function runSummary(run) {
|
|
55
|
+
const out = [];
|
|
56
|
+
const scope = scopeLine(run);
|
|
57
|
+
const mode = modeNote(run, '`verify-only`');
|
|
58
|
+
const details = run.state === 'complete'
|
|
59
|
+
? run.scopeDetails ?? []
|
|
60
|
+
: [...run.notLookedAt, ...(run.scopeDetails ?? [])];
|
|
61
|
+
if (scope)
|
|
62
|
+
out.push(scope, '');
|
|
63
|
+
if (mode)
|
|
64
|
+
out.push(mode, '');
|
|
65
|
+
if (details.length > 0) {
|
|
66
|
+
out.push('<details>', '<summary>' +
|
|
67
|
+
(run.state !== 'complete'
|
|
68
|
+
? 'Why this is not a verdict'
|
|
69
|
+
: run.coverage === 'portable' ? 'Coverage details' : 'Review scope') +
|
|
70
|
+
'</summary>', '', ...details.map((detail) => '- ' + text(detail)), '', '</details>', '');
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
54
74
|
export function markdown(findings, run) {
|
|
55
75
|
// "No findings" from a run that could not look is the one thing this must never
|
|
56
76
|
// say on its own — the reader takes a comment at face value, and a red job beside
|
|
@@ -59,33 +79,25 @@ export function markdown(findings, run) {
|
|
|
59
79
|
const banner = incomplete
|
|
60
80
|
? [
|
|
61
81
|
'> [!WARNING]',
|
|
62
|
-
'> **This review is ' + text(run.state) + ' — not a verdict.**
|
|
63
|
-
...run.notLookedAt.slice(0, 8).map((f) => '> - ' + text(f)),
|
|
64
|
-
'',
|
|
65
|
-
]
|
|
66
|
-
: [];
|
|
67
|
-
const portable = run?.state === 'complete' && run.coverage === 'portable';
|
|
68
|
-
const coverage = portable
|
|
69
|
-
? [
|
|
70
|
-
'> [!NOTE]',
|
|
71
|
-
'> **Portable coverage.** Self-contained oracles ran; enriched semantic depth was unavailable:',
|
|
72
|
-
...unavailableCoverage(run).map((reason) => '> - ' + text(reason)),
|
|
82
|
+
'> **This review is ' + text(run.state) + ' — not a verdict.** Some files or checks were not reviewed.',
|
|
73
83
|
'',
|
|
74
84
|
]
|
|
75
85
|
: [];
|
|
86
|
+
const summary = run ? runSummary(run) : [];
|
|
76
87
|
if (findings.length === 0) {
|
|
77
88
|
return [
|
|
78
|
-
'## PowerShot', '', ...banner,
|
|
89
|
+
'## PowerShot', '', ...banner,
|
|
79
90
|
incomplete
|
|
80
91
|
? 'No findings *from what it managed to review*.'
|
|
81
|
-
:
|
|
82
|
-
'',
|
|
92
|
+
: run ? '✅ **' + noFindingsLabel(run) + '**' : 'No findings.',
|
|
93
|
+
'', ...summary,
|
|
83
94
|
].join('\n');
|
|
84
95
|
}
|
|
85
96
|
const verified = findings.filter((f) => f.class === 'verified').length;
|
|
86
97
|
const judged = findings.length - verified;
|
|
87
|
-
const out = ['## PowerShot', '', ...banner
|
|
98
|
+
const out = ['## PowerShot', '', ...banner];
|
|
88
99
|
out.push('**' + verified + ' verified** (deterministic, 0 tokens) · **' + judged + ' judged** (agent)', '');
|
|
100
|
+
out.push(...summary);
|
|
89
101
|
for (const [file, list] of group(findings)) {
|
|
90
102
|
out.push('### `' + path(file) + '`', '');
|
|
91
103
|
for (const f of list) {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
function plural(count, singular, pluralForm = singular + 's') {
|
|
2
|
+
return count + ' ' + (count === 1 ? singular : pluralForm);
|
|
3
|
+
}
|
|
4
|
+
function capabilityName(capability) {
|
|
5
|
+
return {
|
|
6
|
+
types: 'type information',
|
|
7
|
+
references: 'a reference graph',
|
|
8
|
+
'python-types': 'Python type information',
|
|
9
|
+
base: 'a base revision',
|
|
10
|
+
syntax: 'a syntax tree',
|
|
11
|
+
}[capability] ?? capability;
|
|
12
|
+
}
|
|
13
|
+
function naturalList(values, conjunction = 'and') {
|
|
14
|
+
if (values.length < 2)
|
|
15
|
+
return values[0] ?? '';
|
|
16
|
+
if (values.length === 2)
|
|
17
|
+
return values[0] + ' ' + conjunction + ' ' + values[1];
|
|
18
|
+
return values.slice(0, -1).join(', ') + ', ' + conjunction + ' ' + values.at(-1);
|
|
19
|
+
}
|
|
20
|
+
function scopeDetails(record) {
|
|
21
|
+
const details = [];
|
|
22
|
+
const selected = (record.files ?? []).filter((file) => file.disposition === 'selected');
|
|
23
|
+
const unavailableGroups = new Map();
|
|
24
|
+
for (const file of selected) {
|
|
25
|
+
if (!file.unavailable?.length)
|
|
26
|
+
continue;
|
|
27
|
+
const order = ['types', 'references', 'python-types', 'base', 'syntax'];
|
|
28
|
+
const capabilities = [...new Set(file.unavailable)].sort((left, right) => {
|
|
29
|
+
const leftRank = order.indexOf(left);
|
|
30
|
+
const rightRank = order.indexOf(right);
|
|
31
|
+
return (leftRank === -1 ? order.length : leftRank) - (rightRank === -1 ? order.length : rightRank) ||
|
|
32
|
+
left.localeCompare(right);
|
|
33
|
+
});
|
|
34
|
+
const key = capabilities.join('\0');
|
|
35
|
+
const group = unavailableGroups.get(key) ?? { capabilities, count: 0 };
|
|
36
|
+
group.count++;
|
|
37
|
+
unavailableGroups.set(key, group);
|
|
38
|
+
}
|
|
39
|
+
for (const group of unavailableGroups.values()) {
|
|
40
|
+
const capabilities = naturalList(group.capabilities.map(capabilityName));
|
|
41
|
+
details.push(plural(group.count, 'reviewed file') + ' lacked ' + capabilities + '.');
|
|
42
|
+
}
|
|
43
|
+
const unavailableChecks = record.checks?.unavailable ?? [];
|
|
44
|
+
if (unavailableChecks.length > 0) {
|
|
45
|
+
const shown = unavailableChecks.slice(0, 8).map((check) => check.check);
|
|
46
|
+
const requirements = [...new Set(unavailableChecks.flatMap((check) => check.missing.split(/,\s*/)))]
|
|
47
|
+
.map(capabilityName);
|
|
48
|
+
details.push(plural(unavailableChecks.length, 'check') + ' requiring ' + naturalList(requirements, 'or') +
|
|
49
|
+
' did not run: ' + shown.join(', ') +
|
|
50
|
+
(unavailableChecks.length > shown.length ? ', and ' + (unavailableChecks.length - shown.length) + ' more' : '') + '.');
|
|
51
|
+
}
|
|
52
|
+
const waived = new Map();
|
|
53
|
+
for (const file of record.files ?? []) {
|
|
54
|
+
if (file.disposition !== 'waived')
|
|
55
|
+
continue;
|
|
56
|
+
const reason = file.reason ?? 'unspecified reason';
|
|
57
|
+
waived.set(reason, (waived.get(reason) ?? 0) + 1);
|
|
58
|
+
}
|
|
59
|
+
for (const [reason, count] of waived) {
|
|
60
|
+
details.push(plural(count, 'changed file') + ' not reviewed: ' + reason + '.');
|
|
61
|
+
}
|
|
62
|
+
return details;
|
|
63
|
+
}
|
|
64
|
+
export function summarizeRun(record) {
|
|
65
|
+
const hasFiles = record.files !== undefined;
|
|
66
|
+
const hasChecks = record.checks?.ran !== undefined;
|
|
67
|
+
return {
|
|
68
|
+
state: record.state,
|
|
69
|
+
notLookedAt: [...record.notLookedAt],
|
|
70
|
+
coverage: record.coverage,
|
|
71
|
+
verifyOnly: record.engine?.verifyOnly,
|
|
72
|
+
minSeverity: record.engine?.minSeverity,
|
|
73
|
+
filesReviewed: hasFiles
|
|
74
|
+
? record.files.filter((file) => file.disposition === 'selected').length
|
|
75
|
+
: undefined,
|
|
76
|
+
deterministicChecks: hasChecks ? new Set(record.checks.ran).size : undefined,
|
|
77
|
+
scopeDetails: scopeDetails(record),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function noFindingsLabel(summary) {
|
|
81
|
+
const threshold = summary.minSeverity === undefined || summary.minSeverity === 'info'
|
|
82
|
+
? ''
|
|
83
|
+
: summary.minSeverity === 'critical'
|
|
84
|
+
? 'critical '
|
|
85
|
+
: summary.minSeverity + '-or-higher ';
|
|
86
|
+
const origin = summary.verifyOnly === true ? 'deterministic ' : '';
|
|
87
|
+
return 'No ' + threshold + origin + 'findings';
|
|
88
|
+
}
|
|
89
|
+
export function scopeLine(summary) {
|
|
90
|
+
const parts = [];
|
|
91
|
+
if (summary.filesReviewed !== undefined)
|
|
92
|
+
parts.push(plural(summary.filesReviewed, 'file') + ' reviewed');
|
|
93
|
+
if (summary.deterministicChecks !== undefined) {
|
|
94
|
+
parts.push(plural(summary.deterministicChecks, 'deterministic check'));
|
|
95
|
+
}
|
|
96
|
+
if (summary.coverage !== undefined)
|
|
97
|
+
parts.push(summary.coverage + ' coverage');
|
|
98
|
+
return parts.length > 0 ? parts.join(' · ') : undefined;
|
|
99
|
+
}
|
|
100
|
+
export function modeNote(summary, verifyOnly = 'verify-only') {
|
|
101
|
+
return summary.verifyOnly === true ? 'Model review was disabled (' + verifyOnly + ').' : undefined;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=summary.js.map
|
package/dist/report/terminal.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { bold, brightRed, dim, gray, red, steel, yellow } from './ansi.js';
|
|
2
2
|
import { highlight, isJsx } from './highlight.js';
|
|
3
|
+
import { modeNote, noFindingsLabel, scopeLine } from './summary.js';
|
|
3
4
|
const SEVERITY_COLOR = {
|
|
4
5
|
critical: brightRed,
|
|
5
6
|
high: red,
|
|
@@ -106,17 +107,22 @@ export function terminal(findings, opts) {
|
|
|
106
107
|
out.push(' ' + bold('PowerShot') + dim(' · ' + opts.subtitle));
|
|
107
108
|
out.push(rule);
|
|
108
109
|
const incomplete = opts.state !== 'complete';
|
|
109
|
-
const
|
|
110
|
+
const scope = scopeLine(opts);
|
|
111
|
+
const mode = modeNote(opts);
|
|
110
112
|
if (findings.length === 0) {
|
|
111
113
|
out.push('', incomplete
|
|
112
114
|
? ' ' + yellow('!') + ' No findings — but this review is ' + opts.state + ', not a verdict.'
|
|
113
|
-
: ' ' + steel('✔') +
|
|
115
|
+
: ' ' + steel('✔') + ' ' + noFindingsLabel(opts) + '.');
|
|
114
116
|
for (const reason of opts.notLookedAt)
|
|
115
117
|
out.push(dim(' ' + reason));
|
|
116
|
-
if (
|
|
117
|
-
out.push(dim('
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
if (scope)
|
|
119
|
+
out.push(dim(' ' + scope));
|
|
120
|
+
if (mode)
|
|
121
|
+
out.push(dim(' ' + mode));
|
|
122
|
+
if ((opts.scopeDetails?.length ?? 0) > 0) {
|
|
123
|
+
out.push(dim(' ' + (opts.coverage === 'portable' ? 'Coverage details:' : 'Review scope:')));
|
|
124
|
+
for (const detail of opts.scopeDetails)
|
|
125
|
+
out.push(dim(' - ' + detail));
|
|
120
126
|
}
|
|
121
127
|
out.push('');
|
|
122
128
|
return out.join('\n');
|
|
@@ -152,11 +158,12 @@ export function terminal(findings, opts) {
|
|
|
152
158
|
for (const reason of opts.notLookedAt)
|
|
153
159
|
out.push(dim(' ' + reason));
|
|
154
160
|
}
|
|
155
|
-
|
|
156
|
-
out.push(' ' + steel('◇
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
if (scope)
|
|
162
|
+
out.push(' ' + steel('◇ ') + dim(scope));
|
|
163
|
+
if (mode)
|
|
164
|
+
out.push(dim(' ' + mode));
|
|
165
|
+
for (const detail of opts.scopeDetails ?? [])
|
|
166
|
+
out.push(dim(' ' + detail));
|
|
160
167
|
out.push('');
|
|
161
168
|
return out.join('\n');
|
|
162
169
|
}
|
package/dist/report/viewer.js
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
|
+
import { modeNote, noFindingsLabel, scopeLine } from './summary.js';
|
|
1
2
|
const escape = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
2
3
|
export function viewer(findings, meta) {
|
|
3
4
|
const verified = findings.filter((f) => f.class === 'verified').length;
|
|
4
5
|
const incomplete = meta.state !== 'complete';
|
|
5
|
-
const
|
|
6
|
+
const scope = scopeLine(meta);
|
|
7
|
+
const mode = modeNote(meta);
|
|
6
8
|
const warning = incomplete
|
|
7
9
|
? '<div class="warning"><strong>This review is ' + escape(meta.state) + ' — not a verdict.</strong>' +
|
|
8
10
|
(meta.notLookedAt.length > 0
|
|
9
11
|
? '<ul>' + meta.notLookedAt.map((reason) => '<li>' + escape(reason) + '</li>').join('') + '</ul>'
|
|
10
12
|
: '') + '</div>'
|
|
11
13
|
: '';
|
|
12
|
-
const
|
|
13
|
-
? '<div class="coverage"
|
|
14
|
-
(
|
|
15
|
-
|
|
14
|
+
const context = scope || mode || (meta.scopeDetails?.length ?? 0) > 0
|
|
15
|
+
? '<div class="coverage">' +
|
|
16
|
+
(scope ? '<strong>' + escape(scope) + '</strong>' : '') +
|
|
17
|
+
(mode ? '<p>' + escape(mode) + '</p>' : '') +
|
|
18
|
+
((meta.scopeDetails?.length ?? 0) > 0
|
|
19
|
+
? '<details><summary>' + (meta.coverage === 'portable' ? 'Coverage details' : 'Review scope') + '</summary><ul>' +
|
|
20
|
+
meta.scopeDetails.map((detail) => '<li>' + escape(detail) + '</li>').join('') +
|
|
21
|
+
'</ul></details>'
|
|
16
22
|
: '') + '</div>'
|
|
17
23
|
: '';
|
|
24
|
+
const verdict = findings.length === 0
|
|
25
|
+
? '<p class="none">' +
|
|
26
|
+
(incomplete ? 'No findings from what completed.' : escape(noFindingsLabel(meta)) + '.') + '</p>'
|
|
27
|
+
: '';
|
|
18
28
|
const rows = findings
|
|
19
29
|
.map((f) => {
|
|
20
30
|
const frame = f.frame
|
|
@@ -91,16 +101,15 @@ export function viewer(findings, meta) {
|
|
|
91
101
|
<p class="meta">${escape(meta.target)} · ${escape(meta.started.slice(0, 19).replace('T', ' '))} · session ${escape(meta.id)}<br>
|
|
92
102
|
${findings.length} finding(s) — ${verified} verified, ${findings.length - verified} judged</p>
|
|
93
103
|
${warning}
|
|
94
|
-
${
|
|
104
|
+
${verdict}
|
|
105
|
+
${context}
|
|
95
106
|
<div class="bar">
|
|
96
107
|
<button data-filter="all" aria-pressed="true">all</button>
|
|
97
108
|
<button data-filter="verified" aria-pressed="false">verified</button>
|
|
98
109
|
<button data-filter="judged" aria-pressed="false">judged</button>
|
|
99
110
|
<button id="show-away" aria-pressed="false">show put away</button>
|
|
100
111
|
</div>
|
|
101
|
-
${findings.length === 0
|
|
102
|
-
? '<p class="none">' + (incomplete ? 'No findings from what completed.' : portable ? 'No findings in portable coverage.' : 'No findings.') + '</p>'
|
|
103
|
-
: rows}
|
|
112
|
+
${findings.length === 0 ? '' : rows}
|
|
104
113
|
</div>
|
|
105
114
|
<script>
|
|
106
115
|
// Putting a finding away is per-reader and per-browser on purpose: this page is a
|