@pmelab/gtd 32.1.0 → 33.0.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.
@@ -36,7 +36,12 @@ Feature: the refactor review's lenses take their model hints from refactorQualit
36
36
  And step "design.gate.answer" changes nothing
37
37
 
38
38
  Scenario: a refactor lens runs on its refactorQualityReviews hint, else its default
39
- Given step "seam.naive" writes "src/calc.ts" with:
39
+ Given step "seam.check" changes nothing
40
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
41
+ """
42
+ it("[AC-1] holds", () => {})
43
+ """
44
+ And step "seam.naive" writes "src/calc.ts" with:
40
45
  """
41
46
  export const add = (a: number, b: number) => a + b
42
47
  export const subtract = (a: number, b: number) => a - b
@@ -52,6 +52,11 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
52
52
  - AC-1: the change behaves as described.
53
53
  """
54
54
  And step "design.gate.answer" changes nothing
55
+ And step "seam.check" changes nothing
56
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
57
+ """
58
+ it("[AC-1] holds", () => {})
59
+ """
55
60
  And step "seam.naive" writes "src/calc.ts" with:
56
61
  """
57
62
  export const add = (a: number, b: number) => a + b
@@ -136,6 +141,11 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
136
141
  - AC-1: the change behaves as described.
137
142
  """
138
143
  And step "design.gate.answer" changes nothing
144
+ And step "seam.check" changes nothing
145
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
146
+ """
147
+ it("[AC-1] holds", () => {})
148
+ """
139
149
  And step "seam.naive" writes "src/calc.ts" with:
140
150
  """
141
151
  export const add = (a: number, b: number) => a + b
@@ -219,6 +229,11 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
219
229
  - AC-1: the change behaves as described.
220
230
  """
221
231
  And step "design.gate.answer" changes nothing
232
+ And step "seam.check" changes nothing
233
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
234
+ """
235
+ it("[AC-1] holds", () => {})
236
+ """
222
237
  And step "seam.naive" writes "src/calc.ts" with:
223
238
  """
224
239
  export const add = (a: number, b: number) => a + b
@@ -1426,6 +1441,11 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1426
1441
  - AC-1: the change behaves as described.
1427
1442
  """
1428
1443
  And step "design.gate.answer" changes nothing
1444
+ And step "seam.check" changes nothing
1445
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
1446
+ """
1447
+ it("[AC-1] holds", () => {})
1448
+ """
1429
1449
  And step "seam.naive" writes "src/calc.ts" with:
1430
1450
  """
1431
1451
  export const add = (a: number, b: number) => a + b
@@ -1614,7 +1634,12 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1614
1634
  """
1615
1635
 
1616
1636
  Scenario: a test-file stray is dropped from the derived refactor package
1617
- Given step "seam.naive" writes "src/calc.ts" with:
1637
+ Given step "seam.check" changes nothing
1638
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
1639
+ """
1640
+ it("[AC-1] holds", () => {})
1641
+ """
1642
+ And step "seam.naive" writes "src/calc.ts" with:
1618
1643
  """
1619
1644
  export const add = (a: number, b: number) => a + b
1620
1645
  export const subtract = (a: number, b: number) => a - b
@@ -1631,7 +1656,12 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1631
1656
  And ".gtd/REFACTOR.md" does not contain "src/calc.test.ts"
1632
1657
 
1633
1658
  Scenario: the refactor updates an existing test's call sites to the seam's new shape
1634
- Given step "seam.naive" writes "src/calc.ts" with:
1659
+ Given step "seam.check" changes nothing
1660
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
1661
+ """
1662
+ it("[AC-1] holds", () => {})
1663
+ """
1664
+ And step "seam.naive" writes "src/calc.ts" with:
1635
1665
  """
1636
1666
  export const add = ({ a, b }: { a: number; b: number }) => a + b
1637
1667
  export const subtract = ({ a, b }: { a: number; b: number }) => a - b
@@ -1651,7 +1681,12 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1651
1681
  Then the process rests at "refactor.health.check"
1652
1682
 
1653
1683
  Scenario: the refactor may not drop an assertion from an existing test
1654
- Given step "seam.naive" writes "src/calc.ts" with:
1684
+ Given step "seam.check" changes nothing
1685
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
1686
+ """
1687
+ it("[AC-1] holds", () => {})
1688
+ """
1689
+ And step "seam.naive" writes "src/calc.ts" with:
1655
1690
  """
1656
1691
  export const add = (a: number, b: number) => a + b
1657
1692
  export const subtract = (a: number, b: number) => a - b
@@ -1665,7 +1700,12 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1665
1700
  Then driving the process is refused with "refactor-tests"
1666
1701
 
1667
1702
  Scenario: a refactor-first whose strays are all test files goes straight to implement
1668
- Given step "seam.naive" writes "src/calc.test.ts" with:
1703
+ Given step "seam.check" changes nothing
1704
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
1705
+ """
1706
+ it("[AC-1] holds", () => {})
1707
+ """
1708
+ And step "seam.naive" writes "src/calc.test.ts" with:
1669
1709
  """
1670
1710
  import { expect, it } from "vitest"
1671
1711
  it("subtracts", () => expect(3 - 1).toBe(2))
@@ -1675,7 +1715,12 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1675
1715
  And ".gtd/REFACTOR.md" does not exist
1676
1716
 
1677
1717
  Scenario: collected review headings re-derive at ### with the deepest capped at ######
1678
- Given step "seam.naive" writes "src/calc.ts" with:
1718
+ Given step "seam.check" changes nothing
1719
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
1720
+ """
1721
+ it("[AC-1] holds", () => {})
1722
+ """
1723
+ And step "seam.naive" writes "src/calc.ts" with:
1679
1724
  """
1680
1725
  export const add = (a: number, b: number) => a + b
1681
1726
  export const subtract = (a: number, b: number) => a - b
@@ -42,7 +42,8 @@ export const requireWithin = (allowed: readonly string[]): void => {
42
42
  }
43
43
 
44
44
  // A fixed list, not `seamAllow`: narrowing that setting must not let a test file through.
45
- const isTestFile = (path: string): boolean => testFileGlobs.some((glob) => matches(path, glob))
45
+ export const isTestFile = (path: string): boolean =>
46
+ testFileGlobs.some((glob) => matches(path, glob))
46
47
 
47
48
  // The bundled refactor uses `requireTestCallSitesOnly`; this stays a public export.
48
49
  export const requireNoTestFiles = (): void => {
@@ -56,11 +57,11 @@ export const requireNoTestFiles = (): void => {
56
57
  }
57
58
 
58
59
  // Call sites live in code: a `.feature` file or a snapshot holds only expectations.
59
- const holdsCallSites = (path: string): boolean =>
60
+ export const holdsCallSites = (path: string): boolean =>
60
61
  !path.endsWith(".feature") && !path.endsWith(".snap")
61
62
 
62
63
  // `//` after a `:` (a URL) and `/*` after `*` or `/` (a glob) sit in strings, not comments.
63
- const COMMENT = /(?<![*/])\/\*[\s\S]*?\*\/|(?<!:)\/\/.*$/gm
64
+ export const COMMENT = /(?<![*/])\/\*[\s\S]*?\*\/|(?<!:)\/\/.*$/gm
64
65
  const ASSERTION = /\b(?:expect|assert\w*)\s*[.(]/g
65
66
  const TEST_CASE = /(?<![.\w])x?(?:it|test|describe)\s*[.(]/g
66
67
  const MODIFIER =
@@ -146,6 +146,11 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
146
146
 
147
147
  - AC-1: the change behaves as described.
148
148
  """
149
+ And step "seam.check" changes nothing
150
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
151
+ """
152
+ it("[AC-1] holds", () => {})
153
+ """
149
154
  And step "seam.naive" writes "src/calc.ts" with:
150
155
  """
151
156
  export const add = (a: number, b: number) => a + b
@@ -254,6 +259,11 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
254
259
  - AC-1: the change behaves as described.
255
260
  """
256
261
  And step "design.gate.answer" changes nothing
262
+ And step "seam.check" changes nothing
263
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
264
+ """
265
+ it("[AC-1] holds", () => {})
266
+ """
257
267
  And step "seam.naive" writes "src/calc.ts" with:
258
268
  """
259
269
  export const add = (a: number, b: number) => a + b
@@ -41,7 +41,12 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
41
41
  And step "design.gate.answer" changes nothing
42
42
 
43
43
  Scenario: three rejects rest at the steer gate, a steered rerun lands and runs to idle
44
- Given step "seam.naive" writes "src/calc.ts" with:
44
+ Given step "seam.check" changes nothing
45
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
46
+ """
47
+ it("[AC-1] holds", () => {})
48
+ """
49
+ And step "seam.naive" writes "src/calc.ts" with:
45
50
  """
46
51
  export const add = (a: number, b: number) => b + a
47
52
  """
@@ -87,6 +92,10 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
87
92
  - AC-1: the change behaves as described.
88
93
  """
89
94
  And step "seam.naive" from now on changes nothing
95
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
96
+ """
97
+ it("[AC-1] holds", () => {})
98
+ """
90
99
  And step "seam.naive" writes "src/calc.ts" with:
91
100
  """
92
101
  export const add = (a: number, b: number) => a + b
@@ -127,14 +136,18 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
127
136
  gtd(check): start-gate.check → design.triage
128
137
  gtd(agent): design.triage → design.gate.answer
129
138
  gtd(human): design.gate.answer → seam.naive
130
- gtd(agent): seam.naive → seam.judge
139
+ gtd(agent): seam.naive → seam.check
140
+ gtd(check): seam.check → seam.judge
131
141
  gtd(judge): seam.judge → seam.naive
132
- gtd(agent): seam.naive → seam.judge
142
+ gtd(agent): seam.naive → seam.check
143
+ gtd(check): seam.check → seam.judge
133
144
  gtd(judge): seam.judge → seam.naive
134
- gtd(agent): seam.naive → seam.judge
145
+ gtd(agent): seam.naive → seam.check
146
+ gtd(check): seam.check → seam.judge
135
147
  gtd(judge): seam.judge → seam.steer
136
148
  gtd(human): seam.steer → seam.naive
137
- gtd(agent): seam.naive → seam.judge
149
+ gtd(agent): seam.naive → seam.check
150
+ gtd(check): seam.check → seam.judge
138
151
  gtd(judge): seam.judge → implement.building
139
152
  gtd(agent): implement.building → implement.health.check
140
153
  gtd(check): implement.health.check → implement.closing
@@ -144,7 +157,7 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
144
157
  gtd(check): build.review.closing → idle
145
158
  """
146
159
  And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
147
- And the commit "gtd(agent): seam.naive → seam.judge" has a body containing "Gtd-Kept:"
160
+ And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
148
161
  And the commit "gtd(judge): seam.judge → seam.naive" has a body containing "\"answer\":\"reject\""
149
162
  And the commit "gtd(judge): seam.judge → seam.steer" has a body containing "\"answer\":\"reject\""
150
163
  And the commit "gtd(judge): seam.judge → implement.building" has a body containing "\"answer\":\"land\""
@@ -51,7 +51,12 @@ Feature: the naive pass and seam judge decide how a feature is built
51
51
  And step "design.gate.answer" changes nothing
52
52
 
53
53
  Scenario: a land judgment goes straight to the implementation with no seam gate or refactor
54
- Given step "seam.naive" writes "src/calc.ts" with:
54
+ Given step "seam.check" changes nothing
55
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
56
+ """
57
+ it("[AC-1] holds", () => {})
58
+ """
59
+ And step "seam.naive" writes "src/calc.ts" with:
55
60
  """
56
61
  export const add = (a: number, b: number) => a + b
57
62
  export const subtract = (a: number, b: number) => a - b
@@ -72,12 +77,18 @@ Feature: the naive pass and seam judge decide how a feature is built
72
77
  gtd(check): start-gate.check → design.triage
73
78
  gtd(agent): design.triage → design.gate.answer
74
79
  gtd(human): design.gate.answer → seam.naive
75
- gtd(agent): seam.naive → seam.judge
80
+ gtd(agent): seam.naive → seam.check
81
+ gtd(check): seam.check → seam.judge
76
82
  gtd(judge): seam.judge → implement.building
77
83
  """
78
84
 
79
85
  Scenario: config skills for the implement scope reach the implement step
80
- Given step "seam.naive" writes "src/calc.ts" with:
86
+ Given step "seam.check" changes nothing
87
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
88
+ """
89
+ it("[AC-1] holds", () => {})
90
+ """
91
+ And step "seam.naive" writes "src/calc.ts" with:
81
92
  """
82
93
  export const add = (a: number, b: number) => a + b
83
94
  export const subtract = (a: number, b: number) => a - b
@@ -91,19 +102,33 @@ Feature: the naive pass and seam judge decide how a feature is built
91
102
  And the resting step's content contains "my-tdd, my-runbook"
92
103
 
93
104
  Scenario: the naive pass lands as a marker commit and its turn stays off the branch
94
- Given step "seam.naive" writes "src/calc.ts" with:
105
+ Given step "seam.check" changes nothing
106
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
107
+ """
108
+ it("[AC-1] holds", () => {})
109
+ """
110
+ And step "seam.naive" writes "src/calc.ts" with:
95
111
  """
96
112
  export const add = (a: number, b: number) => a + b
97
113
  export const subtract = (a: number, b: number) => a - b
98
114
  """
99
115
  When the process is driven
100
116
  Then the process rests at "seam.judge"
101
- And the last commit subject is "gtd(agent): seam.naive → seam.judge"
102
- And the last commit body contains "Gtd-Kept:"
117
+ And the commit subjects end with:
118
+ """
119
+ gtd(agent): seam.naive → seam.check
120
+ gtd(check): seam.check → seam.judge
121
+ """
122
+ And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
103
123
  And "src/calc.ts" does not contain "subtract"
104
124
 
105
125
  Scenario: the implementation is one build turn, a green check, then the requirements are closed out
106
- Given step "seam.naive" writes "src/calc.ts" with:
126
+ Given step "seam.check" changes nothing
127
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
128
+ """
129
+ it("[AC-1] holds", () => {})
130
+ """
131
+ And step "seam.naive" writes "src/calc.ts" with:
107
132
  """
108
133
  export const add = (a: number, b: number) => a + b
109
134
  export const subtract = (a: number, b: number) => a - b
@@ -132,7 +157,12 @@ Feature: the naive pass and seam judge decide how a feature is built
132
157
  And ".gtd/REQUIREMENTS.md" does not exist
133
158
 
134
159
  Scenario: a naive pass modifying a test file and adding a new one skips the judge
135
- Given step "seam.naive" writes "src/subtract.ts" with:
160
+ Given step "seam.check" changes nothing
161
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
162
+ """
163
+ it("[AC-1] holds", () => {})
164
+ """
165
+ And step "seam.naive" writes "src/subtract.ts" with:
136
166
  """
137
167
  export const subtract = (a: number, b: number) => a - b
138
168
  """
@@ -146,11 +176,17 @@ Feature: the naive pass and seam judge decide how a feature is built
146
176
  And the commit subjects end with:
147
177
  """
148
178
  gtd(human): design.gate.answer → seam.naive
149
- gtd(agent): seam.naive → implement.building
179
+ gtd(agent): seam.naive → seam.check
180
+ gtd(check): seam.check → implement.building
150
181
  """
151
182
 
152
183
  Scenario: a naive pass adding a glob-named file does not hide a modified sibling
153
- Given step "seam.naive" writes "src/calc*.ts" with:
184
+ Given step "seam.check" changes nothing
185
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
186
+ """
187
+ it("[AC-1] holds", () => {})
188
+ """
189
+ And step "seam.naive" writes "src/calc*.ts" with:
154
190
  """
155
191
  export const subtract = (a: number, b: number) => a - b
156
192
  """
@@ -169,7 +205,12 @@ Feature: the naive pass and seam judge decide how a feature is built
169
205
  And the resting step's content does not contain "src/calc*.ts"
170
206
 
171
207
  Scenario: an edit at the steer gate runs the naive pass again with a fresh retry budget
172
- Given step "seam.naive" writes "src/calc.ts" with:
208
+ Given step "seam.check" changes nothing
209
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
210
+ """
211
+ it("[AC-1] holds", () => {})
212
+ """
213
+ And step "seam.naive" writes "src/calc.ts" with:
173
214
  """
174
215
  export const add = (a: number, b: number) => b + a
175
216
  """
@@ -201,16 +242,24 @@ Feature: the naive pass and seam judge decide how a feature is built
201
242
  And the commit subjects end with:
202
243
  """
203
244
  gtd(human): seam.steer → seam.naive
204
- gtd(agent): seam.naive → seam.judge
245
+ gtd(agent): seam.naive → seam.check
246
+ gtd(check): seam.check → seam.judge
205
247
  gtd(judge): seam.judge → seam.naive
206
- gtd(agent): seam.naive → seam.judge
248
+ gtd(agent): seam.naive → seam.check
249
+ gtd(check): seam.check → seam.judge
207
250
  gtd(judge): seam.judge → seam.naive
208
- gtd(agent): seam.naive → seam.judge
251
+ gtd(agent): seam.naive → seam.check
252
+ gtd(check): seam.check → seam.judge
209
253
  gtd(judge): seam.judge → seam.steer
210
254
  """
211
255
 
212
256
  Scenario: a clean landing at the steer gate overrules, with no seam gate and no refactor
213
- Given step "seam.naive" writes "src/calc.ts" with:
257
+ Given step "seam.check" changes nothing
258
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
259
+ """
260
+ it("[AC-1] holds", () => {})
261
+ """
262
+ And step "seam.naive" writes "src/calc.ts" with:
214
263
  """
215
264
  export const add = (a: number, b: number) => b + a
216
265
  """
@@ -229,7 +278,12 @@ Feature: the naive pass and seam judge decide how a feature is built
229
278
  """
230
279
 
231
280
  Scenario: review feedback after land re-runs design and implement, never the seam phase
232
- Given step "seam.naive" writes "src/calc.ts" with:
281
+ Given step "seam.check" changes nothing
282
+ And step "seam.naive" writes "src/acceptance.test.ts" with:
283
+ """
284
+ it("[AC-1] holds", () => {})
285
+ """
286
+ And step "seam.naive" writes "src/calc.ts" with:
233
287
  """
234
288
  export const add = (a: number, b: number) => b + a
235
289
  """
@@ -8,7 +8,14 @@ import {
8
8
  } from "../../flows/index.js"
9
9
  import { globMatches } from "../../wire/index.js"
10
10
  import { fixtureContext } from "../shared/text.fixture.js"
11
- import { implement, judgeSeam, naivePass, seamBuild, type SeamOutcome } from "./seam.js"
11
+ import {
12
+ implement,
13
+ judgeSeam,
14
+ naivePass,
15
+ acceptanceFault,
16
+ seamBuild,
17
+ type SeamOutcome,
18
+ } from "./seam.js"
12
19
  import { captureStep } from "../shared/text.fixture.js"
13
20
 
14
21
  afterEach(() => installContext(undefined))
@@ -98,6 +105,130 @@ describe("naivePass", () => {
98
105
  })
99
106
  })
100
107
 
108
+ describe("acceptanceFault", () => {
109
+ const requirements = "# R\n\n## Acceptance Criteria\n\n- AC-1: adds.\n- AC-2: subtracts.\n"
110
+ const file = (path: string, status: Change["status"], after: string): Change => ({
111
+ path,
112
+ status,
113
+ before: status === "added" ? undefined : "",
114
+ after,
115
+ })
116
+ const refusal = (...changes: Change[]): string | undefined => {
117
+ installContext({
118
+ ...fixtureContext({}),
119
+ matches: globMatches,
120
+ read: (path: string) => (path === ".gtd/REQUIREMENTS.md" ? requirements : undefined),
121
+ } as unknown as FlowContext)
122
+ return acceptanceFault(".gtd/REQUIREMENTS.md", tip(...changes))
123
+ }
124
+ const tagged = 'import { add } from "../src/calc.js"\nit("[AC-1] adds")\nit("[AC-2] subtracts")'
125
+
126
+ it("passes when every ID is named in a test the pass wrote", () => {
127
+ expect(
128
+ refusal(file("src/calc.ts", "modified", "x"), file("tests/calc.test.ts", "added", tagged)),
129
+ ).toBe(undefined)
130
+ })
131
+
132
+ it("names every ID no test of the pass mentions", () => {
133
+ const message = refusal(file("tests/calc.test.ts", "added", 'it("[AC-12] x")'))
134
+ expect(message).toMatch(/^acceptance-tests: /)
135
+ expect(message).toContain("AC-1, AC-2")
136
+ })
137
+
138
+ it("does not count an ID in a non-test file", () => {
139
+ expect(refusal(file("src/calc.ts", "modified", "AC-1 AC-2"))).toContain("AC-1, AC-2")
140
+ })
141
+
142
+ it("refuses a test importing a file the pass added, resolved from the test's directory", () => {
143
+ const message = refusal(
144
+ file("src/naive/helper.ts", "added", "export const h = 1"),
145
+ file("src/naive/helper.test.ts", "added", `import { h } from "./helper.js"\n${tagged}`),
146
+ )
147
+ expect(message).toMatch(/^acceptance-internals: /)
148
+ expect(message).toContain("src/naive/helper.test.ts → src/naive/helper.ts")
149
+ })
150
+
151
+ it("refuses an aliased or directory import of an added module", () => {
152
+ const added = file("src/naive/index.ts", "added", "")
153
+ expect(
154
+ refusal(added, file("tests/a.test.ts", "added", `import "@/naive"\n${tagged}`)),
155
+ ).toContain("acceptance-internals")
156
+ expect(
157
+ refusal(added, file("tests/a.test.ts", "added", `import "../src/naive/index.js"\n${tagged}`)),
158
+ ).toContain("acceptance-internals")
159
+ })
160
+
161
+ it("counts only the lines the pass added: an ID an earlier feature left does not", () => {
162
+ const old = 'it("[AC-1] old")\nit("[AC-2] old")'
163
+ const message = refusal({
164
+ path: "tests/calc.test.ts",
165
+ status: "modified",
166
+ before: old,
167
+ after: `${old}\nit("[AC-2] new")`,
168
+ })
169
+ expect(message).toContain("names AC-1 —")
170
+ })
171
+
172
+ it("refuses a bare . or .. import of an added index", () => {
173
+ expect(
174
+ refusal(
175
+ file("src/naive/index.ts", "added", ""),
176
+ file("src/naive/__tests__/a.test.ts", "added", `import { x } from ".."\n${tagged}`),
177
+ ),
178
+ ).toContain("acceptance-internals")
179
+ })
180
+
181
+ it("reads a URL route or path-like data as no import", () => {
182
+ expect(
183
+ refusal(
184
+ file("src/routes/users.ts", "added", ""),
185
+ file(
186
+ "tests/a.test.ts",
187
+ "added",
188
+ `request(app).get("/users")\nrun("src/routes/users.ts")\n${tagged}`,
189
+ ),
190
+ ),
191
+ ).toBe(undefined)
192
+ })
193
+
194
+ it("reads no import out of a .feature file", () => {
195
+ expect(
196
+ refusal(
197
+ file("src/subtract.ts", "added", ""),
198
+ file("features/calc.feature", "added", `Given a file "src/subtract.ts"\n${tagged}`),
199
+ ),
200
+ ).toBe(undefined)
201
+ })
202
+
203
+ it("skips commented-out imports and takes a package subpath as no repository path", () => {
204
+ expect(
205
+ refusal(
206
+ file("src/naive/helper.ts", "added", ""),
207
+ file("src/date-fns/format.ts", "added", ""),
208
+ file(
209
+ "src/naive/a.test.ts",
210
+ "added",
211
+ `// import { h } from "./helper.js"\nimport { format } from "date-fns/format"\n${tagged}`,
212
+ ),
213
+ ),
214
+ ).toBe(undefined)
215
+ })
216
+
217
+ it("lets a test reach an existing module, a same-named file elsewhere, or a URL", () => {
218
+ expect(
219
+ refusal(
220
+ file("src/naive/api.ts", "added", ""),
221
+ file("src/calc.ts", "modified", ""),
222
+ file(
223
+ "tests/a.test.ts",
224
+ "added",
225
+ `import "../src/calc.js"\nimport "./api.js"\nfetch("https://x/api")\n${tagged}`,
226
+ ),
227
+ ),
228
+ ).toBe(undefined)
229
+ })
230
+ })
231
+
101
232
  const REVIEW = ".gtd/REVIEW.md"
102
233
  const REFACTOR = ".gtd/REFACTOR.md"
103
234
  const reviewDoc = [
@@ -124,7 +255,10 @@ const driveSeam = async (
124
255
  answer: string,
125
256
  stray: Change = change("src/a.ts", "modified"),
126
257
  vars: Record<string, string> = {},
258
+ reds = 0,
127
259
  ): Promise<Seamed> => {
260
+ let red = false
261
+ let redsLeft = reds
128
262
  const names: string[] = []
129
263
  const slices: string[] = []
130
264
  const inside: Record<string, string[]> = {}
@@ -158,11 +292,17 @@ const driveSeam = async (
158
292
  read: (path: string) => files.get(path),
159
293
  head: () => current,
160
294
  kept: () => tip(stray),
161
- changes: () => [],
295
+ changes: () => (red ? [change(".gtd/FEEDBACK.md", "added")] : []),
162
296
  changesSince: () => [],
163
297
  step: (request: StepRequest) => {
164
298
  if (request.kind === "restart") return Promise.resolve()
165
299
  const full = [...stack, request.name].join(".")
300
+ red = full === "seam.check" && redsLeft-- > 0
301
+ if (full === "seam.check") {
302
+ if (red) files.set(".gtd/FEEDBACK.md", "FAIL")
303
+ else files.delete(".gtd/FEEDBACK.md")
304
+ }
305
+ if (full === "seam.clear") files.delete(".gtd/FEEDBACK.md")
166
306
  names.push(full)
167
307
  for (const name of open) inside[name]!.push(full)
168
308
  effects[request.name]?.()
@@ -185,7 +325,37 @@ describe("seamBuild", () => {
185
325
  const h = await driveSeam("land")
186
326
  expect(h.outcome).toEqual({ reference: "abc", base: "c-base" })
187
327
  expect(h.slices).toEqual([])
188
- expect(h.names).toEqual(["seam.naive", "seam.judge"])
328
+ expect(h.names).toEqual(["seam.naive", "seam.check", "seam.judge"])
329
+ })
330
+
331
+ it("a red check retries the pass, and the judge sees it only once green", async () => {
332
+ const h = await driveSeam("land", undefined, {}, 1)
333
+ expect(h.names).toEqual(["seam.naive", "seam.check", "seam.naive", "seam.check", "seam.judge"])
334
+ })
335
+
336
+ it("an overruled red pass gets a fresh budget for the judge's rejects", async () => {
337
+ const h = await driveSeam("reject", undefined, {}, 3)
338
+ const steers = h.names.flatMap((name, i) => (name === "seam.steer" ? [i] : []))
339
+ expect(steers).toHaveLength(2)
340
+ expect(h.names.slice(steers[0]! + 1, steers[1]).filter((n) => n === "seam.judge")).toHaveLength(
341
+ 3,
342
+ )
343
+ })
344
+
345
+ it("a third red rests at seam.steer, and a clean landing there takes the red pass to the judge", async () => {
346
+ const h = await driveSeam("land", undefined, {}, 3)
347
+ expect(h.names).toEqual([
348
+ "seam.naive",
349
+ "seam.check",
350
+ "seam.naive",
351
+ "seam.check",
352
+ "seam.naive",
353
+ "seam.check",
354
+ "seam.steer",
355
+ "seam.clear",
356
+ "seam.judge",
357
+ ])
358
+ expect(h.outcome).toEqual({ reference: "abc", base: "c-base" })
189
359
  })
190
360
 
191
361
  it("refactor-first runs the reviewed refactor in slice refactor and resolves HEAD after it", async () => {