@0xcraft/powershot 1.1.1 → 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.
@@ -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
@@ -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,12 +107,23 @@ 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';
110
+ const scope = scopeLine(opts);
111
+ const mode = modeNote(opts);
109
112
  if (findings.length === 0) {
110
113
  out.push('', incomplete
111
114
  ? ' ' + yellow('!') + ' No findings — but this review is ' + opts.state + ', not a verdict.'
112
- : ' ' + steel('✔') + ' No findings.');
115
+ : ' ' + steel('✔') + ' ' + noFindingsLabel(opts) + '.');
113
116
  for (const reason of opts.notLookedAt)
114
117
  out.push(dim(' ' + reason));
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));
126
+ }
115
127
  out.push('');
116
128
  return out.join('\n');
117
129
  }
@@ -146,6 +158,12 @@ export function terminal(findings, opts) {
146
158
  for (const reason of opts.notLookedAt)
147
159
  out.push(dim(' ' + reason));
148
160
  }
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));
149
167
  out.push('');
150
168
  return out.join('\n');
151
169
  }
@@ -1,13 +1,30 @@
1
+ import { modeNote, noFindingsLabel, scopeLine } from './summary.js';
1
2
  const escape = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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';
6
+ const scope = scopeLine(meta);
7
+ const mode = modeNote(meta);
5
8
  const warning = incomplete
6
9
  ? '<div class="warning"><strong>This review is ' + escape(meta.state) + ' — not a verdict.</strong>' +
7
10
  (meta.notLookedAt.length > 0
8
11
  ? '<ul>' + meta.notLookedAt.map((reason) => '<li>' + escape(reason) + '</li>').join('') + '</ul>'
9
12
  : '') + '</div>'
10
13
  : '';
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>'
22
+ : '') + '</div>'
23
+ : '';
24
+ const verdict = findings.length === 0
25
+ ? '<p class="none">' +
26
+ (incomplete ? 'No findings from what completed.' : escape(noFindingsLabel(meta)) + '.') + '</p>'
27
+ : '';
11
28
  const rows = findings
12
29
  .map((f) => {
13
30
  const frame = f.frame
@@ -70,6 +87,8 @@ export function viewer(findings, meta) {
70
87
  .none { color:var(--muted); }
71
88
  .warning { border:1px solid var(--amber); border-radius:6px; padding:10px 14px; margin:0 0 18px; }
72
89
  .warning ul { margin:6px 0 0; }
90
+ .coverage { border:1px solid var(--steel); border-radius:6px; padding:10px 14px; margin:0 0 18px; }
91
+ .coverage ul { margin:6px 0 0; }
73
92
  .f.put-away { opacity:.4; }
74
93
  .f.put-away .title { text-decoration:line-through; }
75
94
  .act { margin-left:8px; }
@@ -82,15 +101,15 @@ export function viewer(findings, meta) {
82
101
  <p class="meta">${escape(meta.target)} · ${escape(meta.started.slice(0, 19).replace('T', ' '))} · session ${escape(meta.id)}<br>
83
102
  ${findings.length} finding(s) — ${verified} verified, ${findings.length - verified} judged</p>
84
103
  ${warning}
104
+ ${verdict}
105
+ ${context}
85
106
  <div class="bar">
86
107
  <button data-filter="all" aria-pressed="true">all</button>
87
108
  <button data-filter="verified" aria-pressed="false">verified</button>
88
109
  <button data-filter="judged" aria-pressed="false">judged</button>
89
110
  <button id="show-away" aria-pressed="false">show put away</button>
90
111
  </div>
91
- ${findings.length === 0
92
- ? '<p class="none">' + (incomplete ? 'No findings from what completed.' : 'No findings.') + '</p>'
93
- : rows}
112
+ ${findings.length === 0 ? '' : rows}
94
113
  </div>
95
114
  <script>
96
115
  // Putting a finding away is per-reader and per-browser on purpose: this page is a
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. `base` is applicability rather than a missing capability: a before/after
39
- * check has no question to ask about a newly created file.
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.beforeTree !== undefined)
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,6 +146,7 @@ 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');
@@ -166,27 +171,33 @@ export async function review(opts) {
166
171
  targets.set(verifier, files);
167
172
  }
168
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;
169
177
  // a file the change touched that no parser produced a tree for was not reviewed,
170
178
  // whatever the summary says about the ones that were
171
179
  const grounded = new Set([...g.files.map((f) => f.changed.path), ...g.foreign.map((f) => f.path)]);
172
180
  for (const c of changed) {
173
- if (!grounded.has(c.path))
181
+ if (grounded.has(c.path))
182
+ continue;
183
+ if (packFor(c.path))
184
+ plan.fail(c.path, 'declared language parser unavailable');
185
+ else
174
186
  plan.waive(c.path, 'no parser for this language');
175
187
  }
176
188
  // Capabilities belong to files, not runs. A typed file beside one excluded from
177
189
  // tsconfig must not make the latter look checked, and an old Ruby file must not
178
190
  // make a new Python file eligible for a before/after oracle.
179
191
  for (const files of targets.values()) {
180
- for (const file of files)
181
- plan.limit(file.path, file.missing);
182
- }
183
- if (skippedLanguages.length > 0) {
184
- for (const c of changed) {
185
- if (!grounded.has(c.path) && skippedLanguages.includes(packOf(c.path))) {
186
- plan.fail(c.path, 'grammar budget reached');
187
- }
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);
188
200
  }
189
- failures.push('not reviewed, grammar budget reached: ' + skippedLanguages.join(', '));
190
201
  }
191
202
  for (const line of plan.summary())
192
203
  say('selection ' + line);
@@ -201,8 +212,14 @@ export async function review(opts) {
201
212
  const files = targets.get(v);
202
213
  const eligible = files.filter((file) => file.missing.length === 0);
203
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
+ }
204
220
  if (missing.length > 0 && eligible.length === 0) {
205
- skipped.set(v.id ?? v.name, missing.join(', '));
221
+ if (requireEnrichedOracles || !onlyEnrichedMissing)
222
+ skipped.set(check, missing.join(', '));
206
223
  continue;
207
224
  }
208
225
  // a scan spends most of its time here, so Ctrl-C has to reach this half
@@ -211,7 +228,6 @@ export async function review(opts) {
211
228
  break;
212
229
  }
213
230
  ran++;
214
- const check = v.id ?? v.name;
215
231
  manifest?.ran(check);
216
232
  for (const file of eligible)
217
233
  plan.checked(file.path, check);
@@ -227,6 +243,10 @@ export async function review(opts) {
227
243
  const names = [...skipped].map(([n, why]) => n + ' (no ' + why + ')');
228
244
  say('skipped ' + names.join(', '));
229
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
+ }
230
250
  // --checks overrides the config rather than filtering it
231
251
  const isGated = range.from !== undefined || range.commit !== undefined;
232
252
  const judgeCache = opts.cache === false || verifyOnly ? undefined : JudgeCache.open(repo, isGated);
@@ -348,6 +368,7 @@ export async function review(opts) {
348
368
  failures,
349
369
  plan,
350
370
  skippedChecks: [...skipped].map(([check, missing]) => ({ check, missing })),
371
+ unavailableChecks: [...unavailable].map(([check, missing]) => ({ check, missing })),
351
372
  usage: budget.finish(),
352
373
  budgetStop,
353
374
  cancelled: opts.signal?.aborted ?? false,