tdd-enforcer 0.2.6 → 0.2.7
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/.github/workflows/ci.yml +28 -0
- package/adapters/pi/helpers.test.ts +246 -244
- package/adapters/pi/helpers.ts +117 -91
- package/adapters/pi/hooks.test.ts +528 -425
- package/adapters/pi/hooks.ts +297 -230
- package/adapters/pi/index.test.ts +282 -272
- package/adapters/pi/index.ts +222 -200
- package/adapters/pi/log.test.ts +91 -91
- package/adapters/pi/log.ts +39 -27
- package/adapters/pi/prompts.test.ts +25 -25
- package/adapters/pi/prompts.ts +28 -30
- package/adapters/pi/tools.test.ts +391 -331
- package/adapters/pi/tools.ts +368 -325
- package/behaviour.md +15 -1
- package/biome.json +37 -0
- package/engine/config.test.ts +157 -157
- package/engine/config.ts +22 -19
- package/engine/enforce.test.ts +155 -145
- package/engine/enforce.ts +31 -23
- package/engine/git.test.ts +529 -507
- package/engine/git.ts +240 -114
- package/engine/index.ts +27 -4
- package/engine/state.test.ts +69 -69
- package/engine/state.ts +22 -20
- package/engine/transition.test.ts +229 -177
- package/engine/transition.ts +55 -52
- package/engine/types.ts +9 -9
- package/package.json +29 -22
- package/tsconfig.json +10 -10
package/adapters/pi/helpers.ts
CHANGED
|
@@ -1,46 +1,54 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
headMessage,
|
|
5
|
+
initGit,
|
|
6
|
+
loadConfig,
|
|
7
|
+
loadPhaseState,
|
|
8
|
+
nextPhase,
|
|
9
|
+
savePhaseState,
|
|
10
|
+
stageFiles,
|
|
11
|
+
} from "../../engine/index.js";
|
|
12
|
+
import type { Config, PhaseState } from "../../engine/types.js";
|
|
5
13
|
|
|
6
14
|
export type TddLoadResult =
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
| { ok: true; state: PhaseState; config: Config }
|
|
16
|
+
| { ok: false; reason: string };
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* Recover state.json from private git HEAD, or create default.
|
|
12
20
|
* Returns the state (does not save to disk — caller does that).
|
|
13
21
|
*/
|
|
14
22
|
function recoverState(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
root: string,
|
|
24
|
+
tddDir: string,
|
|
25
|
+
deps: {
|
|
26
|
+
existsSync: typeof existsSync;
|
|
27
|
+
headMessage: typeof headMessage;
|
|
28
|
+
nextPhase: typeof nextPhase;
|
|
29
|
+
} = { existsSync, headMessage, nextPhase },
|
|
22
30
|
): PhaseState {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
const gitDir = join(tddDir, ".git");
|
|
32
|
+
if (deps.existsSync(gitDir)) {
|
|
33
|
+
try {
|
|
34
|
+
const msg = deps.headMessage(root);
|
|
35
|
+
const m = msg.match(/^tdd: (red|green|refactor|init)$/);
|
|
36
|
+
if (m) {
|
|
37
|
+
const label = m[1];
|
|
38
|
+
if (label === "init") {
|
|
39
|
+
return { enabled: false, current: "red" };
|
|
40
|
+
}
|
|
41
|
+
if (label !== "red" && label !== "green" && label !== "refactor") {
|
|
42
|
+
return { enabled: false, current: "red" };
|
|
43
|
+
}
|
|
44
|
+
const next = deps.nextPhase(label);
|
|
45
|
+
return { enabled: true, current: next ?? "red" };
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// No commits or bad HEAD — fall through to default
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { enabled: false, current: "red" };
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
/**
|
|
@@ -52,69 +60,87 @@ function recoverState(
|
|
|
52
60
|
* Callers must check state.enabled themselves if they need active enforcement.
|
|
53
61
|
*/
|
|
54
62
|
export function loadTddState(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
root: string,
|
|
64
|
+
deps: {
|
|
65
|
+
existsSync: typeof existsSync;
|
|
66
|
+
loadConfig: typeof loadConfig;
|
|
67
|
+
initGit: typeof initGit;
|
|
68
|
+
loadPhaseState: typeof loadPhaseState;
|
|
69
|
+
savePhaseState: typeof savePhaseState;
|
|
70
|
+
headMessage: typeof headMessage;
|
|
71
|
+
nextPhase: typeof nextPhase;
|
|
72
|
+
stageFiles: typeof stageFiles;
|
|
73
|
+
} = {
|
|
74
|
+
existsSync,
|
|
75
|
+
loadConfig,
|
|
76
|
+
initGit,
|
|
77
|
+
loadPhaseState,
|
|
78
|
+
savePhaseState,
|
|
79
|
+
headMessage,
|
|
80
|
+
nextPhase,
|
|
81
|
+
stageFiles,
|
|
82
|
+
},
|
|
75
83
|
): TddLoadResult {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
const tddDir = join(root, ".pi", "tdd");
|
|
85
|
+
if (!deps.existsSync(tddDir)) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
reason:
|
|
89
|
+
"Missing .pi/tdd/ directory. See the tdd-enforcer skill to learn how to set up TDD configs.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
const rulesPath = join(tddDir, "rules.json");
|
|
94
|
+
if (!deps.existsSync(rulesPath)) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
reason:
|
|
98
|
+
"Missing .pi/tdd/rules.json. See the tdd-enforcer skill to learn how to set up TDD configs.",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
let config: Config;
|
|
103
|
+
try {
|
|
104
|
+
config = deps.loadConfig(root);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
reason: `Invalid .pi/tdd/rules.json: ${(e as Error).message}. See the tdd-enforcer skill.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
92
111
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
// Init git if missing — required for state recovery and all consumers
|
|
113
|
+
const gitDir = join(tddDir, ".git");
|
|
114
|
+
if (!deps.existsSync(gitDir)) {
|
|
115
|
+
try {
|
|
116
|
+
deps.initGit(root);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
reason: `Failed to initialise private git repo: ${(e as Error).message}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
102
124
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
// Auto-create state.json if missing or corrupted
|
|
126
|
+
const phasePath = join(tddDir, "state.json");
|
|
127
|
+
let state: PhaseState | undefined;
|
|
128
|
+
if (deps.existsSync(phasePath)) {
|
|
129
|
+
try {
|
|
130
|
+
state = deps.loadPhaseState(root);
|
|
131
|
+
} catch {
|
|
132
|
+
// Corrupted — recover below
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!state) {
|
|
136
|
+
state = recoverState(root, tddDir, {
|
|
137
|
+
existsSync: deps.existsSync,
|
|
138
|
+
headMessage: deps.headMessage,
|
|
139
|
+
nextPhase: deps.nextPhase,
|
|
140
|
+
});
|
|
141
|
+
deps.savePhaseState(root, state);
|
|
142
|
+
deps.stageFiles(root, [".pi/tdd/state.json"]);
|
|
143
|
+
}
|
|
118
144
|
|
|
119
|
-
|
|
145
|
+
return { ok: true, state, config };
|
|
120
146
|
}
|