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/engine/state.ts CHANGED
@@ -1,38 +1,40 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
- import { join, dirname } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
3
  import type { PhaseState } from "./types.js";
4
4
 
5
5
  const TDD_DIR = ".pi/tdd";
6
6
  const VALID_PHASES = new Set(["red", "green", "refactor"]);
7
7
 
8
8
  export function phaseStatePath(projectRoot: string): string {
9
- return join(projectRoot, TDD_DIR, "state.json");
9
+ return join(projectRoot, TDD_DIR, "state.json");
10
10
  }
11
11
 
12
12
  function ensureDir(path: string): void {
13
- const dir = dirname(path);
14
- if (!existsSync(dir)) {
15
- mkdirSync(dir, { recursive: true });
16
- }
13
+ const dir = dirname(path);
14
+ if (!existsSync(dir)) {
15
+ mkdirSync(dir, { recursive: true });
16
+ }
17
17
  }
18
18
 
19
19
  export function loadPhaseState(projectRoot: string): PhaseState {
20
- const path = phaseStatePath(projectRoot);
21
- const raw = readFileSync(path, "utf-8");
22
- const parsed = JSON.parse(raw) as PhaseState;
20
+ const path = phaseStatePath(projectRoot);
21
+ const raw = readFileSync(path, "utf-8");
22
+ const parsed = JSON.parse(raw) as PhaseState;
23
23
 
24
- if (typeof parsed.current !== "string" || !VALID_PHASES.has(parsed.current)) {
25
- throw new Error(`state.json: invalid phase "${String(parsed.current)}". Must be red, green, or refactor.`);
26
- }
24
+ if (typeof parsed.current !== "string" || !VALID_PHASES.has(parsed.current)) {
25
+ throw new Error(
26
+ `state.json: invalid phase "${String(parsed.current)}". Must be red, green, or refactor.`,
27
+ );
28
+ }
27
29
 
28
- return {
29
- enabled: parsed.enabled === true,
30
- current: parsed.current,
31
- };
30
+ return {
31
+ enabled: parsed.enabled === true,
32
+ current: parsed.current,
33
+ };
32
34
  }
33
35
 
34
36
  export function savePhaseState(projectRoot: string, state: PhaseState): void {
35
- const path = phaseStatePath(projectRoot);
36
- ensureDir(path);
37
- writeFileSync(path, JSON.stringify(state, null, 2), "utf-8");
37
+ const path = phaseStatePath(projectRoot);
38
+ ensureDir(path);
39
+ writeFileSync(path, JSON.stringify(state, null, 2), "utf-8");
38
40
  }
@@ -1,206 +1,258 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
- import { getDisallowedChanges, nextPhase, checkGate } from "./transition.js";
3
- import type { Config, TestRunner } from "./types.js";
4
1
  import picomatch from "picomatch";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { checkGate, getDisallowedChanges, nextPhase } from "./transition.js";
4
+ import type { Config, TestRunner } from "./types.js";
5
5
 
6
6
  // ── Pure unit tests: nextPhase ──────────────────────────────────────────────
7
7
 
8
8
  describe("nextPhase", () => {
9
- it("returns green from red", () => {
10
- expect(nextPhase("red")).toBe("green");
11
- });
9
+ it("returns green from red", () => {
10
+ expect(nextPhase("red")).toBe("green");
11
+ });
12
12
 
13
- it("returns refactor from green", () => {
14
- expect(nextPhase("green")).toBe("refactor");
15
- });
13
+ it("returns refactor from green", () => {
14
+ expect(nextPhase("green")).toBe("refactor");
15
+ });
16
16
 
17
- it("returns red from refactor", () => {
18
- expect(nextPhase("refactor")).toBe("red");
19
- });
17
+ it("returns red from refactor", () => {
18
+ expect(nextPhase("refactor")).toBe("red");
19
+ });
20
20
 
21
- it("returns null for unknown phase", () => {
22
- expect(nextPhase("blurple" as any)).toBeNull();
23
- });
21
+ it("returns null for unknown phase", () => {
22
+ expect(nextPhase("blurple" as any)).toBeNull();
23
+ });
24
24
  });
25
25
 
26
26
  // ── Pure unit tests: checkGate ──────────────────────────────────────────────
27
27
 
28
28
  function makeRunner(passed: boolean): TestRunner {
29
- return async (_cmds, _timeout) => ({
30
- passed,
31
- message: passed ? "all ok" : "tests failed",
32
- });
29
+ return async (_cmds, _timeout) => ({
30
+ passed,
31
+ message: passed ? "all ok" : "tests failed",
32
+ });
33
33
  }
34
34
 
35
35
  const testConfig: Config = {
36
- blockedInRed: [],
37
- blockedInGreen: [],
38
- testCommands: ["npm test"],
39
- timeoutSeconds: 30,
36
+ blockedInRed: [],
37
+ blockedInGreen: [],
38
+ testCommands: ["npm test"],
39
+ timeoutSeconds: 30,
40
40
  };
41
41
 
42
42
  describe("checkGate", () => {
43
+ describe("red → green (tests must fail)", () => {
44
+ it("allows when tests fail", async () => {
45
+ const r = await checkGate("red", "green", makeRunner(false), testConfig);
46
+ expect(r.passed).toBe(true);
47
+ expect(r.message).toMatch(/proceed|fail/i);
48
+ });
49
+
50
+ it("blocks when tests pass", async () => {
51
+ const r = await checkGate("red", "green", makeRunner(true), testConfig);
52
+ expect(r.passed).toBe(false);
53
+ expect(r.message).toMatch(/transitioning to GREEN/i);
54
+ });
55
+ });
56
+
57
+ describe("green → refactor (tests must pass)", () => {
58
+ it("allows when tests pass", async () => {
59
+ const r = await checkGate(
60
+ "green",
61
+ "refactor",
62
+ makeRunner(true),
63
+ testConfig,
64
+ );
65
+ expect(r.passed).toBe(true);
66
+ expect(r.message).toMatch(/pass/i);
67
+ });
68
+
69
+ it("blocks when tests fail", async () => {
70
+ const r = await checkGate(
71
+ "green",
72
+ "refactor",
73
+ makeRunner(false),
74
+ testConfig,
75
+ );
76
+ expect(r.passed).toBe(false);
77
+ expect(r.message).toMatch(/transitioning to REFACTOR/i);
78
+ });
79
+ });
80
+
81
+ describe("refactor → red (tests must pass)", () => {
82
+ it("allows when tests pass", async () => {
83
+ const r = await checkGate(
84
+ "refactor",
85
+ "red",
86
+ makeRunner(true),
87
+ testConfig,
88
+ );
89
+ expect(r.passed).toBe(true);
90
+ expect(r.message).toMatch(/pass/i);
91
+ });
92
+
93
+ it("blocks when tests fail", async () => {
94
+ const r = await checkGate(
95
+ "refactor",
96
+ "red",
97
+ makeRunner(false),
98
+ testConfig,
99
+ );
100
+ expect(r.passed).toBe(false);
101
+ expect(r.message).toMatch(/transitioning to RED/i);
102
+ });
103
+ });
104
+
105
+ it("passes test commands to the runner", async () => {
106
+ let captured: string[] | undefined;
107
+ const runner: TestRunner = async (cmds) => {
108
+ captured = cmds;
109
+ return { passed: true, message: "" };
110
+ };
111
+ await checkGate("red", "green", runner, testConfig);
112
+ expect(captured).toEqual(["npm test"]);
113
+ });
43
114
 
44
- describe("red green (tests must fail)", () => {
45
- it("allows when tests fail", async () => {
46
- const r = await checkGate("red", "green", makeRunner(false), testConfig);
47
- expect(r.passed).toBe(true);
48
- expect(r.message).toMatch(/proceed|fail/i);
49
- });
50
-
51
- it("blocks when tests pass", async () => {
52
- const r = await checkGate("red", "green", makeRunner(true), testConfig);
53
- expect(r.passed).toBe(false);
54
- expect(r.message).toMatch(/transitioning to GREEN/i);
55
- });
56
- });
57
-
58
- describe("green → refactor (tests must pass)", () => {
59
- it("allows when tests pass", async () => {
60
- const r = await checkGate("green", "refactor", makeRunner(true), testConfig);
61
- expect(r.passed).toBe(true);
62
- expect(r.message).toMatch(/pass/i);
63
- });
64
-
65
- it("blocks when tests fail", async () => {
66
- const r = await checkGate("green", "refactor", makeRunner(false), testConfig);
67
- expect(r.passed).toBe(false);
68
- expect(r.message).toMatch(/transitioning to REFACTOR/i);
69
- });
70
- });
71
-
72
- describe("refactor → red (tests must pass)", () => {
73
- it("allows when tests pass", async () => {
74
- const r = await checkGate("refactor", "red", makeRunner(true), testConfig);
75
- expect(r.passed).toBe(true);
76
- expect(r.message).toMatch(/pass/i);
77
- });
78
-
79
- it("blocks when tests fail", async () => {
80
- const r = await checkGate("refactor", "red", makeRunner(false), testConfig);
81
- expect(r.passed).toBe(false);
82
- expect(r.message).toMatch(/transitioning to RED/i);
83
- });
84
- });
85
-
86
-
87
- it("passes test commands to the runner", async () => {
88
- let captured: string[] | undefined;
89
- const runner: TestRunner = async (cmds) => {
90
- captured = cmds;
91
- return { passed: true, message: "" };
92
- };
93
- await checkGate("red", "green", runner, testConfig);
94
- expect(captured).toEqual(["npm test"]);
95
- });
96
-
97
- it("passes timeoutSeconds to the runner", async () => {
98
- let captured: number | undefined;
99
- const runner: TestRunner = async (_cmds, t) => {
100
- captured = t;
101
- return { passed: true, message: "" };
102
- };
103
- await checkGate("red", "green", runner, testConfig);
104
- expect(captured).toBe(30);
105
- });
106
-
107
- it("passes multiple test commands to the runner", async () => {
108
- const multiConfig: Config = {
109
- ...testConfig,
110
- testCommands: ["npm run test:unit", "npm run test:integration"],
111
- };
112
- let captured: string[] | undefined;
113
- const runner: TestRunner = async (cmds) => {
114
- captured = cmds;
115
- return { passed: true, message: "" };
116
- };
117
- await checkGate("red", "green", runner, multiConfig);
118
- expect(captured).toHaveLength(2);
119
- expect(captured).toContain("npm run test:unit");
120
- expect(captured).toContain("npm run test:integration");
121
- });
115
+ it("passes timeoutSeconds to the runner", async () => {
116
+ let captured: number | undefined;
117
+ const runner: TestRunner = async (_cmds, t) => {
118
+ captured = t;
119
+ return { passed: true, message: "" };
120
+ };
121
+ await checkGate("red", "green", runner, testConfig);
122
+ expect(captured).toBe(30);
123
+ });
124
+
125
+ it("passes multiple test commands to the runner", async () => {
126
+ const multiConfig: Config = {
127
+ ...testConfig,
128
+ testCommands: ["npm run test:unit", "npm run test:integration"],
129
+ };
130
+ let captured: string[] | undefined;
131
+ const runner: TestRunner = async (cmds) => {
132
+ captured = cmds;
133
+ return { passed: true, message: "" };
134
+ };
135
+ await checkGate("red", "green", runner, multiConfig);
136
+ expect(captured).toHaveLength(2);
137
+ expect(captured).toContain("npm run test:unit");
138
+ expect(captured).toContain("npm run test:integration");
139
+ });
122
140
  });
123
141
 
124
142
  // ── Pure unit tests: getDisallowedChanges ────────────────────────────────────
125
143
 
126
144
  const denyConfig: Config = {
127
- blockedInRed: ["src/**/*.ts"],
128
- blockedInGreen: ["tests/**/*.test.ts"],
129
- testCommands: [],
130
- timeoutSeconds: 30,
145
+ blockedInRed: ["src/**/*.ts"],
146
+ blockedInGreen: ["tests/**/*.test.ts"],
147
+ testCommands: [],
148
+ timeoutSeconds: 30,
131
149
  };
132
150
 
133
151
  describe("getDisallowedChanges", () => {
134
- let mockChangesSinceSnapshot: ReturnType<typeof vi.fn>;
135
- let mockDisallowedFiles: ReturnType<typeof vi.fn>;
136
-
137
- function makeDeps(overrides = {}) {
138
- return {
139
- changesSinceSnapshot: mockChangesSinceSnapshot,
140
- disallowedFiles: mockDisallowedFiles,
141
- ...overrides,
142
- };
143
- }
144
-
145
- beforeEach(() => {
146
- vi.clearAllMocks();
147
- mockChangesSinceSnapshot = vi.fn().mockReturnValue([]);
148
- mockDisallowedFiles = vi.fn().mockReturnValue([]);
149
- });
150
-
151
- it("returns empty for refactor phase regardless of git state", () => {
152
- const result = getDisallowedChanges("/any", "refactor", denyConfig, makeDeps());
153
- expect(result).toEqual([]);
154
- expect(mockChangesSinceSnapshot).not.toHaveBeenCalled();
155
- });
156
-
157
- it("returns empty when no files changed", () => {
158
- mockChangesSinceSnapshot.mockReturnValue([]);
159
- const result = getDisallowedChanges("/test", "red", denyConfig, makeDeps());
160
- expect(result).toEqual([]);
161
- expect(mockChangesSinceSnapshot).toHaveBeenCalledWith("/test");
162
- });
163
-
164
- it("returns disallowed files in red phase", () => {
165
- mockChangesSinceSnapshot.mockReturnValue(["src/main.ts", "tests/foo.test.ts", "README.md"]);
166
- const matchBlockedInRed = picomatch(denyConfig.blockedInRed);
167
- mockDisallowedFiles.mockImplementation((changed: string[], phase: string) => {
168
- if (phase === "red") {
169
- return changed.filter((f: string) => matchBlockedInRed(f));
170
- }
171
- return [];
172
- });
173
- const violations = getDisallowedChanges("/test", "red", denyConfig, makeDeps());
174
- expect(violations).toContain("src/main.ts");
175
- expect(violations).not.toContain("tests/foo.test.ts");
176
- expect(violations).not.toContain("README.md");
177
- });
178
-
179
- it("returns disallowed files in green phase", () => {
180
- mockChangesSinceSnapshot.mockReturnValue(["tests/foo.test.ts", "src/main.ts", "package.json"]);
181
- const matchBlockedInGreen = picomatch(denyConfig.blockedInGreen);
182
- mockDisallowedFiles.mockImplementation((changed: string[], phase: string) => {
183
- if (phase === "green") {
184
- return changed.filter((f: string) => matchBlockedInGreen(f));
185
- }
186
- return [];
187
- });
188
- const violations = getDisallowedChanges("/test", "green", denyConfig, makeDeps());
189
- expect(violations).toContain("tests/foo.test.ts");
190
- expect(violations).not.toContain("src/main.ts");
191
- expect(violations).not.toContain("package.json");
192
- });
193
-
194
- it("catches untracked files, not just modified", () => {
195
- mockChangesSinceSnapshot.mockReturnValue(["src/new.ts"]);
196
- const matchBlockedInRed = picomatch(denyConfig.blockedInRed);
197
- mockDisallowedFiles.mockImplementation((changed: string[], phase: string) => {
198
- if (phase === "red") {
199
- return changed.filter((f: string) => matchBlockedInRed(f));
200
- }
201
- return [];
202
- });
203
- const violations = getDisallowedChanges("/test", "red", denyConfig, makeDeps());
204
- expect(violations).toContain("src/new.ts");
205
- });
152
+ let mockChangesSinceSnapshot: ReturnType<typeof vi.fn>;
153
+ let mockDisallowedFiles: ReturnType<typeof vi.fn>;
154
+
155
+ function makeDeps(overrides = {}) {
156
+ return {
157
+ changesSinceSnapshot: mockChangesSinceSnapshot,
158
+ disallowedFiles: mockDisallowedFiles,
159
+ ...overrides,
160
+ };
161
+ }
162
+
163
+ beforeEach(() => {
164
+ vi.clearAllMocks();
165
+ mockChangesSinceSnapshot = vi.fn().mockReturnValue([]);
166
+ mockDisallowedFiles = vi.fn().mockReturnValue([]);
167
+ });
168
+
169
+ it("returns empty for refactor phase regardless of git state", () => {
170
+ const result = getDisallowedChanges(
171
+ "/any",
172
+ "refactor",
173
+ denyConfig,
174
+ makeDeps(),
175
+ );
176
+ expect(result).toEqual([]);
177
+ expect(mockChangesSinceSnapshot).not.toHaveBeenCalled();
178
+ });
179
+
180
+ it("returns empty when no files changed", () => {
181
+ mockChangesSinceSnapshot.mockReturnValue([]);
182
+ const result = getDisallowedChanges("/test", "red", denyConfig, makeDeps());
183
+ expect(result).toEqual([]);
184
+ expect(mockChangesSinceSnapshot).toHaveBeenCalledWith("/test");
185
+ });
186
+
187
+ it("returns disallowed files in red phase", () => {
188
+ mockChangesSinceSnapshot.mockReturnValue([
189
+ "src/main.ts",
190
+ "tests/foo.test.ts",
191
+ "README.md",
192
+ ]);
193
+ const matchBlockedInRed = picomatch(denyConfig.blockedInRed);
194
+ mockDisallowedFiles.mockImplementation(
195
+ (changed: string[], phase: string) => {
196
+ if (phase === "red") {
197
+ return changed.filter((f: string) => matchBlockedInRed(f));
198
+ }
199
+ return [];
200
+ },
201
+ );
202
+ const violations = getDisallowedChanges(
203
+ "/test",
204
+ "red",
205
+ denyConfig,
206
+ makeDeps(),
207
+ );
208
+ expect(violations).toContain("src/main.ts");
209
+ expect(violations).not.toContain("tests/foo.test.ts");
210
+ expect(violations).not.toContain("README.md");
211
+ });
212
+
213
+ it("returns disallowed files in green phase", () => {
214
+ mockChangesSinceSnapshot.mockReturnValue([
215
+ "tests/foo.test.ts",
216
+ "src/main.ts",
217
+ "package.json",
218
+ ]);
219
+ const matchBlockedInGreen = picomatch(denyConfig.blockedInGreen);
220
+ mockDisallowedFiles.mockImplementation(
221
+ (changed: string[], phase: string) => {
222
+ if (phase === "green") {
223
+ return changed.filter((f: string) => matchBlockedInGreen(f));
224
+ }
225
+ return [];
226
+ },
227
+ );
228
+ const violations = getDisallowedChanges(
229
+ "/test",
230
+ "green",
231
+ denyConfig,
232
+ makeDeps(),
233
+ );
234
+ expect(violations).toContain("tests/foo.test.ts");
235
+ expect(violations).not.toContain("src/main.ts");
236
+ expect(violations).not.toContain("package.json");
237
+ });
238
+
239
+ it("catches untracked files, not just modified", () => {
240
+ mockChangesSinceSnapshot.mockReturnValue(["src/new.ts"]);
241
+ const matchBlockedInRed = picomatch(denyConfig.blockedInRed);
242
+ mockDisallowedFiles.mockImplementation(
243
+ (changed: string[], phase: string) => {
244
+ if (phase === "red") {
245
+ return changed.filter((f: string) => matchBlockedInRed(f));
246
+ }
247
+ return [];
248
+ },
249
+ );
250
+ const violations = getDisallowedChanges(
251
+ "/test",
252
+ "red",
253
+ denyConfig,
254
+ makeDeps(),
255
+ );
256
+ expect(violations).toContain("src/new.ts");
257
+ });
206
258
  });
@@ -1,21 +1,24 @@
1
- import type { Phase, Config, Transition } from "./types.js";
2
- import { PHASE_CYCLE } from "./types.js";
3
- import { changesSinceSnapshot } from "./git.js";
4
1
  import { disallowedFiles } from "./enforce.js";
2
+ import { changesSinceSnapshot } from "./git.js";
3
+ import type { Config, Phase, Transition } from "./types.js";
4
+ import { PHASE_CYCLE } from "./types.js";
5
5
 
6
6
  /**
7
7
  * Get the next phase in the cycle.
8
8
  */
9
9
  export function nextPhase(current: Phase): Phase | null {
10
- return PHASE_CYCLE[current] ?? null;
10
+ return PHASE_CYCLE[current] ?? null;
11
11
  }
12
12
 
13
13
  export interface GateResult {
14
- passed: boolean;
15
- message: string;
14
+ passed: boolean;
15
+ message: string;
16
16
  }
17
17
 
18
- export type TestRunner = (commands: string[], timeoutSeconds: number) => Promise<GateResult>;
18
+ export type TestRunner = (
19
+ commands: string[],
20
+ timeoutSeconds: number,
21
+ ) => Promise<GateResult>;
19
22
 
20
23
  /**
21
24
  * Run the transition gate check.
@@ -24,42 +27,42 @@ export type TestRunner = (commands: string[], timeoutSeconds: number) => Promise
24
27
  * - REFACTOR→RED: tests must pass (all zero exit)
25
28
  */
26
29
  export async function checkGate(
27
- from: Phase,
28
- to: Phase,
29
- testRunner: TestRunner,
30
- config: Config,
30
+ from: Phase,
31
+ to: Phase,
32
+ testRunner: TestRunner,
33
+ config: Config,
31
34
  ): Promise<GateResult> {
32
- const result = await testRunner(config.testCommands, config.timeoutSeconds);
33
-
34
- switch (`${from}→${to}` as Transition) {
35
- case "red→green":
36
- if (result.passed) {
37
- return {
38
- passed: false,
39
- message: "Tests passed. Add a failing test before transitioning to GREEN.",
40
- };
41
- }
42
- return { passed: true, message: "Tests fail — proceed to GREEN." };
35
+ const result = await testRunner(config.testCommands, config.timeoutSeconds);
43
36
 
44
- case "green→refactor":
45
- if (!result.passed) {
46
- return {
47
- passed: false,
48
- message: "Tests failed. Fix them before transitioning to REFACTOR.",
49
- };
50
- }
51
- return { passed: true, message: "All tests pass — proceeding." };
37
+ switch (`${from}→${to}` as Transition) {
38
+ case "red→green":
39
+ if (result.passed) {
40
+ return {
41
+ passed: false,
42
+ message:
43
+ "Tests passed. Add a failing test before transitioning to GREEN.",
44
+ };
45
+ }
46
+ return { passed: true, message: "Tests fail — proceed to GREEN." };
52
47
 
53
- case "refactor→red":
54
- if (!result.passed) {
55
- return {
56
- passed: false,
57
- message: "Tests failed. Fix them before transitioning to RED.",
58
- };
59
- }
60
- return { passed: true, message: "All tests pass — proceeding." };
48
+ case "green→refactor":
49
+ if (!result.passed) {
50
+ return {
51
+ passed: false,
52
+ message: "Tests failed. Fix them before transitioning to REFACTOR.",
53
+ };
54
+ }
55
+ return { passed: true, message: "All tests pass — proceeding." };
61
56
 
62
- }
57
+ case "refactor→red":
58
+ if (!result.passed) {
59
+ return {
60
+ passed: false,
61
+ message: "Tests failed. Fix them before transitioning to RED.",
62
+ };
63
+ }
64
+ return { passed: true, message: "All tests pass — proceeding." };
65
+ }
63
66
  }
64
67
 
65
68
  /**
@@ -67,19 +70,19 @@ export async function checkGate(
67
70
  * Returns list of violating files (empty = ok).
68
71
  */
69
72
  export function getDisallowedChanges(
70
- projectRoot: string,
71
- phase: Phase,
72
- config: Config,
73
- deps: {
74
- changesSinceSnapshot: typeof changesSinceSnapshot;
75
- disallowedFiles: typeof disallowedFiles;
76
- } = {
77
- changesSinceSnapshot,
78
- disallowedFiles,
79
- },
73
+ projectRoot: string,
74
+ phase: Phase,
75
+ config: Config,
76
+ deps: {
77
+ changesSinceSnapshot: typeof changesSinceSnapshot;
78
+ disallowedFiles: typeof disallowedFiles;
79
+ } = {
80
+ changesSinceSnapshot,
81
+ disallowedFiles,
82
+ },
80
83
  ): string[] {
81
- if (phase === "refactor") return [];
84
+ if (phase === "refactor") return [];
82
85
 
83
- const changed = deps.changesSinceSnapshot(projectRoot);
84
- return deps.disallowedFiles(changed, phase, config);
86
+ const changed = deps.changesSinceSnapshot(projectRoot);
87
+ return deps.disallowedFiles(changed, phase, config);
85
88
  }
package/engine/types.ts CHANGED
@@ -1,21 +1,21 @@
1
1
  export type Phase = "red" | "green" | "refactor";
2
2
 
3
3
  export interface PhaseState {
4
- enabled: boolean;
5
- current: Phase;
4
+ enabled: boolean;
5
+ current: Phase;
6
6
  }
7
7
 
8
8
  export interface Config {
9
- blockedInRed: string[];
10
- blockedInGreen: string[];
11
- testCommands: string[];
12
- timeoutSeconds: number;
9
+ blockedInRed: string[];
10
+ blockedInGreen: string[];
11
+ testCommands: string[];
12
+ timeoutSeconds: number;
13
13
  }
14
14
 
15
15
  export type Transition = "red→green" | "green→refactor" | "refactor→red";
16
16
 
17
17
  export const PHASE_CYCLE: Record<Phase, Phase | null> = {
18
- red: "green",
19
- green: "refactor",
20
- refactor: "red",
18
+ red: "green",
19
+ green: "refactor",
20
+ refactor: "red",
21
21
  };