pi-python-helper 0.1.0

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,33 @@
1
+ export interface CompletionEvidenceInput {
2
+ syncExecuted: boolean;
3
+ syncOk: boolean;
4
+ testExecuted: boolean;
5
+ testOk: boolean;
6
+ stale: boolean;
7
+ changedPaths: string[];
8
+ }
9
+
10
+ export interface CompletionEvidence {
11
+ ok: boolean;
12
+ blockers: string[];
13
+ changedPaths: string[];
14
+ }
15
+
16
+ /**
17
+ * Completion is only proven when the environment was synchronised and the tests
18
+ * actually ran. Declaring completion on a partial run is treated as a blocker,
19
+ * never as a warning.
20
+ */
21
+ export function buildCompletionEvidence(input: CompletionEvidenceInput): CompletionEvidence {
22
+ const blockers: string[] = [];
23
+ if (!input.syncExecuted)
24
+ blockers.push('The environment sync (uv sync/lock check) was not executed.');
25
+ else if (!input.syncOk) blockers.push('The environment sync did not pass.');
26
+ if (!input.testExecuted) blockers.push('Tests were not executed.');
27
+ else if (!input.testOk) blockers.push('Tests did not pass.');
28
+ if (input.stale)
29
+ blockers.push(
30
+ 'Stale artifacts were detected, so the test result does not describe the current sources.',
31
+ );
32
+ return { ok: blockers.length === 0, blockers, changedPaths: input.changedPaths };
33
+ }
@@ -0,0 +1,62 @@
1
+ import { isTestFile } from '../project/paths.ts';
2
+
3
+ export interface TddCheckpoint {
4
+ ok: boolean;
5
+ reasons: string[];
6
+ sourceChanges: string[];
7
+ testChanges: string[];
8
+ }
9
+
10
+ const MIN_TOKEN_LENGTH = 4;
11
+
12
+ function tokens(path: string): string[] {
13
+ return path
14
+ .toLowerCase()
15
+ .replace(/\.py$/, '')
16
+ .split(/[^a-z0-9]+/)
17
+ .filter((token) => token.length >= MIN_TOKEN_LENGTH);
18
+ }
19
+
20
+ function related(source: string, test: string): boolean {
21
+ const testTokens = tokens(test).filter((token) => token !== 'test' && token !== 'tests');
22
+ return tokens(source).some((sourceToken) =>
23
+ testTokens.some(
24
+ (testToken) =>
25
+ sourceToken === testToken ||
26
+ sourceToken.startsWith(testToken) ||
27
+ testToken.startsWith(sourceToken),
28
+ ),
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Check that production changes are accompanied by a plausibly related test
34
+ * change. Matching is name-based on purpose: it is cheap, deterministic, and
35
+ * only used to decide whether to run the heavier verification bundle.
36
+ */
37
+ export function checkTdd(changedPaths: string[], testChangedPaths: string[] = []): TddCheckpoint {
38
+ const all = [...new Set([...changedPaths, ...testChangedPaths])].map((path) =>
39
+ path.replace(/\\/g, '/'),
40
+ );
41
+ const sourceChanges = all.filter((path) => path.endsWith('.py') && !isTestFile(path));
42
+ const testChanges = all.filter((path) => path.endsWith('.py') && isTestFile(path));
43
+
44
+ const hasRelatedTest =
45
+ testChanges.length > 0
46
+ ? sourceChanges.some((source) => testChanges.some((test) => related(source, test)))
47
+ : false;
48
+
49
+ const reasons: string[] = [];
50
+ if (sourceChanges.length > 0 && testChanges.length === 0) {
51
+ reasons.push('Production Python files changed without any test file change.');
52
+ } else if (sourceChanges.length > 0 && !hasRelatedTest) {
53
+ reasons.push('Changed test files do not appear related to the changed production modules.');
54
+ }
55
+
56
+ return {
57
+ ok: reasons.length === 0,
58
+ reasons,
59
+ sourceChanges,
60
+ testChanges,
61
+ };
62
+ }