@0xcraft/powershot 1.1.0 → 1.1.2
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 +43 -18
- package/dist/cli/reports.js +3 -0
- package/dist/cli/review-command.js +8 -2
- package/dist/cli/session-command.js +2 -0
- package/dist/config.js +5 -0
- package/dist/ground.js +297 -95
- package/dist/judges/tools.js +7 -7
- package/dist/lang/packs.js +97 -14
- package/dist/lang/parse-worker.js +15 -0
- package/dist/lang/python-deps.js +21 -8
- package/dist/manifest.js +49 -0
- package/dist/plan.js +7 -0
- package/dist/report/markdown.js +18 -2
- package/dist/report/terminal.js +12 -1
- package/dist/report/viewer.js +11 -1
- package/dist/review.js +43 -20
- package/dist/selftest.js +402 -8
- package/dist/session.js +2 -0
- package/dist/verifiers/foreign-phantom-dep.js +8 -2
- package/dist/verifiers/phantom-config.js +7 -6
- package/docs/architecture.md +52 -12
- package/docs/ci.md +21 -4
- package/examples/github-actions/cli.yml +1 -1
- package/examples/gitlab/.gitlab-ci.yml +1 -1
- package/package.json +1 -1
package/dist/lang/python-deps.js
CHANGED
|
@@ -135,11 +135,20 @@ const SKIP_DIRS = new Set([
|
|
|
135
135
|
'node_modules', '.git', '.venv', 'venv', 'env', '__pycache__', 'dist', 'build',
|
|
136
136
|
'.mypy_cache', '.pytest_cache', '.tox', 'site-packages', 'target', '.next',
|
|
137
137
|
]);
|
|
138
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Importable names near one changed Python file.
|
|
140
|
+
*
|
|
141
|
+
* Only direct entries in its ancestor chain and conventional source roots are read.
|
|
142
|
+
* That bounds work by path depth instead of repository size, while still covering
|
|
143
|
+
* `src/pkg` beside `tests/test_pkg.py` and namespace packages without __init__.py.
|
|
144
|
+
*/
|
|
145
|
+
export function localModules(root, from = root) {
|
|
139
146
|
const local = new Set();
|
|
140
|
-
const
|
|
141
|
-
|
|
147
|
+
const scanned = new Set();
|
|
148
|
+
const inspect = (dir) => {
|
|
149
|
+
if (scanned.has(dir))
|
|
142
150
|
return;
|
|
151
|
+
scanned.add(dir);
|
|
143
152
|
let entries;
|
|
144
153
|
try {
|
|
145
154
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -147,21 +156,25 @@ export function localModules(root) {
|
|
|
147
156
|
catch {
|
|
148
157
|
return;
|
|
149
158
|
}
|
|
150
|
-
const isPackage = entries.some((e) => e.isFile() && e.name === '__init__.py');
|
|
151
159
|
for (const entry of entries) {
|
|
152
160
|
if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name))
|
|
153
161
|
continue;
|
|
154
162
|
if (entry.isDirectory()) {
|
|
155
|
-
//
|
|
163
|
+
// Namespace packages are importable without __init__.py too.
|
|
156
164
|
local.add(entry.name);
|
|
157
|
-
walk(join(dir, entry.name), depth + 1);
|
|
158
165
|
}
|
|
159
|
-
else if (entry.name.endsWith('.py') &&
|
|
166
|
+
else if (entry.isFile() && entry.name.endsWith('.py') && entry.name !== '__init__.py') {
|
|
160
167
|
local.add(entry.name.slice(0, -3));
|
|
161
168
|
}
|
|
162
169
|
}
|
|
163
170
|
};
|
|
164
|
-
|
|
171
|
+
for (let dir = from;; dir = dirname(dir)) {
|
|
172
|
+
inspect(dir);
|
|
173
|
+
for (const source of ['src', 'lib', 'python'])
|
|
174
|
+
inspect(join(dir, source));
|
|
175
|
+
if (dir === root || dirname(dir) === dir)
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
165
178
|
return local;
|
|
166
179
|
}
|
|
167
180
|
export function isPhantom(importName, manifest, local) {
|
package/dist/manifest.js
CHANGED
|
@@ -2,6 +2,23 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
export const SCHEMA = 'powershot.run/v1';
|
|
5
|
+
/** Human-readable optional depth, kept separate from verdict-blocking notLookedAt. */
|
|
6
|
+
export function unavailableCoverage(record) {
|
|
7
|
+
const out = [];
|
|
8
|
+
const files = (record.files ?? []).filter((file) => file.unavailable?.length);
|
|
9
|
+
if (files.length > 0) {
|
|
10
|
+
out.push(files.length + ' file(s) without enriched semantic coverage: ' +
|
|
11
|
+
files.slice(0, 5).map((file) => file.path + ' (' + file.unavailable.join(', ') + ')').join(', ') +
|
|
12
|
+
(files.length > 5 ? ', …' : ''));
|
|
13
|
+
}
|
|
14
|
+
const checks = record.checks?.unavailable ?? [];
|
|
15
|
+
if (checks.length > 0) {
|
|
16
|
+
out.push(checks.length + ' enriched check(s) unavailable: ' +
|
|
17
|
+
checks.slice(0, 8).map((check) => check.check + ' (no ' + check.missing + ')').join(', ') +
|
|
18
|
+
(checks.length > 8 ? ', …' : ''));
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
5
22
|
/** The single state machine behind manifests, benches, renderers and exit codes. */
|
|
6
23
|
export function completionOf(parts) {
|
|
7
24
|
const waivedUnits = parts.units.filter((unit) => unit.outcome === 'waived').length;
|
|
@@ -65,12 +82,20 @@ export class RunManifest {
|
|
|
65
82
|
...file,
|
|
66
83
|
checks: [...file.checks],
|
|
67
84
|
missing: file.missing ? [...file.missing] : undefined,
|
|
85
|
+
unavailable: file.unavailable ? [...file.unavailable] : undefined,
|
|
68
86
|
})),
|
|
69
87
|
units: this.units.map((unit) => ({ ...unit })),
|
|
70
88
|
checks: {
|
|
71
89
|
ran: [...this.ranChecks],
|
|
72
90
|
skipped: parts.skippedChecks.map((check) => ({ ...check })),
|
|
91
|
+
...((parts.unavailableChecks?.length ?? 0) > 0
|
|
92
|
+
? { unavailable: parts.unavailableChecks.map((check) => ({ ...check })) }
|
|
93
|
+
: {}),
|
|
73
94
|
},
|
|
95
|
+
coverage: parts.files.some((file) => file.missing?.length || file.unavailable?.length) ||
|
|
96
|
+
parts.skippedChecks.length > 0 || (parts.unavailableChecks?.length ?? 0) > 0
|
|
97
|
+
? 'portable'
|
|
98
|
+
: 'full',
|
|
74
99
|
findings: { ...parts.findings },
|
|
75
100
|
usage: { ...parts.usage },
|
|
76
101
|
state: completion.state,
|
|
@@ -118,6 +143,15 @@ export function coverageProblems(m) {
|
|
|
118
143
|
if (f.disposition !== 'selected' && f.checks.length > 0) {
|
|
119
144
|
problems.push(f.path + ': ' + f.disposition + ' file received checks');
|
|
120
145
|
}
|
|
146
|
+
if (f.disposition !== 'selected' && f.unavailable?.length) {
|
|
147
|
+
problems.push(f.path + ': ' + f.disposition + ' file has unavailable coverage');
|
|
148
|
+
}
|
|
149
|
+
const missingCaps = new Set(f.missing ?? []);
|
|
150
|
+
for (const capability of f.unavailable ?? []) {
|
|
151
|
+
if (missingCaps.has(capability)) {
|
|
152
|
+
problems.push(f.path + ': capability is both required and unavailable: ' + capability);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
121
155
|
const local = new Set();
|
|
122
156
|
for (const check of f.checks) {
|
|
123
157
|
if (local.has(check))
|
|
@@ -157,6 +191,21 @@ export function coverageProblems(m) {
|
|
|
157
191
|
if (ran.has(check.check))
|
|
158
192
|
problems.push('check counted as both ran and skipped: ' + check.check);
|
|
159
193
|
}
|
|
194
|
+
const unavailable = new Set();
|
|
195
|
+
for (const check of m.checks.unavailable ?? []) {
|
|
196
|
+
if (unavailable.has(check.check))
|
|
197
|
+
problems.push('check counted twice as unavailable: ' + check.check);
|
|
198
|
+
unavailable.add(check.check);
|
|
199
|
+
if (skipped.has(check.check))
|
|
200
|
+
problems.push('check counted as both skipped and unavailable: ' + check.check);
|
|
201
|
+
}
|
|
202
|
+
const expectedCoverage = m.files.some((file) => file.missing?.length || file.unavailable?.length) ||
|
|
203
|
+
m.checks.skipped.length > 0 || (m.checks.unavailable?.length ?? 0) > 0
|
|
204
|
+
? 'portable'
|
|
205
|
+
: 'full';
|
|
206
|
+
if (m.coverage !== undefined && m.coverage !== expectedCoverage) {
|
|
207
|
+
problems.push('coverage is ' + m.coverage + ' but accounting says ' + expectedCoverage);
|
|
208
|
+
}
|
|
160
209
|
// a judged run that reports complete must have reached every unit it selected
|
|
161
210
|
const unreached = m.units.filter((u) => u.outcome === 'failed' || u.outcome === 'waived');
|
|
162
211
|
if (m.state === 'complete' && unreached.length > 0) {
|
package/dist/plan.js
CHANGED
|
@@ -67,6 +67,13 @@ export class SelectionPlan {
|
|
|
67
67
|
row.missing = [...new Set([...(row.missing ?? []), ...missing])];
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
/** Record optional semantic depth that this environment could not provide. */
|
|
71
|
+
noteUnavailable(path, unavailable) {
|
|
72
|
+
const row = this.rows.get(path);
|
|
73
|
+
if (row && row.disposition === 'selected' && unavailable.length > 0) {
|
|
74
|
+
row.unavailable = [...new Set([...(row.unavailable ?? []), ...unavailable])];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
70
77
|
/** Record coverage at the same file granularity used to decide applicability. */
|
|
71
78
|
checked(path, check) {
|
|
72
79
|
const row = this.rows.get(path);
|
package/dist/report/markdown.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unavailableCoverage } from '#app/manifest.js';
|
|
1
2
|
const MARK = { verified: '▣', judged: '▚' };
|
|
2
3
|
/** Untrusted prose encoded as literal CommonMark text. */
|
|
3
4
|
function text(s) {
|
|
@@ -63,12 +64,27 @@ export function markdown(findings, run) {
|
|
|
63
64
|
'',
|
|
64
65
|
]
|
|
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)),
|
|
73
|
+
'',
|
|
74
|
+
]
|
|
75
|
+
: [];
|
|
66
76
|
if (findings.length === 0) {
|
|
67
|
-
return [
|
|
77
|
+
return [
|
|
78
|
+
'## PowerShot', '', ...banner, ...coverage,
|
|
79
|
+
incomplete
|
|
80
|
+
? 'No findings *from what it managed to review*.'
|
|
81
|
+
: portable ? 'No findings in portable coverage.' : 'No findings.',
|
|
82
|
+
'',
|
|
83
|
+
].join('\n');
|
|
68
84
|
}
|
|
69
85
|
const verified = findings.filter((f) => f.class === 'verified').length;
|
|
70
86
|
const judged = findings.length - verified;
|
|
71
|
-
const out = ['## PowerShot', '', ...banner];
|
|
87
|
+
const out = ['## PowerShot', '', ...banner, ...coverage];
|
|
72
88
|
out.push('**' + verified + ' verified** (deterministic, 0 tokens) · **' + judged + ' judged** (agent)', '');
|
|
73
89
|
for (const [file, list] of group(findings)) {
|
|
74
90
|
out.push('### `' + path(file) + '`', '');
|
package/dist/report/terminal.js
CHANGED
|
@@ -106,12 +106,18 @@ export function terminal(findings, opts) {
|
|
|
106
106
|
out.push(' ' + bold('PowerShot') + dim(' · ' + opts.subtitle));
|
|
107
107
|
out.push(rule);
|
|
108
108
|
const incomplete = opts.state !== 'complete';
|
|
109
|
+
const portable = !incomplete && opts.coverage === 'portable';
|
|
109
110
|
if (findings.length === 0) {
|
|
110
111
|
out.push('', incomplete
|
|
111
112
|
? ' ' + yellow('!') + ' No findings — but this review is ' + opts.state + ', not a verdict.'
|
|
112
|
-
: ' ' + steel('✔') + ' No findings.');
|
|
113
|
+
: ' ' + steel('✔') + (portable ? ' No findings in portable coverage.' : ' No findings.'));
|
|
113
114
|
for (const reason of opts.notLookedAt)
|
|
114
115
|
out.push(dim(' ' + reason));
|
|
116
|
+
if (portable) {
|
|
117
|
+
out.push(dim(' Portable coverage: self-contained oracles ran; enriched semantic depth was unavailable.'));
|
|
118
|
+
for (const reason of opts.unavailableCoverage ?? [])
|
|
119
|
+
out.push(dim(' ' + reason));
|
|
120
|
+
}
|
|
115
121
|
out.push('');
|
|
116
122
|
return out.join('\n');
|
|
117
123
|
}
|
|
@@ -146,6 +152,11 @@ export function terminal(findings, opts) {
|
|
|
146
152
|
for (const reason of opts.notLookedAt)
|
|
147
153
|
out.push(dim(' ' + reason));
|
|
148
154
|
}
|
|
155
|
+
else if (portable) {
|
|
156
|
+
out.push(' ' + steel('◇ portable coverage') + dim(' · enriched semantic depth was unavailable'));
|
|
157
|
+
for (const reason of opts.unavailableCoverage ?? [])
|
|
158
|
+
out.push(dim(' ' + reason));
|
|
159
|
+
}
|
|
149
160
|
out.push('');
|
|
150
161
|
return out.join('\n');
|
|
151
162
|
}
|
package/dist/report/viewer.js
CHANGED
|
@@ -2,12 +2,19 @@ const escape = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/
|
|
|
2
2
|
export function viewer(findings, meta) {
|
|
3
3
|
const verified = findings.filter((f) => f.class === 'verified').length;
|
|
4
4
|
const incomplete = meta.state !== 'complete';
|
|
5
|
+
const portable = !incomplete && meta.coverage === 'portable';
|
|
5
6
|
const warning = incomplete
|
|
6
7
|
? '<div class="warning"><strong>This review is ' + escape(meta.state) + ' — not a verdict.</strong>' +
|
|
7
8
|
(meta.notLookedAt.length > 0
|
|
8
9
|
? '<ul>' + meta.notLookedAt.map((reason) => '<li>' + escape(reason) + '</li>').join('') + '</ul>'
|
|
9
10
|
: '') + '</div>'
|
|
10
11
|
: '';
|
|
12
|
+
const coverage = portable
|
|
13
|
+
? '<div class="coverage"><strong>Portable coverage.</strong> Self-contained oracles ran; enriched semantic depth was unavailable.' +
|
|
14
|
+
((meta.unavailableCoverage?.length ?? 0) > 0
|
|
15
|
+
? '<ul>' + meta.unavailableCoverage.map((reason) => '<li>' + escape(reason) + '</li>').join('') + '</ul>'
|
|
16
|
+
: '') + '</div>'
|
|
17
|
+
: '';
|
|
11
18
|
const rows = findings
|
|
12
19
|
.map((f) => {
|
|
13
20
|
const frame = f.frame
|
|
@@ -70,6 +77,8 @@ export function viewer(findings, meta) {
|
|
|
70
77
|
.none { color:var(--muted); }
|
|
71
78
|
.warning { border:1px solid var(--amber); border-radius:6px; padding:10px 14px; margin:0 0 18px; }
|
|
72
79
|
.warning ul { margin:6px 0 0; }
|
|
80
|
+
.coverage { border:1px solid var(--steel); border-radius:6px; padding:10px 14px; margin:0 0 18px; }
|
|
81
|
+
.coverage ul { margin:6px 0 0; }
|
|
73
82
|
.f.put-away { opacity:.4; }
|
|
74
83
|
.f.put-away .title { text-decoration:line-through; }
|
|
75
84
|
.act { margin-left:8px; }
|
|
@@ -82,6 +91,7 @@ export function viewer(findings, meta) {
|
|
|
82
91
|
<p class="meta">${escape(meta.target)} · ${escape(meta.started.slice(0, 19).replace('T', ' '))} · session ${escape(meta.id)}<br>
|
|
83
92
|
${findings.length} finding(s) — ${verified} verified, ${findings.length - verified} judged</p>
|
|
84
93
|
${warning}
|
|
94
|
+
${coverage}
|
|
85
95
|
<div class="bar">
|
|
86
96
|
<button data-filter="all" aria-pressed="true">all</button>
|
|
87
97
|
<button data-filter="verified" aria-pressed="false">verified</button>
|
|
@@ -89,7 +99,7 @@ export function viewer(findings, meta) {
|
|
|
89
99
|
<button id="show-away" aria-pressed="false">show put away</button>
|
|
90
100
|
</div>
|
|
91
101
|
${findings.length === 0
|
|
92
|
-
? '<p class="none">' + (incomplete ? 'No findings from what completed.' : 'No findings.') + '</p>'
|
|
102
|
+
? '<p class="none">' + (incomplete ? 'No findings from what completed.' : portable ? 'No findings in portable coverage.' : 'No findings.') + '</p>'
|
|
93
103
|
: rows}
|
|
94
104
|
</div>
|
|
95
105
|
<script>
|
package/dist/review.js
CHANGED
|
@@ -2,7 +2,6 @@ import { buildGround } from './ground.js';
|
|
|
2
2
|
import { baseRefOf, collectChanges, statedIntent } from './git.js';
|
|
3
3
|
import { bundle, bundleName, reviewables, uncovered } from './bundle.js';
|
|
4
4
|
import { attachFrames, positionable } from './position.js';
|
|
5
|
-
import { skippedLanguages } from './lang/packs.js';
|
|
6
5
|
import { JudgeCache } from './cache.js';
|
|
7
6
|
import { Dismissals, rememberReport } from './dismissed.js';
|
|
8
7
|
import { renderChanges } from './judges/judge.js';
|
|
@@ -16,7 +15,6 @@ import { Budget } from './budget.js';
|
|
|
16
15
|
import { packFor } from './lang/packs.js';
|
|
17
16
|
import { SEVERITIES } from './types.js';
|
|
18
17
|
import { stripControl, stripPath } from './text.js';
|
|
19
|
-
const packOf = (path) => packFor(path)?.name ?? 'other';
|
|
20
18
|
export function atLeast(severity, min) {
|
|
21
19
|
return SEVERITIES.indexOf(severity) >= SEVERITIES.indexOf(min);
|
|
22
20
|
}
|
|
@@ -33,20 +31,24 @@ export function titleOverlap(a, b) {
|
|
|
33
31
|
shared++;
|
|
34
32
|
return shared / Math.min(left.size, right.size);
|
|
35
33
|
}
|
|
34
|
+
const PORTABLE_OPTIONAL = new Set(['types', 'references', 'python-types']);
|
|
36
35
|
/**
|
|
37
36
|
* Files this verifier can actually answer for, with unavailable oracles kept per
|
|
38
|
-
* file.
|
|
39
|
-
*
|
|
37
|
+
* file. A before/after check has no question to ask about a newly created file;
|
|
38
|
+
* when an existing file has a base snapshot that cannot be parsed, `base` is a real
|
|
39
|
+
* missing capability and remains verdict-blocking in every coverage profile.
|
|
40
40
|
*/
|
|
41
41
|
function verifierTargets(v, g, have) {
|
|
42
42
|
if (v.domain === 'typescript') {
|
|
43
43
|
return g.files
|
|
44
|
-
.filter((file) => !v.needs.includes('base') || file.before !== undefined)
|
|
44
|
+
.filter((file) => !v.needs.includes('base') || file.changed.before !== undefined)
|
|
45
45
|
.map((file) => ({
|
|
46
46
|
kind: 'typescript',
|
|
47
47
|
path: file.changed.path,
|
|
48
48
|
file,
|
|
49
49
|
missing: v.needs.filter((need) => {
|
|
50
|
+
if (need === 'base')
|
|
51
|
+
return file.before === undefined;
|
|
50
52
|
if (need === 'types' || need === 'references')
|
|
51
53
|
return !file.typed;
|
|
52
54
|
if (need === 'python-types')
|
|
@@ -59,12 +61,14 @@ function verifierTargets(v, g, have) {
|
|
|
59
61
|
return g.foreign
|
|
60
62
|
.filter((file) => v.domain !== 'python' || file.pack.name === 'python')
|
|
61
63
|
.filter((file) => !v.supports || v.supports(file))
|
|
62
|
-
.filter((file) => !v.needs.includes('base') || file.
|
|
64
|
+
.filter((file) => !v.needs.includes('base') || file.changed.before !== undefined)
|
|
63
65
|
.map((file) => ({
|
|
64
66
|
kind: 'foreign',
|
|
65
67
|
path: file.path,
|
|
66
68
|
file,
|
|
67
69
|
missing: v.needs.filter((need) => {
|
|
70
|
+
if (need === 'base')
|
|
71
|
+
return file.beforeTree === undefined;
|
|
68
72
|
if (need === 'python-types')
|
|
69
73
|
return file.pack.name !== 'python' || !have.has('python-types');
|
|
70
74
|
if (need === 'types' || need === 'references')
|
|
@@ -142,12 +146,15 @@ export async function review(opts) {
|
|
|
142
146
|
return { findings: [], stats: { files: 0, verified: 0, judged: 0, dismissed: 0 }, failures, plan };
|
|
143
147
|
}
|
|
144
148
|
const skipped = new Map();
|
|
149
|
+
const unavailable = new Map();
|
|
145
150
|
const budget = opts.budget ?? new Budget();
|
|
146
151
|
const manifest = opts.manifest;
|
|
147
152
|
const groundDone = stage('ground');
|
|
148
153
|
const g = await buildGround(root, changed, opts.signal);
|
|
149
|
-
groundDone(g.
|
|
150
|
-
(g.
|
|
154
|
+
groundDone(g.sourceFiles.length + ' files · ' + g.symbolIndex.size + ' symbols' +
|
|
155
|
+
(g.configFiles.length === 0
|
|
156
|
+
? ' · no usable relevant tsconfig, type checks disabled'
|
|
157
|
+
: ' · ' + g.configFiles.length + ' project(s)' + (g.typed ? '' : ' · type environment incomplete')));
|
|
151
158
|
const wanted = (v) => {
|
|
152
159
|
const id = v.id ?? v.name;
|
|
153
160
|
return opts.checks
|
|
@@ -164,27 +171,33 @@ export async function review(opts) {
|
|
|
164
171
|
targets.set(verifier, files);
|
|
165
172
|
}
|
|
166
173
|
const selectedVerifiers = [...targets.keys()];
|
|
174
|
+
// Naming a check explicitly is a request for that oracle, even under the portable
|
|
175
|
+
// default. Strict policy makes the same promise for every configured verifier.
|
|
176
|
+
const requireEnrichedOracles = config.coverage === 'strict' || opts.checks !== undefined;
|
|
167
177
|
// a file the change touched that no parser produced a tree for was not reviewed,
|
|
168
178
|
// whatever the summary says about the ones that were
|
|
169
179
|
const grounded = new Set([...g.files.map((f) => f.changed.path), ...g.foreign.map((f) => f.path)]);
|
|
170
180
|
for (const c of changed) {
|
|
171
|
-
if (
|
|
181
|
+
if (grounded.has(c.path))
|
|
182
|
+
continue;
|
|
183
|
+
if (packFor(c.path))
|
|
184
|
+
plan.fail(c.path, 'declared language parser unavailable');
|
|
185
|
+
else
|
|
172
186
|
plan.waive(c.path, 'no parser for this language');
|
|
173
187
|
}
|
|
174
188
|
// Capabilities belong to files, not runs. A typed file beside one excluded from
|
|
175
189
|
// tsconfig must not make the latter look checked, and an old Ruby file must not
|
|
176
190
|
// make a new Python file eligible for a before/after oracle.
|
|
177
191
|
for (const files of targets.values()) {
|
|
178
|
-
for (const file of files)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
const required = file.missing.filter((capability) => !PORTABLE_OPTIONAL.has(capability));
|
|
194
|
+
const enriched = file.missing.filter((capability) => PORTABLE_OPTIONAL.has(capability));
|
|
195
|
+
plan.limit(file.path, required);
|
|
196
|
+
if (requireEnrichedOracles)
|
|
197
|
+
plan.limit(file.path, enriched);
|
|
198
|
+
else
|
|
199
|
+
plan.noteUnavailable(file.path, enriched);
|
|
186
200
|
}
|
|
187
|
-
failures.push('not reviewed, grammar budget reached: ' + skippedLanguages.join(', '));
|
|
188
201
|
}
|
|
189
202
|
for (const line of plan.summary())
|
|
190
203
|
say('selection ' + line);
|
|
@@ -199,8 +212,14 @@ export async function review(opts) {
|
|
|
199
212
|
const files = targets.get(v);
|
|
200
213
|
const eligible = files.filter((file) => file.missing.length === 0);
|
|
201
214
|
const missing = [...new Set(files.flatMap((file) => file.missing))];
|
|
215
|
+
const check = v.id ?? v.name;
|
|
216
|
+
const onlyEnrichedMissing = missing.every((capability) => PORTABLE_OPTIONAL.has(capability));
|
|
217
|
+
if (missing.length > 0 && !requireEnrichedOracles && onlyEnrichedMissing) {
|
|
218
|
+
unavailable.set(check, missing.join(', '));
|
|
219
|
+
}
|
|
202
220
|
if (missing.length > 0 && eligible.length === 0) {
|
|
203
|
-
|
|
221
|
+
if (requireEnrichedOracles || !onlyEnrichedMissing)
|
|
222
|
+
skipped.set(check, missing.join(', '));
|
|
204
223
|
continue;
|
|
205
224
|
}
|
|
206
225
|
// a scan spends most of its time here, so Ctrl-C has to reach this half
|
|
@@ -209,7 +228,6 @@ export async function review(opts) {
|
|
|
209
228
|
break;
|
|
210
229
|
}
|
|
211
230
|
ran++;
|
|
212
|
-
const check = v.id ?? v.name;
|
|
213
231
|
manifest?.ran(check);
|
|
214
232
|
for (const file of eligible)
|
|
215
233
|
plan.checked(file.path, check);
|
|
@@ -225,6 +243,10 @@ export async function review(opts) {
|
|
|
225
243
|
const names = [...skipped].map(([n, why]) => n + ' (no ' + why + ')');
|
|
226
244
|
say('skipped ' + names.join(', '));
|
|
227
245
|
}
|
|
246
|
+
if (unavailable.size > 0) {
|
|
247
|
+
const names = [...unavailable].map(([n, why]) => n + ' (no ' + why + ')');
|
|
248
|
+
say('coverage portable · enriched checks unavailable: ' + names.join(', '));
|
|
249
|
+
}
|
|
228
250
|
// --checks overrides the config rather than filtering it
|
|
229
251
|
const isGated = range.from !== undefined || range.commit !== undefined;
|
|
230
252
|
const judgeCache = opts.cache === false || verifyOnly ? undefined : JudgeCache.open(repo, isGated);
|
|
@@ -346,6 +368,7 @@ export async function review(opts) {
|
|
|
346
368
|
failures,
|
|
347
369
|
plan,
|
|
348
370
|
skippedChecks: [...skipped].map(([check, missing]) => ({ check, missing })),
|
|
371
|
+
unavailableChecks: [...unavailable].map(([check, missing]) => ({ check, missing })),
|
|
349
372
|
usage: budget.finish(),
|
|
350
373
|
budgetStop,
|
|
351
374
|
cancelled: opts.signal?.aborted ?? false,
|