@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +18 -15
- package/dist/chunks/{reserved-2fV0mj14.mjs → reserved-BU5Zi-nX.mjs} +67 -11
- package/dist/chunks/{tail-BynhCfuv.mjs → tail-DuXanjIY.mjs} +1 -1
- package/dist/testkit.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +156 -32
- 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 +220 -40
- package/package.json +1 -1
- package/src/workflows/feature/acceptance-criteria.feature +8 -0
- package/src/workflows/feature/fast-path.feature +8 -2
- package/src/workflows/feature/feature.test.ts +19 -9
- package/src/workflows/feature/naive-acceptance.feature +140 -0
- package/src/workflows/feature/one-step-land.feature +8 -2
- package/src/workflows/feature/refactor-absorbs.feature +6 -1
- package/src/workflows/feature/refactor-first.feature +9 -3
- package/src/workflows/feature/refactor-lens-models.feature +6 -1
- package/src/workflows/feature/refactor-review.feature +50 -5
- package/src/workflows/feature/refactor.ts +4 -3
- package/src/workflows/feature/risk-tier.feature +10 -0
- package/src/workflows/feature/seam-rejection.feature +19 -6
- package/src/workflows/feature/seam.feature +70 -16
- package/src/workflows/feature/seam.test.ts +173 -3
- package/src/workflows/feature/seam.ts +195 -30
- package/src/workflows/shared/text.test.ts +40 -0
- package/src/workflows/shared/text.ts +97 -16
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =>
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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.
|
|
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
|
"""
|
|
@@ -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.
|
|
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 {
|
|
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 () => {
|