pi-python-helper 0.1.0 → 0.1.1
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/CHANGELOG.md +9 -0
- package/extensions/tools/dependencies.ts +0 -120
- package/extensions/tools/validation.ts +119 -0
- package/package.json +1 -1
- package/src/build/failure.ts +7 -55
- package/src/build/traceback.ts +55 -0
- package/src/dependencies/aliases.ts +101 -0
- package/src/dependencies/plan.ts +2 -103
- package/src/project/inspect.ts +83 -49
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ does not guarantee a stable public tool schema.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.1] - 2026-09-21
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Refactored `src/dependencies/plan.ts` by extracting static import aliases and console-only distribution tables into `src/dependencies/aliases.ts`.
|
|
15
|
+
- Refactored `src/build/failure.ts` by extracting Python and pytest traceback frame parsing and library frame detection into `src/build/traceback.ts`.
|
|
16
|
+
- Decomposed monolithic `inspectProject` in `src/project/inspect.ts` into focused diagnostic collectors for manifest, lockfile, and environment rules.
|
|
17
|
+
- Moved `py_tdd_checkpoint` and `py_completion_evidence` tools from `extensions/tools/dependencies.ts` to `extensions/tools/validation.ts` to align tool registration with module responsibilities.
|
|
18
|
+
|
|
10
19
|
## [0.1.0] - 2026-09-21
|
|
11
20
|
|
|
12
21
|
### Added
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { Type } from 'typebox';
|
|
2
2
|
import { failure, result } from '../../src/core/result.ts';
|
|
3
|
-
import { runCommand } from '../../src/core/runner.ts';
|
|
4
3
|
import { planDependencies } from '../../src/dependencies/plan.ts';
|
|
5
|
-
import { buildCompletionEvidence } from '../../src/validation/evidence.ts';
|
|
6
|
-
import { checkTdd } from '../../src/validation/tdd.ts';
|
|
7
4
|
import { messageOf, resolveProjectRoot, text, type Pi } from '../shared.ts';
|
|
8
5
|
import { runScanProject } from '../../src/project/scanner.ts';
|
|
9
6
|
|
|
@@ -83,121 +80,4 @@ export function registerDependencyTools(pi: Pi): void {
|
|
|
83
80
|
}
|
|
84
81
|
},
|
|
85
82
|
});
|
|
86
|
-
|
|
87
|
-
pi.registerTool({
|
|
88
|
-
name: 'py_tdd_checkpoint',
|
|
89
|
-
label: 'Python TDD Checkpoint',
|
|
90
|
-
description:
|
|
91
|
-
'Check whether production Python changes have related test changes before implementation is considered complete. Read-only.',
|
|
92
|
-
promptSnippet: 'Check the Python TDD checkpoint for changed files',
|
|
93
|
-
promptGuidelines: [
|
|
94
|
-
'Use py_tdd_checkpoint before reporting Python implementation work as complete.',
|
|
95
|
-
],
|
|
96
|
-
parameters: Type.Object({
|
|
97
|
-
changedPaths: Type.Optional(Type.Array(Type.String(), { maxItems: 500 })),
|
|
98
|
-
testChangedPaths: Type.Optional(Type.Array(Type.String(), { maxItems: 500 })),
|
|
99
|
-
}),
|
|
100
|
-
async execute(_id, params, signal, _update, ctx) {
|
|
101
|
-
const started = Date.now();
|
|
102
|
-
let changedPaths = params.changedPaths ?? [];
|
|
103
|
-
let source = 'argument';
|
|
104
|
-
if (!changedPaths.length) {
|
|
105
|
-
const diff = await runCommand('git', ['diff', '--name-only', 'HEAD'], {
|
|
106
|
-
cwd: ctx.cwd,
|
|
107
|
-
signal,
|
|
108
|
-
timeoutMs: 10000,
|
|
109
|
-
maxBytes: 100_000,
|
|
110
|
-
});
|
|
111
|
-
changedPaths = diff.stdout
|
|
112
|
-
.split(/\r?\n/)
|
|
113
|
-
.map((line) => line.trim())
|
|
114
|
-
.filter(Boolean);
|
|
115
|
-
source = 'git diff';
|
|
116
|
-
}
|
|
117
|
-
const checkpoint = checkTdd(changedPaths, params.testChangedPaths ?? changedPaths);
|
|
118
|
-
return text(
|
|
119
|
-
result(ctx.cwd, started, {
|
|
120
|
-
ok: checkpoint.ok,
|
|
121
|
-
summary: checkpoint.ok
|
|
122
|
-
? `TDD checkpoint passed across ${changedPaths.length} changed path(s) from ${source}.`
|
|
123
|
-
: 'TDD checkpoint found production changes without a related test change.',
|
|
124
|
-
data: { ...checkpoint, changedPaths, source },
|
|
125
|
-
evidence: checkpoint.reasons.map((message) => ({ kind: 'tdd_blocker', message })),
|
|
126
|
-
warnings: checkpoint.reasons.map((message) => ({
|
|
127
|
-
code: 'TDD_CHECKPOINT',
|
|
128
|
-
message,
|
|
129
|
-
severity: 'warning' as const,
|
|
130
|
-
})),
|
|
131
|
-
errors: [],
|
|
132
|
-
suggestions: checkpoint.ok
|
|
133
|
-
? []
|
|
134
|
-
: [
|
|
135
|
-
{
|
|
136
|
-
message:
|
|
137
|
-
'Add the smallest focused test for the changed behaviour, or explain why the change needs no test.',
|
|
138
|
-
confidence: 'high' as const,
|
|
139
|
-
},
|
|
140
|
-
],
|
|
141
|
-
}),
|
|
142
|
-
);
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
pi.registerTool({
|
|
147
|
-
name: 'py_completion_evidence',
|
|
148
|
-
label: 'Python Completion Evidence',
|
|
149
|
-
description:
|
|
150
|
-
'Build a conservative completion report from environment sync and test execution results. Read-only.',
|
|
151
|
-
promptSnippet: 'Create evidence for a Python completion report',
|
|
152
|
-
promptGuidelines: [
|
|
153
|
-
'Use py_completion_evidence before claiming Python work is complete; a partial run is not evidence.',
|
|
154
|
-
],
|
|
155
|
-
parameters: Type.Object({
|
|
156
|
-
syncExecuted: Type.Boolean({
|
|
157
|
-
description: 'Whether uv lock --check / uv sync actually ran.',
|
|
158
|
-
}),
|
|
159
|
-
syncOk: Type.Boolean(),
|
|
160
|
-
testExecuted: Type.Boolean({ description: 'Whether pytest actually ran.' }),
|
|
161
|
-
testOk: Type.Boolean(),
|
|
162
|
-
stale: Type.Boolean({ description: 'Whether stale artifacts were detected.' }),
|
|
163
|
-
changedPaths: Type.Array(Type.String(), { maxItems: 500 }),
|
|
164
|
-
}),
|
|
165
|
-
async execute(_id, params, _signal, _update, ctx) {
|
|
166
|
-
const started = Date.now();
|
|
167
|
-
const evidence = buildCompletionEvidence(params);
|
|
168
|
-
return text(
|
|
169
|
-
result(ctx.cwd, started, {
|
|
170
|
-
ok: evidence.ok,
|
|
171
|
-
summary: evidence.ok
|
|
172
|
-
? 'Completion evidence is sufficient for the supplied checks.'
|
|
173
|
-
: 'Completion evidence is incomplete or contains failing checks.',
|
|
174
|
-
data: evidence,
|
|
175
|
-
evidence: evidence.blockers.map((message) => ({ kind: 'completion_blocker', message })),
|
|
176
|
-
warnings: evidence.blockers.map((message) => ({
|
|
177
|
-
code: 'INCOMPLETE_EVIDENCE',
|
|
178
|
-
message,
|
|
179
|
-
severity: 'warning' as const,
|
|
180
|
-
})),
|
|
181
|
-
errors: evidence.ok
|
|
182
|
-
? []
|
|
183
|
-
: [
|
|
184
|
-
{
|
|
185
|
-
code: 'COMPLETION_NOT_PROVEN',
|
|
186
|
-
message: 'The supplied evidence does not prove completion.',
|
|
187
|
-
severity: 'error' as const,
|
|
188
|
-
},
|
|
189
|
-
],
|
|
190
|
-
suggestions: evidence.ok
|
|
191
|
-
? []
|
|
192
|
-
: [
|
|
193
|
-
{
|
|
194
|
-
message:
|
|
195
|
-
'Run py_validation_bundle and address every blocker before reporting completion.',
|
|
196
|
-
confidence: 'high' as const,
|
|
197
|
-
},
|
|
198
|
-
],
|
|
199
|
-
}),
|
|
200
|
-
);
|
|
201
|
-
},
|
|
202
|
-
});
|
|
203
83
|
}
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
} from '../../src/project/conformance.ts';
|
|
12
12
|
import { readInstalledDistributions } from '../../src/project/installed.ts';
|
|
13
13
|
import { summarizeValidation, type ValidationStep } from '../../src/validation/bundle.ts';
|
|
14
|
+
import { buildCompletionEvidence } from '../../src/validation/evidence.ts';
|
|
15
|
+
import { checkTdd } from '../../src/validation/tdd.ts';
|
|
14
16
|
import { join } from 'node:path';
|
|
15
17
|
import { isFile } from '../../src/project/root.ts';
|
|
16
18
|
import { runScanProject } from '../../src/project/scanner.ts';
|
|
@@ -341,4 +343,121 @@ export function registerValidationTools(pi: Pi): void {
|
|
|
341
343
|
}
|
|
342
344
|
},
|
|
343
345
|
});
|
|
346
|
+
|
|
347
|
+
pi.registerTool({
|
|
348
|
+
name: 'py_tdd_checkpoint',
|
|
349
|
+
label: 'Python TDD Checkpoint',
|
|
350
|
+
description:
|
|
351
|
+
'Check whether production Python changes have related test changes before implementation is considered complete. Read-only.',
|
|
352
|
+
promptSnippet: 'Check the Python TDD checkpoint for changed files',
|
|
353
|
+
promptGuidelines: [
|
|
354
|
+
'Use py_tdd_checkpoint before reporting Python implementation work as complete.',
|
|
355
|
+
],
|
|
356
|
+
parameters: Type.Object({
|
|
357
|
+
changedPaths: Type.Optional(Type.Array(Type.String(), { maxItems: 500 })),
|
|
358
|
+
testChangedPaths: Type.Optional(Type.Array(Type.String(), { maxItems: 500 })),
|
|
359
|
+
}),
|
|
360
|
+
async execute(_id, params, signal, _update, ctx) {
|
|
361
|
+
const started = Date.now();
|
|
362
|
+
let changedPaths = params.changedPaths ?? [];
|
|
363
|
+
let source = 'argument';
|
|
364
|
+
if (!changedPaths.length) {
|
|
365
|
+
const diff = await runCommand('git', ['diff', '--name-only', 'HEAD'], {
|
|
366
|
+
cwd: ctx.cwd,
|
|
367
|
+
signal,
|
|
368
|
+
timeoutMs: 10000,
|
|
369
|
+
maxBytes: 100_000,
|
|
370
|
+
});
|
|
371
|
+
changedPaths = diff.stdout
|
|
372
|
+
.split(/\r?\n/)
|
|
373
|
+
.map((line) => line.trim())
|
|
374
|
+
.filter(Boolean);
|
|
375
|
+
source = 'git diff';
|
|
376
|
+
}
|
|
377
|
+
const checkpoint = checkTdd(changedPaths, params.testChangedPaths ?? changedPaths);
|
|
378
|
+
return text(
|
|
379
|
+
result(ctx.cwd, started, {
|
|
380
|
+
ok: checkpoint.ok,
|
|
381
|
+
summary: checkpoint.ok
|
|
382
|
+
? `TDD checkpoint passed across ${changedPaths.length} changed path(s) from ${source}.`
|
|
383
|
+
: 'TDD checkpoint found production changes without a related test change.',
|
|
384
|
+
data: { ...checkpoint, changedPaths, source },
|
|
385
|
+
evidence: checkpoint.reasons.map((message) => ({ kind: 'tdd_blocker', message })),
|
|
386
|
+
warnings: checkpoint.reasons.map((message) => ({
|
|
387
|
+
code: 'TDD_CHECKPOINT',
|
|
388
|
+
message,
|
|
389
|
+
severity: 'warning' as const,
|
|
390
|
+
})),
|
|
391
|
+
errors: [],
|
|
392
|
+
suggestions: checkpoint.ok
|
|
393
|
+
? []
|
|
394
|
+
: [
|
|
395
|
+
{
|
|
396
|
+
message:
|
|
397
|
+
'Add the smallest focused test for the changed behaviour, or explain why the change needs no test.',
|
|
398
|
+
confidence: 'high' as const,
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
pi.registerTool({
|
|
407
|
+
name: 'py_completion_evidence',
|
|
408
|
+
label: 'Python Completion Evidence',
|
|
409
|
+
description:
|
|
410
|
+
'Build a conservative completion report from environment sync and test execution results. Read-only.',
|
|
411
|
+
promptSnippet: 'Create evidence for a Python completion report',
|
|
412
|
+
promptGuidelines: [
|
|
413
|
+
'Use py_completion_evidence before claiming Python work is complete; a partial run is not evidence.',
|
|
414
|
+
],
|
|
415
|
+
parameters: Type.Object({
|
|
416
|
+
syncExecuted: Type.Boolean({
|
|
417
|
+
description: 'Whether uv lock --check / uv sync actually ran.',
|
|
418
|
+
}),
|
|
419
|
+
syncOk: Type.Boolean(),
|
|
420
|
+
testExecuted: Type.Boolean({ description: 'Whether pytest actually ran.' }),
|
|
421
|
+
testOk: Type.Boolean(),
|
|
422
|
+
stale: Type.Boolean({ description: 'Whether stale artifacts were detected.' }),
|
|
423
|
+
changedPaths: Type.Array(Type.String(), { maxItems: 500 }),
|
|
424
|
+
}),
|
|
425
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
426
|
+
const started = Date.now();
|
|
427
|
+
const evidence = buildCompletionEvidence(params);
|
|
428
|
+
return text(
|
|
429
|
+
result(ctx.cwd, started, {
|
|
430
|
+
ok: evidence.ok,
|
|
431
|
+
summary: evidence.ok
|
|
432
|
+
? 'Completion evidence is sufficient for the supplied checks.'
|
|
433
|
+
: 'Completion evidence is incomplete or contains failing checks.',
|
|
434
|
+
data: evidence,
|
|
435
|
+
evidence: evidence.blockers.map((message) => ({ kind: 'completion_blocker', message })),
|
|
436
|
+
warnings: evidence.blockers.map((message) => ({
|
|
437
|
+
code: 'INCOMPLETE_EVIDENCE',
|
|
438
|
+
message,
|
|
439
|
+
severity: 'warning' as const,
|
|
440
|
+
})),
|
|
441
|
+
errors: evidence.ok
|
|
442
|
+
? []
|
|
443
|
+
: [
|
|
444
|
+
{
|
|
445
|
+
code: 'COMPLETION_NOT_PROVEN',
|
|
446
|
+
message: 'The supplied evidence does not prove completion.',
|
|
447
|
+
severity: 'error' as const,
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
suggestions: evidence.ok
|
|
451
|
+
? []
|
|
452
|
+
: [
|
|
453
|
+
{
|
|
454
|
+
message:
|
|
455
|
+
'Run py_validation_bundle and address every blocker before reporting completion.',
|
|
456
|
+
confidence: 'high' as const,
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
}),
|
|
460
|
+
);
|
|
461
|
+
},
|
|
462
|
+
});
|
|
344
463
|
}
|
package/package.json
CHANGED
package/src/build/failure.ts
CHANGED
|
@@ -15,13 +15,13 @@ export type FailureKind =
|
|
|
15
15
|
| 'timeout'
|
|
16
16
|
| 'unknown';
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
18
|
+
import {
|
|
19
|
+
extractTracebackFrames,
|
|
20
|
+
firstUserFrame,
|
|
21
|
+
isLibraryFrame,
|
|
22
|
+
type TracebackFrame,
|
|
23
|
+
} from './traceback.ts';
|
|
24
|
+
export { extractTracebackFrames, firstUserFrame, isLibraryFrame, type TracebackFrame };
|
|
25
25
|
|
|
26
26
|
export interface FailureEvidence {
|
|
27
27
|
file?: string;
|
|
@@ -42,54 +42,6 @@ export interface FailureDiagnosis {
|
|
|
42
42
|
suggestions: Suggestion[];
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const FRAME_RE = /^\s*File "([^"]+)", line (\d+), in (.+?)\s*$/;
|
|
46
|
-
/**
|
|
47
|
-
* pytest `--tb=short` replaces the `File "..."` form with `path:line: in func`,
|
|
48
|
-
* so both notations must be recognised or short tracebacks yield no frame at all.
|
|
49
|
-
*/
|
|
50
|
-
const PYTEST_FRAME_RE = /^\s*([^\s:]+\.py):(\d+): in (.+?)\s*$/;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* A frame belongs to library code when it sits in an installed distribution or
|
|
54
|
-
* in the interpreter's own library tree. Pointing the agent at those frames is
|
|
55
|
-
* how it ends up editing site-packages instead of the project.
|
|
56
|
-
*/
|
|
57
|
-
export function isLibraryFrame(path: string): boolean {
|
|
58
|
-
const normalized = path.replace(/\\/g, '/');
|
|
59
|
-
return (
|
|
60
|
-
/\/site-packages\//.test(normalized) ||
|
|
61
|
-
/\/dist-packages\//.test(normalized) ||
|
|
62
|
-
/\/lib\/python3\.\d+\//.test(normalized) ||
|
|
63
|
-
/\/python3\.\d+\//.test(normalized) ||
|
|
64
|
-
/<frozen /.test(normalized) ||
|
|
65
|
-
/\/_pytest\//.test(normalized) ||
|
|
66
|
-
/\/pluggy\//.test(normalized)
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function extractTracebackFrames(output: string): TracebackFrame[] {
|
|
71
|
-
const frames: TracebackFrame[] = [];
|
|
72
|
-
for (const line of output.split(/\r?\n/)) {
|
|
73
|
-
const match = line.match(FRAME_RE) ?? line.match(PYTEST_FRAME_RE);
|
|
74
|
-
if (!match) continue;
|
|
75
|
-
const path = match[1];
|
|
76
|
-
frames.push({
|
|
77
|
-
path,
|
|
78
|
-
line: Number.parseInt(match[2], 10),
|
|
79
|
-
func: match[3].trim(),
|
|
80
|
-
library: isLibraryFrame(path),
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
return frames;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function firstUserFrame(frames: TracebackFrame[]): TracebackFrame | undefined {
|
|
87
|
-
for (let index = frames.length - 1; index >= 0; index -= 1) {
|
|
88
|
-
if (!frames[index].library) return frames[index];
|
|
89
|
-
}
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
45
|
function lastMatch(output: string, pattern: RegExp): RegExpMatchArray | undefined {
|
|
94
46
|
const matches = [...output.matchAll(new RegExp(pattern.source, `${pattern.flags}g`))];
|
|
95
47
|
return matches.at(-1);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface TracebackFrame {
|
|
2
|
+
path: string;
|
|
3
|
+
line: number;
|
|
4
|
+
func: string;
|
|
5
|
+
/** True for site-packages, the standard library, and pytest internals. */
|
|
6
|
+
library: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const FRAME_RE = /^\s*File "([^"]+)", line (\d+), in (.+?)\s*$/;
|
|
10
|
+
/**
|
|
11
|
+
* pytest `--tb=short` replaces the `File "..."` form with `path:line: in func`,
|
|
12
|
+
* so both notations must be recognised or short tracebacks yield no frame at all.
|
|
13
|
+
*/
|
|
14
|
+
const PYTEST_FRAME_RE = /^\s*([^\s:]+\.py):(\d+): in (.+?)\s*$/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A frame belongs to library code when it sits in an installed distribution or
|
|
18
|
+
* in the interpreter's own library tree. Pointing the agent at those frames is
|
|
19
|
+
* how it ends up editing site-packages instead of the project.
|
|
20
|
+
*/
|
|
21
|
+
export function isLibraryFrame(path: string): boolean {
|
|
22
|
+
const normalized = path.replace(/\\/g, '/');
|
|
23
|
+
return (
|
|
24
|
+
/\/site-packages\//.test(normalized) ||
|
|
25
|
+
/\/dist-packages\//.test(normalized) ||
|
|
26
|
+
/\/lib\/python3\.\d+\//.test(normalized) ||
|
|
27
|
+
/\/python3\.\d+\//.test(normalized) ||
|
|
28
|
+
/<frozen /.test(normalized) ||
|
|
29
|
+
/\/_pytest\//.test(normalized) ||
|
|
30
|
+
/\/pluggy\//.test(normalized)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function extractTracebackFrames(output: string): TracebackFrame[] {
|
|
35
|
+
const frames: TracebackFrame[] = [];
|
|
36
|
+
for (const line of output.split(/\r?\n/)) {
|
|
37
|
+
const match = line.match(FRAME_RE) ?? line.match(PYTEST_FRAME_RE);
|
|
38
|
+
if (!match) continue;
|
|
39
|
+
const path = match[1];
|
|
40
|
+
frames.push({
|
|
41
|
+
path,
|
|
42
|
+
line: Number.parseInt(match[2], 10),
|
|
43
|
+
func: match[3].trim(),
|
|
44
|
+
library: isLibraryFrame(path),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return frames;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function firstUserFrame(frames: TracebackFrame[]): TracebackFrame | undefined {
|
|
51
|
+
for (let index = frames.length - 1; index >= 0; index -= 1) {
|
|
52
|
+
if (!frames[index].library) return frames[index];
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import name and distribution name frequently disagree. The scanner supplies
|
|
3
|
+
* the authoritative mapping when the analysing interpreter has the package
|
|
4
|
+
* installed; this table covers the cases where it does not, so an uninstalled
|
|
5
|
+
* checkout is still analysed correctly.
|
|
6
|
+
*/
|
|
7
|
+
export const IMPORT_ALIASES: Record<string, string[]> = {
|
|
8
|
+
PIL: ['pillow'],
|
|
9
|
+
yaml: ['pyyaml'],
|
|
10
|
+
dateutil: ['python-dateutil'],
|
|
11
|
+
bs4: ['beautifulsoup4'],
|
|
12
|
+
cv2: ['opencv-python', 'opencv-python-headless'],
|
|
13
|
+
sklearn: ['scikit-learn'],
|
|
14
|
+
skimage: ['scikit-image'],
|
|
15
|
+
dotenv: ['python-dotenv'],
|
|
16
|
+
attr: ['attrs'],
|
|
17
|
+
attrs: ['attrs'],
|
|
18
|
+
jwt: ['pyjwt'],
|
|
19
|
+
jose: ['python-jose'],
|
|
20
|
+
serial: ['pyserial'],
|
|
21
|
+
OpenSSL: ['pyopenssl'],
|
|
22
|
+
Crypto: ['pycryptodome'],
|
|
23
|
+
pkg_resources: ['setuptools'],
|
|
24
|
+
MySQLdb: ['mysqlclient'],
|
|
25
|
+
googleapiclient: ['google-api-python-client'],
|
|
26
|
+
github: ['PyGithub'],
|
|
27
|
+
pytest_cov: ['pytest-cov'],
|
|
28
|
+
_pytest: ['pytest'],
|
|
29
|
+
pytest_asyncio: ['pytest-asyncio'],
|
|
30
|
+
psycopg: ['psycopg', 'psycopg-binary'],
|
|
31
|
+
psycopg2: ['psycopg2', 'psycopg2-binary'],
|
|
32
|
+
prometheus_client: ['prometheus-client'],
|
|
33
|
+
grpc: ['grpcio'],
|
|
34
|
+
kafka: ['kafka-python'],
|
|
35
|
+
docker: ['docker'],
|
|
36
|
+
numpy: ['numpy'],
|
|
37
|
+
pandas: ['pandas'],
|
|
38
|
+
ruamel: ['ruamel.yaml'],
|
|
39
|
+
setuptools: ['setuptools'],
|
|
40
|
+
mako: ['Mako'],
|
|
41
|
+
pytz: ['pytz'],
|
|
42
|
+
tzlocal: ['tzlocal'],
|
|
43
|
+
win32com: ['pywin32'],
|
|
44
|
+
lxml: ['lxml'],
|
|
45
|
+
matplotlib: ['matplotlib'],
|
|
46
|
+
seaborn: ['seaborn'],
|
|
47
|
+
sqlalchemy: ['sqlalchemy', 'SQLAlchemy'],
|
|
48
|
+
pydantic: ['pydantic'],
|
|
49
|
+
fastapi: ['fastapi'],
|
|
50
|
+
starlette: ['starlette'],
|
|
51
|
+
uvicorn: ['uvicorn'],
|
|
52
|
+
celery: ['celery'],
|
|
53
|
+
redis: ['redis'],
|
|
54
|
+
boto3: ['boto3'],
|
|
55
|
+
botocore: ['botocore'],
|
|
56
|
+
httpx: ['httpx'],
|
|
57
|
+
aiohttp: ['aiohttp'],
|
|
58
|
+
werkzeug: ['werkzeug'],
|
|
59
|
+
flask: ['flask'],
|
|
60
|
+
django: ['django'],
|
|
61
|
+
jinja2: ['jinja2'],
|
|
62
|
+
typer: ['typer'],
|
|
63
|
+
click: ['click'],
|
|
64
|
+
rich: ['rich'],
|
|
65
|
+
tqdm: ['tqdm'],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Distributions that are normally invoked as a console script, not imported. */
|
|
69
|
+
export const CONSOLE_ONLY: Set<string> = new Set([
|
|
70
|
+
'ruff',
|
|
71
|
+
'mypy',
|
|
72
|
+
'pyright',
|
|
73
|
+
'ty',
|
|
74
|
+
'pytest',
|
|
75
|
+
'pytest-cov',
|
|
76
|
+
'coverage',
|
|
77
|
+
'pre-commit',
|
|
78
|
+
'tox',
|
|
79
|
+
'nox',
|
|
80
|
+
'hatch',
|
|
81
|
+
'hatchling',
|
|
82
|
+
'build',
|
|
83
|
+
'twine',
|
|
84
|
+
'black',
|
|
85
|
+
'isort',
|
|
86
|
+
'flake8',
|
|
87
|
+
'pylint',
|
|
88
|
+
'sphinx',
|
|
89
|
+
'mkdocs',
|
|
90
|
+
'uvicorn',
|
|
91
|
+
'gunicorn',
|
|
92
|
+
'alembic',
|
|
93
|
+
'celery',
|
|
94
|
+
'honcho',
|
|
95
|
+
'maturin',
|
|
96
|
+
'setuptools-scm',
|
|
97
|
+
'pip-audit',
|
|
98
|
+
'bandit',
|
|
99
|
+
'commitizen',
|
|
100
|
+
'towncrier',
|
|
101
|
+
]);
|
package/src/dependencies/plan.ts
CHANGED
|
@@ -3,109 +3,8 @@ import { warn } from '../core/result.ts';
|
|
|
3
3
|
import { isTestFile } from '../project/paths.ts';
|
|
4
4
|
import type { DeclaredDependency, ScanPayload } from '../project/scanner.ts';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* the authoritative mapping when the analysing interpreter has the package
|
|
9
|
-
* installed; this table covers the cases where it does not, so an uninstalled
|
|
10
|
-
* checkout is still analysed correctly.
|
|
11
|
-
*/
|
|
12
|
-
export const IMPORT_ALIASES: Record<string, string[]> = {
|
|
13
|
-
PIL: ['pillow'],
|
|
14
|
-
yaml: ['pyyaml'],
|
|
15
|
-
dateutil: ['python-dateutil'],
|
|
16
|
-
bs4: ['beautifulsoup4'],
|
|
17
|
-
cv2: ['opencv-python', 'opencv-python-headless'],
|
|
18
|
-
sklearn: ['scikit-learn'],
|
|
19
|
-
skimage: ['scikit-image'],
|
|
20
|
-
dotenv: ['python-dotenv'],
|
|
21
|
-
attr: ['attrs'],
|
|
22
|
-
attrs: ['attrs'],
|
|
23
|
-
jwt: ['pyjwt'],
|
|
24
|
-
jose: ['python-jose'],
|
|
25
|
-
serial: ['pyserial'],
|
|
26
|
-
OpenSSL: ['pyopenssl'],
|
|
27
|
-
Crypto: ['pycryptodome'],
|
|
28
|
-
pkg_resources: ['setuptools'],
|
|
29
|
-
MySQLdb: ['mysqlclient'],
|
|
30
|
-
googleapiclient: ['google-api-python-client'],
|
|
31
|
-
github: ['PyGithub'],
|
|
32
|
-
pytest_cov: ['pytest-cov'],
|
|
33
|
-
_pytest: ['pytest'],
|
|
34
|
-
pytest_asyncio: ['pytest-asyncio'],
|
|
35
|
-
psycopg: ['psycopg', 'psycopg-binary'],
|
|
36
|
-
psycopg2: ['psycopg2', 'psycopg2-binary'],
|
|
37
|
-
prometheus_client: ['prometheus-client'],
|
|
38
|
-
grpc: ['grpcio'],
|
|
39
|
-
kafka: ['kafka-python'],
|
|
40
|
-
docker: ['docker'],
|
|
41
|
-
numpy: ['numpy'],
|
|
42
|
-
pandas: ['pandas'],
|
|
43
|
-
ruamel: ['ruamel.yaml'],
|
|
44
|
-
setuptools: ['setuptools'],
|
|
45
|
-
mako: ['Mako'],
|
|
46
|
-
pytz: ['pytz'],
|
|
47
|
-
tzlocal: ['tzlocal'],
|
|
48
|
-
win32com: ['pywin32'],
|
|
49
|
-
lxml: ['lxml'],
|
|
50
|
-
matplotlib: ['matplotlib'],
|
|
51
|
-
seaborn: ['seaborn'],
|
|
52
|
-
sqlalchemy: ['sqlalchemy', 'SQLAlchemy'],
|
|
53
|
-
pydantic: ['pydantic'],
|
|
54
|
-
fastapi: ['fastapi'],
|
|
55
|
-
starlette: ['starlette'],
|
|
56
|
-
uvicorn: ['uvicorn'],
|
|
57
|
-
celery: ['celery'],
|
|
58
|
-
redis: ['redis'],
|
|
59
|
-
boto3: ['boto3'],
|
|
60
|
-
botocore: ['botocore'],
|
|
61
|
-
httpx: ['httpx'],
|
|
62
|
-
aiohttp: ['aiohttp'],
|
|
63
|
-
werkzeug: ['werkzeug'],
|
|
64
|
-
flask: ['flask'],
|
|
65
|
-
django: ['django'],
|
|
66
|
-
jinja2: ['jinja2'],
|
|
67
|
-
typer: ['typer'],
|
|
68
|
-
click: ['click'],
|
|
69
|
-
rich: ['rich'],
|
|
70
|
-
tqdm: ['tqdm'],
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/** Distributions that are normally invoked as a console script, not imported. */
|
|
74
|
-
const CONSOLE_ONLY = new Set(
|
|
75
|
-
[
|
|
76
|
-
'ruff',
|
|
77
|
-
'mypy',
|
|
78
|
-
'pyright',
|
|
79
|
-
'ty',
|
|
80
|
-
'pytest',
|
|
81
|
-
'pytest-cov',
|
|
82
|
-
'coverage',
|
|
83
|
-
'pre-commit',
|
|
84
|
-
'tox',
|
|
85
|
-
'nox',
|
|
86
|
-
'hatch',
|
|
87
|
-
'hatchling',
|
|
88
|
-
'build',
|
|
89
|
-
'twine',
|
|
90
|
-
'black',
|
|
91
|
-
'isort',
|
|
92
|
-
'flake8',
|
|
93
|
-
'pylint',
|
|
94
|
-
'sphinx',
|
|
95
|
-
'mkdocs',
|
|
96
|
-
'uvicorn',
|
|
97
|
-
'gunicorn',
|
|
98
|
-
'alembic',
|
|
99
|
-
'celery',
|
|
100
|
-
'honcho',
|
|
101
|
-
'maturin',
|
|
102
|
-
'setuptools-scm',
|
|
103
|
-
'pip-audit',
|
|
104
|
-
'bandit',
|
|
105
|
-
'commitizen',
|
|
106
|
-
'towncrier',
|
|
107
|
-
].map((name) => name),
|
|
108
|
-
);
|
|
6
|
+
import { CONSOLE_ONLY, IMPORT_ALIASES } from './aliases.ts';
|
|
7
|
+
export { CONSOLE_ONLY, IMPORT_ALIASES };
|
|
109
8
|
|
|
110
9
|
const NORMALIZE_RE = /[-_.]+/g;
|
|
111
10
|
|
package/src/project/inspect.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
type ConformanceReport,
|
|
6
6
|
} from './conformance.ts';
|
|
7
7
|
import type { InstalledEnvironment } from './installed.ts';
|
|
8
|
-
import type { ScanPayload } from './scanner.ts';
|
|
8
|
+
import type { LockComparison, LockSection, ManifestSection, ScanPayload } from './scanner.ts';
|
|
9
9
|
|
|
10
10
|
export interface ProjectInspection {
|
|
11
11
|
root: string;
|
|
@@ -48,60 +48,56 @@ export interface InspectInput {
|
|
|
48
48
|
installed?: InstalledEnvironment;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const { payload, venvDir, venvIgnored, hasTestsDirectory, installed } = input;
|
|
57
|
-
const manifest = payload.manifest;
|
|
58
|
-
const lock = payload.lock;
|
|
59
|
-
const comparison = payload.lockComparison;
|
|
60
|
-
const warnings: Diagnostic[] = [];
|
|
61
|
-
const notes: Diagnostic[] = [];
|
|
62
|
-
const suggestions: Suggestion[] = [];
|
|
51
|
+
interface DiagnosticCollector {
|
|
52
|
+
warnings: Diagnostic[];
|
|
53
|
+
notes: Diagnostic[];
|
|
54
|
+
suggestions: Suggestion[];
|
|
55
|
+
}
|
|
63
56
|
|
|
64
|
-
|
|
57
|
+
function collectManifestDiagnostics(
|
|
58
|
+
manifest: ManifestSection | null | undefined,
|
|
59
|
+
root: string,
|
|
60
|
+
collector: DiagnosticCollector,
|
|
61
|
+
): void {
|
|
65
62
|
if (manifest?.tomlError) {
|
|
66
|
-
warnings.push(
|
|
63
|
+
collector.warnings.push(
|
|
67
64
|
warn('TOML_PARSE_ERROR', manifest.tomlError, manifest.pyprojectPath ?? undefined),
|
|
68
65
|
);
|
|
69
66
|
}
|
|
70
67
|
for (const message of manifest?.warnings ?? []) {
|
|
71
|
-
warnings.push(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
notes.push({ code: 'LOCKFILE_NOTE', message, severity: 'info' });
|
|
68
|
+
collector.warnings.push(
|
|
69
|
+
warn('MANIFEST_WARNING', message, manifest?.pyprojectPath ?? undefined),
|
|
70
|
+
);
|
|
75
71
|
}
|
|
76
72
|
|
|
77
73
|
if (!manifest?.pyprojectPath) {
|
|
78
|
-
warnings.push(
|
|
74
|
+
collector.warnings.push(
|
|
79
75
|
warn(
|
|
80
76
|
'PYPROJECT_MISSING',
|
|
81
77
|
'pyproject.toml was not found; dependencies, layout, and tool configuration cannot be verified.',
|
|
82
78
|
),
|
|
83
79
|
);
|
|
84
|
-
suggestions.push({
|
|
80
|
+
collector.suggestions.push({
|
|
85
81
|
message: 'Run uv init to create a pyproject.toml, then uv add the runtime dependencies.',
|
|
86
82
|
confidence: 'high',
|
|
87
83
|
command: 'uv init',
|
|
88
84
|
});
|
|
89
85
|
} else if (!manifest.name) {
|
|
90
|
-
warnings.push(
|
|
86
|
+
collector.warnings.push(
|
|
91
87
|
warn(
|
|
92
88
|
'PROJECT_NAME_MISSING',
|
|
93
89
|
'pyproject.toml has no [project] name, so the installed distribution name is unknown.',
|
|
94
90
|
manifest.pyprojectPath ?? undefined,
|
|
95
91
|
),
|
|
96
92
|
);
|
|
97
|
-
suggestions.push({
|
|
93
|
+
collector.suggestions.push({
|
|
98
94
|
message: 'Add a [project] table with name and version to pyproject.toml.',
|
|
99
95
|
confidence: 'high',
|
|
100
96
|
});
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
if (manifest?.legacySetupPy || manifest?.legacySetupCfg) {
|
|
104
|
-
warnings.push(
|
|
100
|
+
collector.warnings.push(
|
|
105
101
|
warn(
|
|
106
102
|
'LEGACY_PACKAGING',
|
|
107
103
|
`The project still uses ${[
|
|
@@ -113,14 +109,14 @@ export function inspectProject(input: InspectInput): ProjectInspection {
|
|
|
113
109
|
root,
|
|
114
110
|
),
|
|
115
111
|
);
|
|
116
|
-
suggestions.push({
|
|
112
|
+
collector.suggestions.push({
|
|
117
113
|
message: 'Move dependency metadata from setup.py/setup.cfg into [project] in pyproject.toml.',
|
|
118
114
|
confidence: 'medium',
|
|
119
115
|
});
|
|
120
116
|
}
|
|
121
117
|
|
|
122
118
|
if (manifest?.requirementsFiles.length && manifest.pyprojectPath) {
|
|
123
|
-
warnings.push(
|
|
119
|
+
collector.warnings.push(
|
|
124
120
|
warn(
|
|
125
121
|
'DUPLICATE_DEPENDENCY_SOURCE',
|
|
126
122
|
`${manifest.requirementsFiles.join(', ')} also declares dependencies; uv resolves from pyproject.toml and uv.lock only.`,
|
|
@@ -129,14 +125,31 @@ export function inspectProject(input: InspectInput): ProjectInspection {
|
|
|
129
125
|
);
|
|
130
126
|
}
|
|
131
127
|
|
|
128
|
+
if (manifest?.legacySetupPy && manifest.buildBackend === null && !manifest.pyprojectPath) {
|
|
129
|
+
collector.suggestions.push({
|
|
130
|
+
message: 'uv manages dependencies from pyproject.toml; migrate before running uv sync.',
|
|
131
|
+
confidence: 'medium',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function collectLockDiagnostics(
|
|
137
|
+
lock: LockSection | null | undefined,
|
|
138
|
+
comparison: LockComparison | null | undefined,
|
|
139
|
+
collector: DiagnosticCollector,
|
|
140
|
+
): void {
|
|
141
|
+
for (const message of lock?.warnings ?? []) {
|
|
142
|
+
collector.notes.push({ code: 'LOCKFILE_NOTE', message, severity: 'info' });
|
|
143
|
+
}
|
|
144
|
+
|
|
132
145
|
if (!lock?.present) {
|
|
133
|
-
notes.push({
|
|
146
|
+
collector.notes.push({
|
|
134
147
|
code: 'LOCKFILE_MISSING',
|
|
135
148
|
message:
|
|
136
149
|
'uv.lock was not found, so dependency drift and exact resolved versions cannot be verified.',
|
|
137
150
|
severity: 'info',
|
|
138
151
|
});
|
|
139
|
-
suggestions.push({
|
|
152
|
+
collector.suggestions.push({
|
|
140
153
|
message: 'Run uv lock to record resolved versions in uv.lock.',
|
|
141
154
|
confidence: 'high',
|
|
142
155
|
command: 'uv lock',
|
|
@@ -144,14 +157,14 @@ export function inspectProject(input: InspectInput): ProjectInspection {
|
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
if (comparison?.requiresPythonMismatch) {
|
|
147
|
-
warnings.push(
|
|
160
|
+
collector.warnings.push(
|
|
148
161
|
warn(
|
|
149
162
|
'REQUIRES_PYTHON_MISMATCH',
|
|
150
163
|
`pyproject.toml requires-python is "${comparison.requiresPythonMismatch.manifest}" but uv.lock records "${comparison.requiresPythonMismatch.lock}".`,
|
|
151
164
|
lock?.path ?? undefined,
|
|
152
165
|
),
|
|
153
166
|
);
|
|
154
|
-
suggestions.push({
|
|
167
|
+
collector.suggestions.push({
|
|
155
168
|
message: 'Run uv lock so the lockfile reflects the current requires-python constraint.',
|
|
156
169
|
confidence: 'high',
|
|
157
170
|
command: 'uv lock',
|
|
@@ -159,7 +172,7 @@ export function inspectProject(input: InspectInput): ProjectInspection {
|
|
|
159
172
|
}
|
|
160
173
|
|
|
161
174
|
for (const name of comparison?.missingFromLock ?? []) {
|
|
162
|
-
warnings.push(
|
|
175
|
+
collector.warnings.push(
|
|
163
176
|
warn(
|
|
164
177
|
'LOCKFILE_MISSING_DEPENDENCY',
|
|
165
178
|
`"${name}" is declared in pyproject.toml but absent from uv.lock.`,
|
|
@@ -168,7 +181,7 @@ export function inspectProject(input: InspectInput): ProjectInspection {
|
|
|
168
181
|
);
|
|
169
182
|
}
|
|
170
183
|
if (comparison?.missingFromLock.length) {
|
|
171
|
-
suggestions.push({
|
|
184
|
+
collector.suggestions.push({
|
|
172
185
|
message: 'Run uv lock to add the missing declarations to the lockfile.',
|
|
173
186
|
confidence: 'high',
|
|
174
187
|
command: 'uv lock',
|
|
@@ -176,7 +189,7 @@ export function inspectProject(input: InspectInput): ProjectInspection {
|
|
|
176
189
|
}
|
|
177
190
|
|
|
178
191
|
for (const entry of comparison?.unsatisfiedInLock ?? []) {
|
|
179
|
-
warnings.push(
|
|
192
|
+
collector.warnings.push(
|
|
180
193
|
warn(
|
|
181
194
|
'LOCKFILE_UNSATISFIED_DEPENDENCY',
|
|
182
195
|
`"${entry.name}" is locked at ${entry.locked} which does not satisfy "${entry.specifier}".`,
|
|
@@ -186,42 +199,51 @@ export function inspectProject(input: InspectInput): ProjectInspection {
|
|
|
186
199
|
}
|
|
187
200
|
|
|
188
201
|
if (lock?.present && comparison && !comparison.specifierCheckAvailable) {
|
|
189
|
-
notes.push({
|
|
202
|
+
collector.notes.push({
|
|
190
203
|
code: 'SPECIFIER_CHECK_UNAVAILABLE',
|
|
191
204
|
message:
|
|
192
205
|
'The packaging library was unavailable, so only declared-versus-locked names were compared, not version constraints.',
|
|
193
206
|
severity: 'info',
|
|
194
207
|
});
|
|
195
|
-
suggestions.push({
|
|
208
|
+
collector.suggestions.push({
|
|
196
209
|
message:
|
|
197
210
|
'Install the packaging library in the analysing interpreter to compare declared version constraints against uv.lock.',
|
|
198
211
|
confidence: 'medium',
|
|
199
212
|
command: 'python3 -m pip install packaging',
|
|
200
213
|
});
|
|
201
214
|
}
|
|
215
|
+
}
|
|
202
216
|
|
|
217
|
+
function collectEnvironmentDiagnostics(
|
|
218
|
+
root: string,
|
|
219
|
+
manifest: ManifestSection | null | undefined,
|
|
220
|
+
venvDir: string | undefined,
|
|
221
|
+
venvIgnored: boolean | undefined,
|
|
222
|
+
hasTestsDirectory: boolean,
|
|
223
|
+
collector: DiagnosticCollector,
|
|
224
|
+
): void {
|
|
203
225
|
if (!venvDir) {
|
|
204
|
-
notes.push({
|
|
226
|
+
collector.notes.push({
|
|
205
227
|
code: 'VENV_MISSING',
|
|
206
228
|
message: 'No .venv directory exists at the project root; run uv sync before running tests.',
|
|
207
229
|
severity: 'info',
|
|
208
230
|
});
|
|
209
231
|
} else if (venvIgnored === false) {
|
|
210
|
-
warnings.push(
|
|
232
|
+
collector.warnings.push(
|
|
211
233
|
warn(
|
|
212
234
|
'VENV_NOT_IGNORED',
|
|
213
235
|
'.venv exists but is not listed in .gitignore.',
|
|
214
236
|
`${root}/.gitignore`,
|
|
215
237
|
),
|
|
216
238
|
);
|
|
217
|
-
suggestions.push({
|
|
239
|
+
collector.suggestions.push({
|
|
218
240
|
message: 'Add .venv/ to .gitignore so the environment is never committed.',
|
|
219
241
|
confidence: 'high',
|
|
220
242
|
});
|
|
221
243
|
}
|
|
222
244
|
|
|
223
245
|
if (!hasTestsDirectory) {
|
|
224
|
-
notes.push({
|
|
246
|
+
collector.notes.push({
|
|
225
247
|
code: 'TESTS_DIRECTORY_MISSING',
|
|
226
248
|
message:
|
|
227
249
|
'No tests/ directory was found; test selection and TDD gates cannot match changed sources.',
|
|
@@ -230,7 +252,7 @@ export function inspectProject(input: InspectInput): ProjectInspection {
|
|
|
230
252
|
}
|
|
231
253
|
|
|
232
254
|
if (manifest?.pyprojectPath && manifest.toolConfiguration && !manifest.toolConfiguration.pytest) {
|
|
233
|
-
notes.push({
|
|
255
|
+
collector.notes.push({
|
|
234
256
|
code: 'PYTEST_NOT_CONFIGURED',
|
|
235
257
|
message: 'pyproject.toml has no [tool.pytest.ini_options] table.',
|
|
236
258
|
severity: 'info',
|
|
@@ -238,7 +260,7 @@ export function inspectProject(input: InspectInput): ProjectInspection {
|
|
|
238
260
|
}
|
|
239
261
|
|
|
240
262
|
if (manifest?.layout === 'src' && manifest.modules.length === 0) {
|
|
241
|
-
warnings.push(
|
|
263
|
+
collector.warnings.push(
|
|
242
264
|
warn(
|
|
243
265
|
'EMPTY_SRC_LAYOUT',
|
|
244
266
|
'The src/ directory exists but contains no importable module directories or modules.',
|
|
@@ -247,20 +269,32 @@ export function inspectProject(input: InspectInput): ProjectInspection {
|
|
|
247
269
|
);
|
|
248
270
|
}
|
|
249
271
|
|
|
250
|
-
if (manifest?.legacySetupPy && manifest.buildBackend === null && !manifest.pyprojectPath) {
|
|
251
|
-
suggestions.push({
|
|
252
|
-
message: 'uv manages dependencies from pyproject.toml; migrate before running uv sync.',
|
|
253
|
-
confidence: 'medium',
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
272
|
if (manifest?.uvWorkspaceMembers.length) {
|
|
258
|
-
notes.push({
|
|
273
|
+
collector.notes.push({
|
|
259
274
|
code: 'UV_WORKSPACE',
|
|
260
275
|
message: `This is a uv workspace with ${manifest.uvWorkspaceMembers.length} member(s); scope build and test tools per member.`,
|
|
261
276
|
severity: 'info',
|
|
262
277
|
});
|
|
263
278
|
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Turn a scanner payload into a project model plus diagnostics. Pure so the
|
|
283
|
+
* whole diagnostic surface is unit-testable without touching a filesystem.
|
|
284
|
+
*/
|
|
285
|
+
export function inspectProject(input: InspectInput): ProjectInspection {
|
|
286
|
+
const { payload, venvDir, venvIgnored, hasTestsDirectory, installed } = input;
|
|
287
|
+
const manifest = payload.manifest;
|
|
288
|
+
const lock = payload.lock;
|
|
289
|
+
const comparison = payload.lockComparison;
|
|
290
|
+
const root = payload.root;
|
|
291
|
+
|
|
292
|
+
const collector: DiagnosticCollector = { warnings: [], notes: [], suggestions: [] };
|
|
293
|
+
collectManifestDiagnostics(manifest, root, collector);
|
|
294
|
+
collectLockDiagnostics(lock, comparison, collector);
|
|
295
|
+
collectEnvironmentDiagnostics(root, manifest, venvDir, venvIgnored, hasTestsDirectory, collector);
|
|
296
|
+
|
|
297
|
+
const { warnings, notes, suggestions } = collector;
|
|
264
298
|
|
|
265
299
|
const conformance =
|
|
266
300
|
installed !== undefined
|