@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.
@@ -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
- export function localModules(root) {
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 walk = (dir, depth) => {
141
- if (depth > 6 || local.size > 4000)
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
- // a package directory is importable by name; so is a plain source folder
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') && (isPackage || depth <= 2)) {
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
- walk(root, 0);
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);
@@ -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 ['## PowerShot', '', ...banner, incomplete ? 'No findings *from what it managed to review*.' : 'No findings.', ''].join('\n');
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) + '`', '');
@@ -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
  }
@@ -2,12 +2,19 @@ const escape = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').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. `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,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.project.getSourceFiles().length + ' files · ' + g.symbolIndex.size + ' symbols' +
150
- (g.typed ? '' : ' · no tsconfig, phantom-api disabled'));
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 (!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
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
- 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
- }
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
- skipped.set(v.id ?? v.name, missing.join(', '));
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,