@pmelab/gtd 32.1.0 → 34.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +66 -53
- package/dist/chunks/{reserved-2fV0mj14.mjs → reserved-BcJgt5_R.mjs} +79 -12
- package/dist/chunks/{tail-BynhCfuv.mjs → tail-CM_fa93v.mjs} +1 -1
- package/dist/testkit.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +174 -40
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +249 -48
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +2 -2
- package/src/testkit/steps.ts +4 -0
- package/src/workflows/feature/acceptance-criteria.feature +8 -0
- package/src/workflows/feature/fast-path.feature +11 -35
- package/src/workflows/feature/feature.test.ts +9 -100
- package/src/workflows/feature/feature.ts +29 -16
- package/src/workflows/feature/flag-mode.feature +2 -91
- package/src/workflows/feature/naive-acceptance.feature +140 -0
- package/src/workflows/feature/one-step-land.feature +11 -34
- package/src/workflows/feature/quality-unresolved.feature +96 -0
- package/src/workflows/feature/refactor-absorbs.feature +17 -25
- package/src/workflows/feature/refactor-first.feature +11 -35
- package/src/workflows/feature/refactor-lens-models.feature +6 -1
- package/src/workflows/feature/refactor-review.feature +67 -360
- package/src/workflows/feature/refactor.ts +4 -3
- package/src/workflows/feature/risk-tier.feature +10 -0
- package/src/workflows/feature/seam-rejection.feature +20 -37
- package/src/workflows/feature/seam.feature +66 -113
- package/src/workflows/feature/seam.test.ts +173 -3
- package/src/workflows/feature/seam.ts +196 -31
- package/src/workflows/review/review-door.feature +108 -0
- package/src/workflows/shared/tail.ts +2 -2
- package/src/workflows/shared/text.test.ts +40 -0
- package/src/workflows/shared/text.ts +110 -16
|
@@ -41,24 +41,17 @@ 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.
|
|
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
|
"""
|
|
48
53
|
And step "implement.health.check" changes nothing
|
|
49
54
|
And step "implement.closing" runs its script
|
|
50
|
-
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
51
|
-
"""
|
|
52
|
-
# Review: abc1234
|
|
53
|
-
|
|
54
|
-
<!-- base: 0000000 -->
|
|
55
|
-
|
|
56
|
-
## calc
|
|
57
|
-
|
|
58
|
-
- [ ] ./src/calc.ts#1-2
|
|
59
|
-
subtract added
|
|
60
|
-
"""
|
|
61
|
-
And step "build.review.closing" runs its script
|
|
62
55
|
When the process is driven
|
|
63
56
|
Then the process rests at "seam.judge"
|
|
64
57
|
|
|
@@ -87,6 +80,10 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
|
|
|
87
80
|
- AC-1: the change behaves as described.
|
|
88
81
|
"""
|
|
89
82
|
And step "seam.naive" from now on changes nothing
|
|
83
|
+
And step "seam.naive" writes "src/acceptance.test.ts" with:
|
|
84
|
+
"""
|
|
85
|
+
it("[AC-1] holds", () => {})
|
|
86
|
+
"""
|
|
90
87
|
And step "seam.naive" writes "src/calc.ts" with:
|
|
91
88
|
"""
|
|
92
89
|
export const add = (a: number, b: number) => a + b
|
|
@@ -104,21 +101,6 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
|
|
|
104
101
|
"""
|
|
105
102
|
When gtd lands
|
|
106
103
|
And the process is driven
|
|
107
|
-
Then the process rests at "build.review.await-review"
|
|
108
|
-
|
|
109
|
-
Given a file ".gtd/REVIEW.md" with:
|
|
110
|
-
"""
|
|
111
|
-
# Review: abc1234
|
|
112
|
-
|
|
113
|
-
<!-- base: 0000000 -->
|
|
114
|
-
|
|
115
|
-
## calc
|
|
116
|
-
|
|
117
|
-
- [x] ./src/calc.ts#1-2
|
|
118
|
-
subtract added
|
|
119
|
-
"""
|
|
120
|
-
When gtd lands
|
|
121
|
-
And the process is driven
|
|
122
104
|
Then the process rests at "idle"
|
|
123
105
|
And the commit subjects end with:
|
|
124
106
|
"""
|
|
@@ -127,24 +109,25 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
|
|
|
127
109
|
gtd(check): start-gate.check → design.triage
|
|
128
110
|
gtd(agent): design.triage → design.gate.answer
|
|
129
111
|
gtd(human): design.gate.answer → seam.naive
|
|
130
|
-
gtd(agent): seam.naive → seam.
|
|
112
|
+
gtd(agent): seam.naive → seam.check
|
|
113
|
+
gtd(check): seam.check → seam.judge
|
|
131
114
|
gtd(judge): seam.judge → seam.naive
|
|
132
|
-
gtd(agent): seam.naive → seam.
|
|
115
|
+
gtd(agent): seam.naive → seam.check
|
|
116
|
+
gtd(check): seam.check → seam.judge
|
|
133
117
|
gtd(judge): seam.judge → seam.naive
|
|
134
|
-
gtd(agent): seam.naive → seam.
|
|
118
|
+
gtd(agent): seam.naive → seam.check
|
|
119
|
+
gtd(check): seam.check → seam.judge
|
|
135
120
|
gtd(judge): seam.judge → seam.steer
|
|
136
121
|
gtd(human): seam.steer → seam.naive
|
|
137
|
-
gtd(agent): seam.naive → seam.
|
|
122
|
+
gtd(agent): seam.naive → seam.check
|
|
123
|
+
gtd(check): seam.check → seam.judge
|
|
138
124
|
gtd(judge): seam.judge → implement.building
|
|
139
125
|
gtd(agent): implement.building → implement.health.check
|
|
140
126
|
gtd(check): implement.health.check → implement.closing
|
|
141
|
-
gtd(check): implement.closing →
|
|
142
|
-
gtd(agent): build.review.reviewing → build.review.await-review
|
|
143
|
-
gtd(human): build.review.await-review → build.review.closing
|
|
144
|
-
gtd(check): build.review.closing → idle
|
|
127
|
+
gtd(check): implement.closing → idle
|
|
145
128
|
"""
|
|
146
129
|
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.
|
|
130
|
+
And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
|
|
148
131
|
And the commit "gtd(judge): seam.judge → seam.naive" has a body containing "\"answer\":\"reject\""
|
|
149
132
|
And the commit "gtd(judge): seam.judge → seam.steer" has a body containing "\"answer\":\"reject\""
|
|
150
133
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
102
|
-
|
|
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.
|
|
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
|
|
@@ -121,18 +146,23 @@ Feature: the naive pass and seam judge decide how a feature is built
|
|
|
121
146
|
And step "implement.health.check" changes nothing
|
|
122
147
|
And step "implement.closing" runs its script
|
|
123
148
|
When the process is driven
|
|
124
|
-
Then the process rests at "
|
|
149
|
+
Then the process rests at "idle"
|
|
125
150
|
And the commit subjects end with:
|
|
126
151
|
"""
|
|
127
152
|
gtd(judge): seam.judge → implement.building
|
|
128
153
|
gtd(agent): implement.building → implement.health.check
|
|
129
154
|
gtd(check): implement.health.check → implement.closing
|
|
130
|
-
gtd(check): implement.closing →
|
|
155
|
+
gtd(check): implement.closing → idle
|
|
131
156
|
"""
|
|
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.
|
|
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 →
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
"""
|
|
@@ -227,99 +276,3 @@ Feature: the naive pass and seam judge decide how a feature is built
|
|
|
227
276
|
gtd(judge): seam.judge → seam.steer
|
|
228
277
|
gtd(human): seam.steer → implement.building
|
|
229
278
|
"""
|
|
230
|
-
|
|
231
|
-
Scenario: review feedback after land re-runs design and implement, never the seam phase
|
|
232
|
-
Given step "seam.naive" writes "src/calc.ts" with:
|
|
233
|
-
"""
|
|
234
|
-
export const add = (a: number, b: number) => b + a
|
|
235
|
-
"""
|
|
236
|
-
And judge step "seam.judge" answers:
|
|
237
|
-
"""
|
|
238
|
-
[{"id":"shape","answer":"land","p":0.9}]
|
|
239
|
-
"""
|
|
240
|
-
And step "implement.building" writes "src/calc.ts" with:
|
|
241
|
-
"""
|
|
242
|
-
export const add = (a: number, b: number) => a + b
|
|
243
|
-
export const SUBTRACT = process.env.GTD_FLAG_SUBTRACT === "1"
|
|
244
|
-
"""
|
|
245
|
-
And step "implement.health.check" changes nothing
|
|
246
|
-
And step "implement.closing" runs its script
|
|
247
|
-
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
248
|
-
"""
|
|
249
|
-
# Review: abc1234
|
|
250
|
-
|
|
251
|
-
<!-- base: 0000000 -->
|
|
252
|
-
|
|
253
|
-
## calc
|
|
254
|
-
|
|
255
|
-
- [ ] ./src/calc.ts#1-1
|
|
256
|
-
new add function
|
|
257
|
-
"""
|
|
258
|
-
When the process is driven
|
|
259
|
-
Then the process rests at "build.review.await-review"
|
|
260
|
-
Given a file ".gtd/REVIEW.md" with:
|
|
261
|
-
"""
|
|
262
|
-
# Review: abc1234
|
|
263
|
-
|
|
264
|
-
<!-- base: 0000000 -->
|
|
265
|
-
|
|
266
|
-
## calc
|
|
267
|
-
|
|
268
|
-
- [ ] ./src/calc.ts#1-1
|
|
269
|
-
new add function[^why]
|
|
270
|
-
|
|
271
|
-
[^why]:
|
|
272
|
-
- H: also export multiply
|
|
273
|
-
"""
|
|
274
|
-
And step "build.review.collecting" writes ".gtd/REQUIREMENTS.md" with:
|
|
275
|
-
"""
|
|
276
|
-
# Requirements
|
|
277
|
-
|
|
278
|
-
## Concerns
|
|
279
|
-
|
|
280
|
-
### Multiply
|
|
281
|
-
|
|
282
|
-
- Export `multiply()`.
|
|
283
|
-
"""
|
|
284
|
-
And step "build.review.collecting" writes ".gtd/REVIEW.md" with:
|
|
285
|
-
"""
|
|
286
|
-
# Review: abc1234
|
|
287
|
-
|
|
288
|
-
<!-- base: 0000000 -->
|
|
289
|
-
|
|
290
|
-
## calc
|
|
291
|
-
|
|
292
|
-
- [ ] ./src/calc.ts#1-1
|
|
293
|
-
new add function[^why]
|
|
294
|
-
|
|
295
|
-
[^why]:
|
|
296
|
-
- H: also export multiply
|
|
297
|
-
- A: noted in the requirements.
|
|
298
|
-
"""
|
|
299
|
-
When gtd lands
|
|
300
|
-
And the process is driven
|
|
301
|
-
Then the process rests at "build.review.await-review"
|
|
302
|
-
Given a file ".gtd/REVIEW.md" with:
|
|
303
|
-
"""
|
|
304
|
-
# Review: abc1234
|
|
305
|
-
|
|
306
|
-
<!-- base: 0000000 -->
|
|
307
|
-
|
|
308
|
-
## calc
|
|
309
|
-
|
|
310
|
-
- [ ] ./src/calc.ts#1-1
|
|
311
|
-
new add function
|
|
312
|
-
"""
|
|
313
|
-
And step "build.review.closing" runs its script
|
|
314
|
-
And step "re-unwind" runs its script
|
|
315
|
-
When gtd lands
|
|
316
|
-
And the process is driven
|
|
317
|
-
Then the process rests at "implement.building"
|
|
318
|
-
And the commit subjects end with:
|
|
319
|
-
"""
|
|
320
|
-
gtd(check): build.review.closing → re-unwind
|
|
321
|
-
gtd(check): re-unwind → design.triage
|
|
322
|
-
gtd(agent): design.triage → design.gate.answer
|
|
323
|
-
gtd(human): design.gate.answer → implement.building
|
|
324
|
-
gtd(agent): implement.building
|
|
325
|
-
"""
|
|
@@ -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 {
|
|
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 () => {
|