@pmelab/gtd 53.0.0 → 55.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 +56 -50
- package/dist/chunks/{reserved-CQ7amcKG.mjs → reserved-DHafcW7t.mjs} +47 -27
- package/dist/chunks/{review-CNgN_V8T.mjs → review-BJGKwlYv.mjs} +12 -5
- package/dist/chunks/{tail-JZ9PAOwp.mjs → tail-B9Hw2AFB.mjs} +12 -8
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +41 -94
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +107 -129
- package/package.json +1 -1
- package/src/testkit/steps.ts +23 -0
- package/src/workflows/feature/criteria-coverage.feature +109 -0
- package/src/workflows/feature/feature.ts +9 -4
- package/src/workflows/feature/green.ts +2 -5
- package/src/workflows/feature/lens-review.feature +50 -0
- package/src/workflows/feature/naive-acceptance.feature +31 -32
- package/src/workflows/feature/prototype.ts +5 -8
- package/src/workflows/feature/refactor-rebuild.feature +16 -12
- package/src/workflows/feature/refactor-review.feature +38 -0
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -31
- package/src/workflows/feature/seam.test.ts +19 -101
- package/src/workflows/feature/seam.ts +13 -89
- package/src/workflows/fix/fix-quality.feature +4 -2
- package/src/workflows/shared/access.test.ts +1 -0
- package/src/workflows/shared/review.test.ts +3 -1
- package/src/workflows/shared/review.ts +12 -2
- package/src/workflows/shared/skills.test.ts +3 -1
- package/src/workflows/shared/steps.test.ts +9 -6
- package/src/workflows/shared/steps.ts +12 -3
- package/src/workflows/shared/tail.test.ts +20 -2
- package/src/workflows/shared/tail.ts +20 -7
- package/src/workflows/shared/text.test.ts +19 -6
- package/src/workflows/shared/text.ts +50 -29
- package/src/workflows/shared/vars.ts +1 -1
|
@@ -57,6 +57,8 @@ export const buildFeature = async (firstBase: string): Promise<void> => {
|
|
|
57
57
|
const handedOut = new Set(listedIds())
|
|
58
58
|
const seam = await seamBuild(firstBase)
|
|
59
59
|
const used = gather(handedOut)
|
|
60
|
+
// Read before the implementation closes the file: the quality lap's coverage lens checks them.
|
|
61
|
+
const requirements = read(REQUIREMENTS)
|
|
60
62
|
const closed = await implementing({
|
|
61
63
|
reference: seam.reference,
|
|
62
64
|
tier,
|
|
@@ -72,7 +74,7 @@ export const buildFeature = async (firstBase: string): Promise<void> => {
|
|
|
72
74
|
!changesSince(seam.base).some((c) => isCode(c.path) && !carried.includes(c.path))
|
|
73
75
|
? firstBase
|
|
74
76
|
: seam.base
|
|
75
|
-
const outcome = await reviewedTail(base, base)
|
|
77
|
+
const outcome = await reviewedTail(base, base, requirements)
|
|
76
78
|
await featureFeedback(outcome, base, { closed, used })
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -87,11 +89,12 @@ const gather = (handedOut: Set<string>): readonly string[] => {
|
|
|
87
89
|
const reviewedTail = (
|
|
88
90
|
base: string,
|
|
89
91
|
lensBase: string,
|
|
92
|
+
requirements: string | undefined,
|
|
90
93
|
lap: "first" | "feedback" = "first",
|
|
91
94
|
): Promise<ReviewOutcome> =>
|
|
92
95
|
tail(
|
|
93
96
|
(escalations, report) => humanReview(base, escalations, { report, walkthrough: true, lap }),
|
|
94
|
-
{ green: true, lensBase },
|
|
97
|
+
{ green: true, lensBase, requirements },
|
|
95
98
|
)
|
|
96
99
|
|
|
97
100
|
// seam.naive / seam.judge never run on a feedback lap: its design → implement is built unjudged
|
|
@@ -113,8 +116,9 @@ const featureFeedback = async (
|
|
|
113
116
|
gather(handedOut)
|
|
114
117
|
})
|
|
115
118
|
const tier = riskTierOn() ? recordRisk() : undefined
|
|
119
|
+
const requirements = read(REQUIREMENTS)
|
|
116
120
|
closed = await implementing({ tier, amend: true })
|
|
117
|
-
outcome = await reviewedTail(outcome.base, lensBase, "feedback")
|
|
121
|
+
outcome = await reviewedTail(outcome.base, lensBase, requirements, "feedback")
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
|
|
@@ -133,8 +137,9 @@ export const afterTail = async (first: ReviewOutcome): Promise<void> => {
|
|
|
133
137
|
await reUnwind(outcome)
|
|
134
138
|
await design(outcome.base)
|
|
135
139
|
const tier = riskTierOn() ? recordRisk() : undefined
|
|
140
|
+
const requirements = read(REQUIREMENTS)
|
|
136
141
|
await implement({ tier, amend: true })
|
|
137
|
-
outcome = await buildTail(outcome.base, { green: true })
|
|
142
|
+
outcome = await buildTail(outcome.base, { green: true, requirements })
|
|
138
143
|
}
|
|
139
144
|
}
|
|
140
145
|
|
|
@@ -3,7 +3,7 @@ import { coder, FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
|
|
|
3
3
|
import * as t from "../shared/text.js"
|
|
4
4
|
import { requireCriteriaFrozen } from "./criteria.js"
|
|
5
5
|
|
|
6
|
-
export const green = (
|
|
6
|
+
export const green = (): Promise<void> =>
|
|
7
7
|
healthy(
|
|
8
8
|
() =>
|
|
9
9
|
t.agentWithSkills("fix.suite.fixing", t.implementFixPrompt(), {
|
|
@@ -13,10 +13,7 @@ export const green = (guard?: () => void): Promise<void> =>
|
|
|
13
13
|
system: t.builderSystem(),
|
|
14
14
|
}),
|
|
15
15
|
{
|
|
16
|
-
guard: () =>
|
|
17
|
-
requireCriteriaFrozen(REQUIREMENTS)
|
|
18
|
-
guard?.()
|
|
19
|
-
},
|
|
16
|
+
guard: () => requireCriteriaFrozen(REQUIREMENTS),
|
|
20
17
|
guardRests: false,
|
|
21
18
|
},
|
|
22
19
|
)
|
|
@@ -161,6 +161,56 @@ Feature: the quality lap's findings are triaged before the review
|
|
|
161
161
|
gtd(check): build.review.carrying → build.review.await-review
|
|
162
162
|
"""
|
|
163
163
|
|
|
164
|
+
Scenario: a risk is fixed before the human sees the review, re-reviewed in the reviewer's own draft
|
|
165
|
+
Given judge step "build.quality.triage" answers:
|
|
166
|
+
"""
|
|
167
|
+
[{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"noise","p":0.85},{"id":"Q3","answer":"design","p":0.8}]
|
|
168
|
+
"""
|
|
169
|
+
And step "build.fix.quality.fixing" writes "src/calc.test.ts" with:
|
|
170
|
+
"""
|
|
171
|
+
it("[AC-1] subtracts", () => {})
|
|
172
|
+
it("subtracts a larger number", () => {})
|
|
173
|
+
"""
|
|
174
|
+
And step "build.fix.quality.fixing" deletes ".gtd/QUALITY.md"
|
|
175
|
+
And step "build.health.check" changes nothing
|
|
176
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
177
|
+
"""
|
|
178
|
+
# Review: abc1234
|
|
179
|
+
|
|
180
|
+
<!-- base: 0000000 -->
|
|
181
|
+
|
|
182
|
+
## Subtract
|
|
183
|
+
|
|
184
|
+
- [ ] ./src/calc.ts#2-2 — Risk: overflows on large numbers
|
|
185
|
+
|
|
186
|
+
## AC-1 ignores floating point
|
|
187
|
+
|
|
188
|
+
- [ ] ./src/calc.ts#2-2 — where it applies
|
|
189
|
+
"""
|
|
190
|
+
When the process is driven
|
|
191
|
+
Then the process rests at "build.review.fix.risks.fixing"
|
|
192
|
+
Given step "build.review.reviewing" is no longer scripted
|
|
193
|
+
And step "build.review.reviewing" replaces the text "Risk: overflows on large numbers" in ".gtd/REVIEW.md" with "subtract(a, b), now guarded"
|
|
194
|
+
And step "build.review.fix.risks.fixing" changes nothing
|
|
195
|
+
When the process is driven
|
|
196
|
+
Then the process rests at "build.review.await-review"
|
|
197
|
+
And ".gtd/REVIEW.md" contains "subtract(a, b), now guarded"
|
|
198
|
+
And ".gtd/REVIEW.md" contains "## Fixed automatically" exactly once
|
|
199
|
+
And ".gtd/REVIEW.md" contains "## Dropped as noise" exactly once
|
|
200
|
+
And ".gtd/REVIEW.md" contains "## Walkthrough" exactly once
|
|
201
|
+
And ".gtd/REVIEW.md" contains "Should subtract round? The criterion does not say." exactly once
|
|
202
|
+
And ".gtd/REVIEW.md" contains "Tick exactly one:" exactly once
|
|
203
|
+
And the commit subjects end with:
|
|
204
|
+
"""
|
|
205
|
+
gtd(check): build.health.check → build.review.reviewing
|
|
206
|
+
gtd(agent): build.review.reviewing → build.review.fix.risks.fixing
|
|
207
|
+
gtd(agent): build.review.fix.risks.fixing → build.health.check
|
|
208
|
+
gtd(check): build.health.check → build.review.reviewing
|
|
209
|
+
gtd(agent): build.review.reviewing → build.review.ranking
|
|
210
|
+
gtd(judge): build.review.ranking → build.review.carrying
|
|
211
|
+
gtd(check): build.review.carrying → build.review.await-review
|
|
212
|
+
"""
|
|
213
|
+
|
|
164
214
|
Scenario: an unsure verdict makes a finding a note, never a fix or a drop
|
|
165
215
|
Given judge step "build.quality.triage" answers:
|
|
166
216
|
"""
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
@inmem
|
|
2
|
-
Feature: a naive pass ends only when
|
|
2
|
+
Feature: a naive pass ends only when its tests reach no file it added and the suite is green
|
|
3
3
|
|
|
4
|
-
`seam.naive`
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Three failed attempts rest at `seam.steer`.
|
|
4
|
+
`seam.naive` needs no test at all. A pass whose test imports a file the pass
|
|
5
|
+
added is sent back; `seam.check` then runs the suite with the kept pass
|
|
6
|
+
applied, and a red run sends it back too, before the judge sees it. Three
|
|
7
|
+
failed attempts rest at `seam.steer`.
|
|
9
8
|
|
|
10
9
|
Background:
|
|
11
10
|
Given the shipped workflows, as gtd install scaffolds them
|
|
@@ -44,15 +43,30 @@ Feature: a naive pass ends only when a test names every criterion and the suite
|
|
|
44
43
|
export const subtract = (a: number, b: number) => a - b
|
|
45
44
|
"""
|
|
46
45
|
|
|
47
|
-
Scenario: a pass
|
|
48
|
-
Given step "seam.
|
|
46
|
+
Scenario: a pass without a test goes on to the check and the judge
|
|
47
|
+
Given step "seam.check" changes nothing
|
|
48
|
+
When the process is driven
|
|
49
|
+
Then the process rests at "seam.judge"
|
|
50
|
+
And the commit subjects end with:
|
|
49
51
|
"""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
gtd(human): design.gate.answer → seam.naive
|
|
53
|
+
gtd(agent): seam.naive → seam.check
|
|
54
|
+
gtd(check): seam.check → seam.judge
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
Scenario: a test importing a file the pass added is sent back, and an overrule runs the check and sticks
|
|
58
|
+
Given step "seam.naive" writes "src/subtract.ts" with:
|
|
59
|
+
"""
|
|
60
|
+
export const subtract = (a: number, b: number) => a - b
|
|
61
|
+
"""
|
|
62
|
+
And step "seam.naive" writes "src/subtract.test.ts" with:
|
|
63
|
+
"""
|
|
64
|
+
import { subtract } from "./subtract.js"
|
|
65
|
+
it("subtracts", () => expect(subtract(5, 3)).toBe(2))
|
|
52
66
|
"""
|
|
53
67
|
When the process is driven
|
|
54
68
|
Then the process rests at "seam.steer"
|
|
55
|
-
And the resting step's content contains "
|
|
69
|
+
And the resting step's content contains "src/subtract.test.ts → src/subtract.ts"
|
|
56
70
|
|
|
57
71
|
Given step "seam.check" changes nothing
|
|
58
72
|
When gtd lands
|
|
@@ -87,27 +101,12 @@ Feature: a naive pass ends only when a test names every criterion and the suite
|
|
|
87
101
|
gtd(judge): seam.judge → seam.steer
|
|
88
102
|
"""
|
|
89
103
|
|
|
90
|
-
Scenario: a test importing a file the pass added is sent back
|
|
91
|
-
Given step "seam.naive" writes "src/subtract.ts" with:
|
|
92
|
-
"""
|
|
93
|
-
export const subtract = (a: number, b: number) => a - b
|
|
94
|
-
"""
|
|
95
|
-
And step "seam.naive" writes "src/subtract.test.ts" with:
|
|
96
|
-
"""
|
|
97
|
-
import { subtract } from "./subtract.js"
|
|
98
|
-
it("[AC-1] subtracts", () => expect(subtract(5, 3)).toBe(2))
|
|
99
|
-
it("[AC-2] still adds", () => {})
|
|
100
|
-
"""
|
|
101
|
-
When the process is driven
|
|
102
|
-
Then the process rests at "seam.steer"
|
|
103
|
-
And the resting step's content contains "src/subtract.test.ts → src/subtract.ts"
|
|
104
|
-
|
|
105
104
|
Scenario: a red suite sends the pass back, and it moves on only once green
|
|
106
105
|
Given step "seam.naive" writes "tests/calc.test.ts" with:
|
|
107
106
|
"""
|
|
108
107
|
import { add, subtract } from "../src/calc.js"
|
|
109
|
-
it("
|
|
110
|
-
it("
|
|
108
|
+
it("subtracts", () => expect(subtract(5, 3)).toBe(2))
|
|
109
|
+
it("still adds", () => expect(add(1, 1)).toBe(2))
|
|
111
110
|
"""
|
|
112
111
|
When the process is driven
|
|
113
112
|
Then the process rests at "seam.check"
|
|
@@ -118,18 +117,18 @@ Feature: a naive pass ends only when a test names every criterion and the suite
|
|
|
118
117
|
Given step "seam.naive" is no longer scripted
|
|
119
118
|
And step "seam.check" writes ".gtd/FEEDBACK.md" with:
|
|
120
119
|
"""
|
|
121
|
-
FAIL tests/calc.test.ts >
|
|
120
|
+
FAIL tests/calc.test.ts > subtracts
|
|
122
121
|
"""
|
|
123
122
|
When the process is driven
|
|
124
123
|
Then the process rests at "seam.naive"
|
|
125
124
|
And the resting step's content contains "left the suite red"
|
|
126
|
-
And ".gtd/FEEDBACK.md" contains "
|
|
125
|
+
And ".gtd/FEEDBACK.md" contains "> subtracts"
|
|
127
126
|
|
|
128
127
|
Given step "seam.naive" writes "tests/calc.test.ts" with:
|
|
129
128
|
"""
|
|
130
129
|
import { add, subtract } from "../src/calc.js"
|
|
131
|
-
it("
|
|
132
|
-
it("
|
|
130
|
+
it("subtracts", () => expect(subtract(5, 3)).toBe(2))
|
|
131
|
+
it("still adds", () => expect(add(2, 2)).toBe(4))
|
|
133
132
|
"""
|
|
134
133
|
And step "seam.check" from now on changes nothing
|
|
135
134
|
When the process is driven
|
|
@@ -3,7 +3,6 @@ import { HEADING, markFenced } from "../shared/fences.js"
|
|
|
3
3
|
import { coder, PROTOTYPE, REQUIREMENTS } from "../shared/steps.js"
|
|
4
4
|
import * as t from "../shared/text.js"
|
|
5
5
|
import { defaults } from "../shared/vars.js"
|
|
6
|
-
import type { Guard } from "../shared/health.js"
|
|
7
6
|
import { green } from "./green.js"
|
|
8
7
|
import { criteriaOf, criterionIds, handsOnIds, requireCriteriaFrozen } from "./criteria.js"
|
|
9
8
|
|
|
@@ -130,7 +129,7 @@ const testing = async (
|
|
|
130
129
|
return requireReport(ids, retest)
|
|
131
130
|
}
|
|
132
131
|
|
|
133
|
-
const fixing = async (
|
|
132
|
+
const fixing = async (): Promise<void> => {
|
|
134
133
|
await scope("fix", () =>
|
|
135
134
|
t.agentWithSkills("fixing", t.prototypeFixingPrompt(), {
|
|
136
135
|
label: "Fixing what the hands-on test found",
|
|
@@ -145,7 +144,6 @@ const fixing = async (guard?: Guard): Promise<void> => {
|
|
|
145
144
|
if (changes().some((c) => c.path === PROTOTYPE))
|
|
146
145
|
refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`)
|
|
147
146
|
frozen()
|
|
148
|
-
guard?.()
|
|
149
147
|
}
|
|
150
148
|
|
|
151
149
|
/** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
|
|
@@ -161,10 +159,9 @@ const askPerson = async (name: string, message: string, label: string): Promise<
|
|
|
161
159
|
* Use the change by hand: a testing turn reports on every criterion a person
|
|
162
160
|
* can observe, a fix turn takes each failure, and the change is tested again —
|
|
163
161
|
* until the report is clean, or `prototypeRounds` fixes are spent and a person
|
|
164
|
-
* decides. A change with no such criterion is not tested by hand.
|
|
165
|
-
* holds every fix turn, as it holds the implementation's.
|
|
162
|
+
* decides. A change with no such criterion is not tested by hand.
|
|
166
163
|
*/
|
|
167
|
-
export const prototypeTest = async (
|
|
164
|
+
export const prototypeTest = async (): Promise<void> => {
|
|
168
165
|
const cap = prototypeRounds()
|
|
169
166
|
if (cap === 0) return
|
|
170
167
|
await scope("prototype", async () => {
|
|
@@ -185,7 +182,7 @@ export const prototypeTest = async (guard?: Guard): Promise<void> => {
|
|
|
185
182
|
} else if (open.length === 0) return
|
|
186
183
|
else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
|
|
187
184
|
fixes++
|
|
188
|
-
await fixing(
|
|
185
|
+
await fixing()
|
|
189
186
|
} else {
|
|
190
187
|
const changed = await askPerson(
|
|
191
188
|
"open",
|
|
@@ -195,7 +192,7 @@ export const prototypeTest = async (guard?: Guard): Promise<void> => {
|
|
|
195
192
|
if (!changed) return
|
|
196
193
|
fixes = 0
|
|
197
194
|
}
|
|
198
|
-
await green(
|
|
195
|
+
await green()
|
|
199
196
|
}
|
|
200
197
|
})
|
|
201
198
|
}
|
|
@@ -5,8 +5,9 @@ Feature: refactor-first writes a refactor note, refactors on its own, then rebui
|
|
|
5
5
|
On a refactor-first judgment the naive pass is never applied. `seam.note`
|
|
6
6
|
reads the whole naive diff and writes `.gtd/REFACTOR-NOTE.md`; the refactor
|
|
7
7
|
lands on its own with the existing suite green; the rebuild re-implements the
|
|
8
|
-
change on the refactored base
|
|
9
|
-
|
|
8
|
+
change on the refactored base, is asked to carry over every test file the
|
|
9
|
+
naive pass added or changed, and lands once the suite is green. The note
|
|
10
|
+
reaches both turns.
|
|
10
11
|
|
|
11
12
|
Background:
|
|
12
13
|
Given the shipped workflows, as gtd install scaffolds them
|
|
@@ -18,6 +19,10 @@ Feature: refactor-first writes a refactor note, refactors on its own, then rebui
|
|
|
18
19
|
"""
|
|
19
20
|
export const add = (a: number, b: number) => a + b
|
|
20
21
|
"""
|
|
22
|
+
And a file "src/calc.test.ts" with:
|
|
23
|
+
"""
|
|
24
|
+
it("adds", () => {})
|
|
25
|
+
"""
|
|
21
26
|
And the working tree is committed as "feat: add calculator"
|
|
22
27
|
And a file "TODO.md" with:
|
|
23
28
|
"""
|
|
@@ -49,7 +54,12 @@ Feature: refactor-first writes a refactor note, refactors on its own, then rebui
|
|
|
49
54
|
And step "seam.check" changes nothing
|
|
50
55
|
And step "seam.naive" writes "src/acceptance.test.ts" with:
|
|
51
56
|
"""
|
|
52
|
-
it("
|
|
57
|
+
it("subtracts", () => {})
|
|
58
|
+
"""
|
|
59
|
+
And step "seam.naive" writes "src/calc.test.ts" with:
|
|
60
|
+
"""
|
|
61
|
+
it("adds", () => {})
|
|
62
|
+
it("subtracts a larger number", () => {})
|
|
53
63
|
"""
|
|
54
64
|
And step "seam.naive" writes "src/calc.ts" with:
|
|
55
65
|
"""
|
|
@@ -125,7 +135,8 @@ Feature: refactor-first writes a refactor note, refactors on its own, then rebui
|
|
|
125
135
|
And the process is driven
|
|
126
136
|
Then the process rests at "implement.building"
|
|
127
137
|
And the resting step's content contains "shared `op(a, b, sign)`"
|
|
128
|
-
And the resting step's content contains "acceptance tests
|
|
138
|
+
And the resting step's content contains "Carry over the naive pass's acceptance tests"
|
|
139
|
+
And the resting step's content contains "'src/acceptance.test.ts' 'src/calc.test.ts'"
|
|
129
140
|
|
|
130
141
|
Scenario: a note turn that changes anything but the note is refused
|
|
131
142
|
Given step "seam.note" writes "src/calc.ts" with:
|
|
@@ -198,7 +209,7 @@ Feature: refactor-first writes a refactor note, refactors on its own, then rebui
|
|
|
198
209
|
And "src/calc.ts" contains "op(a, b, 1)"
|
|
199
210
|
And "src/calc.ts" does not contain "subtract"
|
|
200
211
|
|
|
201
|
-
Scenario: the rebuild lands
|
|
212
|
+
Scenario: the rebuild lands once green, whichever tests it carried
|
|
202
213
|
Given step "seam.note" writes ".gtd/REFACTOR-NOTE.md" with:
|
|
203
214
|
"""
|
|
204
215
|
Seam: a shared `op` in src/calc.ts.
|
|
@@ -254,18 +265,11 @@ Feature: refactor-first writes a refactor note, refactors on its own, then rebui
|
|
|
254
265
|
export const add = (a: number, b: number) => op(a, b, 1)
|
|
255
266
|
export const subtract = (a: number, b: number) => op(a, b, -1)
|
|
256
267
|
"""
|
|
257
|
-
Then driving the process is refused with "rebuild-acceptance"
|
|
258
|
-
|
|
259
|
-
Given step "implement.building" writes "src/acceptance.test.ts" with:
|
|
260
|
-
"""
|
|
261
|
-
it("[AC-1] subtracts", () => {})
|
|
262
|
-
"""
|
|
263
268
|
And step "implement.health.check" changes nothing
|
|
264
269
|
And step "implement.closing" runs its script
|
|
265
270
|
When the process is driven
|
|
266
271
|
Then the process rests at "build.review.reviewing"
|
|
267
272
|
And "src/calc.ts" contains "op(a, b, -1)"
|
|
268
|
-
And "src/acceptance.test.ts" contains "[AC-1]"
|
|
269
273
|
And ".gtd/REFACTOR-NOTE.md" does not exist
|
|
270
274
|
And the commit subjects end with:
|
|
271
275
|
"""
|
|
@@ -1185,6 +1185,44 @@ Feature: the refactor is reviewed on its own before the feature is implemented
|
|
|
1185
1185
|
"""
|
|
1186
1186
|
And no commit body contains "refactor.build.review.composing"
|
|
1187
1187
|
|
|
1188
|
+
Scenario: a risk the refactor's re-review marks again reaches the human unfixed
|
|
1189
|
+
Given step "refactor.build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
1190
|
+
"""
|
|
1191
|
+
# Review: abc1234
|
|
1192
|
+
|
|
1193
|
+
<!-- base: 0000000 -->
|
|
1194
|
+
|
|
1195
|
+
## calc
|
|
1196
|
+
|
|
1197
|
+
- [ ] ./src/calc.ts#1-2 — Risk: drops the sign
|
|
1198
|
+
"""
|
|
1199
|
+
When the process is driven
|
|
1200
|
+
Then the process rests at "refactor.build.review.fix.risks.fixing"
|
|
1201
|
+
Given step "refactor.build.review.reviewing" is no longer scripted
|
|
1202
|
+
And step "refactor.build.review.reviewing" replaces the text "Risk: drops the sign" in ".gtd/REVIEW.md" with "Risk: still drops the sign"
|
|
1203
|
+
And step "refactor.build.review.fix.risks.fixing" changes nothing
|
|
1204
|
+
And step "refactor.build.health.check" changes nothing
|
|
1205
|
+
When the process is driven
|
|
1206
|
+
Then the process rests at "refactor.build.review.await-review"
|
|
1207
|
+
And ".gtd/REVIEW.md" contains:
|
|
1208
|
+
"""
|
|
1209
|
+
## Walkthrough
|
|
1210
|
+
|
|
1211
|
+
### calc
|
|
1212
|
+
|
|
1213
|
+
- [ ] ./src/calc.ts#1-2 — Risk: still drops the sign
|
|
1214
|
+
"""
|
|
1215
|
+
And ".gtd/REVIEW.md" contains "## Walkthrough" exactly once
|
|
1216
|
+
And the commit subjects end with:
|
|
1217
|
+
"""
|
|
1218
|
+
gtd(agent): refactor.build.review.reviewing → refactor.build.review.fix.risks.fixing
|
|
1219
|
+
gtd(agent): refactor.build.review.fix.risks.fixing → refactor.build.health.check
|
|
1220
|
+
gtd(check): refactor.build.health.check → refactor.build.review.reviewing
|
|
1221
|
+
gtd(agent): refactor.build.review.reviewing → refactor.build.review.ranking
|
|
1222
|
+
gtd(judge): refactor.build.review.ranking → refactor.build.review.carrying
|
|
1223
|
+
gtd(check): refactor.build.review.carrying → refactor.build.review.await-review
|
|
1224
|
+
"""
|
|
1225
|
+
|
|
1188
1226
|
Rule: with a quality lens on both the refactor and the feature
|
|
1189
1227
|
|
|
1190
1228
|
Background:
|
|
@@ -179,37 +179,6 @@ Feature: a reviewer walks the human through the feature's code
|
|
|
179
179
|
"""
|
|
180
180
|
Then driving the process is refused with "AC-1 ignores floating point — no chunk of its own"
|
|
181
181
|
|
|
182
|
-
Scenario: a risk the reviewer marks is fixed before the human sees the review
|
|
183
|
-
Given step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
184
|
-
"""
|
|
185
|
-
# Review: abc1234
|
|
186
|
-
|
|
187
|
-
<!-- base: 0000000 -->
|
|
188
|
-
|
|
189
|
-
## Subtract
|
|
190
|
-
|
|
191
|
-
- [ ] ./src/calc.ts#2-2 — Risk: overflows on large numbers
|
|
192
|
-
|
|
193
|
-
## AC-1 ignores floating point
|
|
194
|
-
|
|
195
|
-
- [ ] ./src/calc.ts#2-2 — returns the raw difference
|
|
196
|
-
"""
|
|
197
|
-
And step "build.review.fix.risks.fixing" changes nothing
|
|
198
|
-
And step "build.health.check" changes nothing
|
|
199
|
-
When the process is driven
|
|
200
|
-
Then the process rests at "build.review.await-review"
|
|
201
|
-
And the commit subjects end with:
|
|
202
|
-
"""
|
|
203
|
-
gtd(agent): build.review.reviewing → build.review.ranking
|
|
204
|
-
gtd(judge): build.review.ranking → build.review.carrying
|
|
205
|
-
gtd(check): build.review.carrying → build.review.fix.risks.fixing
|
|
206
|
-
gtd(agent): build.review.fix.risks.fixing → build.health.check
|
|
207
|
-
gtd(check): build.health.check → build.review.reviewing
|
|
208
|
-
gtd(agent): build.review.reviewing → build.review.ranking
|
|
209
|
-
gtd(judge): build.review.ranking → build.review.carrying
|
|
210
|
-
gtd(check): build.review.carrying → build.review.await-review
|
|
211
|
-
"""
|
|
212
|
-
|
|
213
182
|
Scenario: a standard conflict the reviewer writes itself is refused
|
|
214
183
|
Given step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
215
184
|
"""
|
|
@@ -14,8 +14,6 @@ import {
|
|
|
14
14
|
naivePass,
|
|
15
15
|
acceptanceFault,
|
|
16
16
|
naiveAcceptance,
|
|
17
|
-
requireAcceptance,
|
|
18
|
-
requireAcceptanceKept,
|
|
19
17
|
seamBuild,
|
|
20
18
|
type SeamOutcome,
|
|
21
19
|
} from "./seam.js"
|
|
@@ -108,71 +106,24 @@ describe("naivePass", () => {
|
|
|
108
106
|
})
|
|
109
107
|
})
|
|
110
108
|
|
|
111
|
-
describe("naiveAcceptance
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const file = (path: string, before: string | undefined, after: string): Change => ({
|
|
115
|
-
path,
|
|
116
|
-
status: before === undefined ? "added" : "modified",
|
|
117
|
-
before,
|
|
118
|
-
after,
|
|
119
|
-
})
|
|
120
|
-
const install = (since: Change[] = []) =>
|
|
121
|
-
installContext({
|
|
122
|
-
...fixtureContext({}),
|
|
123
|
-
matches: globMatches,
|
|
124
|
-
read: (path: string) => (path === ".gtd/REQUIREMENTS.md" ? requirements : undefined),
|
|
125
|
-
changesSince: () => since,
|
|
126
|
-
changes: () => since,
|
|
127
|
-
refuse: (message: string) => {
|
|
128
|
-
throw new Error(message)
|
|
129
|
-
},
|
|
130
|
-
} as unknown as FlowContext)
|
|
131
|
-
|
|
132
|
-
it("takes the IDs the pass's tests name on an added line, and only those test files", () => {
|
|
133
|
-
install()
|
|
109
|
+
describe("naiveAcceptance", () => {
|
|
110
|
+
it("carries every test file the pass added or changed, named or not", () => {
|
|
111
|
+
installContext({ ...fixtureContext({}), matches: globMatches } as unknown as FlowContext)
|
|
134
112
|
expect(
|
|
135
113
|
naiveAcceptance(
|
|
136
114
|
tip(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
115
|
+
change("src/a.test.ts", "modified"),
|
|
116
|
+
change("tests/b.feature", "added"),
|
|
117
|
+
change("src/gone.test.ts", "deleted"),
|
|
118
|
+
change("src/calc.ts", "modified"),
|
|
119
|
+
change(".gtd/x.test.ts", "added"),
|
|
140
120
|
),
|
|
141
121
|
),
|
|
142
|
-
).toEqual({ hash: "abc", tests: ["src/a.test.ts"
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it("refuses a rebuild whose tests leave out an ID, naming the naive tests to carry over", () => {
|
|
146
|
-
install([file("src/calc.test.ts", undefined, 'it("[AC-1] adds")')])
|
|
147
|
-
expect(() =>
|
|
148
|
-
requireAcceptance({ hash: "abc", tests: ["src/a.test.ts"], ids: ["AC-1", "AC-2"] }, "c0"),
|
|
149
|
-
).toThrow(
|
|
150
|
-
/^gtd land: rebuild-acceptance: .*leave out AC-2 — .*git show abc -- 'src\/a\.test\.ts'/,
|
|
151
|
-
)
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it("refuses a fix turn that itself drops a carried ID, never one that leaves it alone", () => {
|
|
155
|
-
const acceptance = { hash: "abc", tests: [], ids: ["AC-1"] }
|
|
156
|
-
install([file("src/calc.test.ts", 'it("[AC-1] adds")', 'it("adds")')])
|
|
157
|
-
expect(() => requireAcceptanceKept(acceptance)).toThrow(/drops AC-1/)
|
|
158
|
-
install([file("src/calc.ts", "a", "b")])
|
|
159
|
-
expect(() => requireAcceptanceKept(acceptance)).not.toThrow()
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it("passes a rebuild whose tests name every ID, wherever they live", () => {
|
|
163
|
-
install([
|
|
164
|
-
file("tests/x.test.ts", undefined, 'it("[AC-1] adds")'),
|
|
165
|
-
file("tests/y.feature", "", "Scenario: [AC-2] subtracts"),
|
|
166
|
-
])
|
|
167
|
-
expect(() =>
|
|
168
|
-
requireAcceptance({ hash: "abc", tests: ["src/a.test.ts"], ids: ["AC-1", "AC-2"] }, "c0"),
|
|
169
|
-
).not.toThrow()
|
|
122
|
+
).toEqual({ hash: "abc", tests: ["src/a.test.ts", "tests/b.feature"] })
|
|
170
123
|
})
|
|
171
124
|
})
|
|
172
125
|
|
|
173
126
|
describe("acceptanceFault", () => {
|
|
174
|
-
const requirements =
|
|
175
|
-
"# R\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: adds.\n- AC-2: subtracts.\n"
|
|
176
127
|
const file = (path: string, status: Change["status"], after: string): Change => ({
|
|
177
128
|
path,
|
|
178
129
|
status,
|
|
@@ -180,30 +131,10 @@ describe("acceptanceFault", () => {
|
|
|
180
131
|
after,
|
|
181
132
|
})
|
|
182
133
|
const refusal = (...changes: Change[]): string | undefined => {
|
|
183
|
-
installContext({
|
|
184
|
-
|
|
185
|
-
matches: globMatches,
|
|
186
|
-
read: (path: string) => (path === ".gtd/REQUIREMENTS.md" ? requirements : undefined),
|
|
187
|
-
} as unknown as FlowContext)
|
|
188
|
-
return acceptanceFault(".gtd/REQUIREMENTS.md", tip(...changes))
|
|
134
|
+
installContext({ ...fixtureContext({}), matches: globMatches } as unknown as FlowContext)
|
|
135
|
+
return acceptanceFault(tip(...changes))
|
|
189
136
|
}
|
|
190
|
-
const tagged = 'import { add } from "../src/calc.js"\nit("
|
|
191
|
-
|
|
192
|
-
it("passes when every ID is named in a test the pass wrote", () => {
|
|
193
|
-
expect(
|
|
194
|
-
refusal(file("src/calc.ts", "modified", "x"), file("tests/calc.test.ts", "added", tagged)),
|
|
195
|
-
).toBe(undefined)
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it("names every ID no test of the pass mentions", () => {
|
|
199
|
-
const message = refusal(file("tests/calc.test.ts", "added", 'it("[AC-12] x")'))
|
|
200
|
-
expect(message).toMatch(/^acceptance-tests: /)
|
|
201
|
-
expect(message).toContain("AC-1, AC-2")
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
it("does not count an ID in a non-test file", () => {
|
|
205
|
-
expect(refusal(file("src/calc.ts", "modified", "AC-1 AC-2"))).toContain("AC-1, AC-2")
|
|
206
|
-
})
|
|
137
|
+
const tagged = 'import { add } from "../src/calc.js"\nit("adds")'
|
|
207
138
|
|
|
208
139
|
it("refuses a test importing a file the pass added, resolved from the test's directory", () => {
|
|
209
140
|
const message = refusal(
|
|
@@ -224,17 +155,6 @@ describe("acceptanceFault", () => {
|
|
|
224
155
|
).toContain("acceptance-internals")
|
|
225
156
|
})
|
|
226
157
|
|
|
227
|
-
it("counts only the lines the pass added: an ID an earlier feature left does not", () => {
|
|
228
|
-
const old = 'it("[AC-1] old")\nit("[AC-2] old")'
|
|
229
|
-
const message = refusal({
|
|
230
|
-
path: "tests/calc.test.ts",
|
|
231
|
-
status: "modified",
|
|
232
|
-
before: old,
|
|
233
|
-
after: `${old}\nit("[AC-2] new")`,
|
|
234
|
-
})
|
|
235
|
-
expect(message).toContain("names AC-1 —")
|
|
236
|
-
})
|
|
237
|
-
|
|
238
158
|
it("refuses a bare . or .. import of an added index", () => {
|
|
239
159
|
expect(
|
|
240
160
|
refusal(
|
|
@@ -424,7 +344,7 @@ describe("seamBuild", () => {
|
|
|
424
344
|
reference: "abc",
|
|
425
345
|
base: "after-refactor",
|
|
426
346
|
refactored: true,
|
|
427
|
-
rebuild: { note: "Seam: a shared op", acceptance: { hash: "abc", tests: []
|
|
347
|
+
rebuild: { note: "Seam: a shared op", acceptance: { hash: "abc", tests: [] } },
|
|
428
348
|
})
|
|
429
349
|
expect(h.names.indexOf("seam.note")).toBe(h.names.indexOf("seam.judge") + 1)
|
|
430
350
|
expect(h.names.indexOf("refactor.deriving")).toBe(h.names.indexOf("seam.note") + 1)
|
|
@@ -440,7 +360,7 @@ describe("seamBuild", () => {
|
|
|
440
360
|
expect(h.outcome).toEqual({
|
|
441
361
|
reference: "abc",
|
|
442
362
|
base: "c-base",
|
|
443
|
-
rebuild: { acceptance: { hash: "abc", tests: []
|
|
363
|
+
rebuild: { acceptance: { hash: "abc", tests: ["src/a.test.ts"] } },
|
|
444
364
|
})
|
|
445
365
|
expect(h.names).not.toContain("seam.by-hand")
|
|
446
366
|
expect(h.names.some((n) => n.startsWith("refactor"))).toBe(false)
|
|
@@ -466,7 +386,7 @@ describe("seamBuild", () => {
|
|
|
466
386
|
expect(h.outcome).toEqual({
|
|
467
387
|
reference: "abc",
|
|
468
388
|
base: "c-base",
|
|
469
|
-
rebuild: { acceptance: { hash: "abc", tests: []
|
|
389
|
+
rebuild: { acceptance: { hash: "abc", tests: [] } },
|
|
470
390
|
})
|
|
471
391
|
})
|
|
472
392
|
})
|
|
@@ -543,19 +463,17 @@ describe("implement", () => {
|
|
|
543
463
|
reference: "abc",
|
|
544
464
|
rebuild: {
|
|
545
465
|
note: "# Note\n\nSeam: a shared op",
|
|
546
|
-
acceptance: { hash: "abc", tests: ["src/a.test.ts"]
|
|
466
|
+
acceptance: { hash: "abc", tests: ["src/a.test.ts"] },
|
|
547
467
|
},
|
|
548
|
-
|
|
549
|
-
}).catch(() => undefined),
|
|
468
|
+
}),
|
|
550
469
|
)
|
|
551
470
|
expect(prompt).toContain(" > Seam: a shared op")
|
|
552
|
-
expect(prompt).toContain("acceptance tests for AC-1")
|
|
553
471
|
expect(prompt).toContain("git show abc -- 'src/a.test.ts'")
|
|
554
472
|
})
|
|
555
473
|
|
|
556
|
-
it("a rebuild whose naive
|
|
474
|
+
it("a rebuild whose naive pass wrote no test asks to carry nothing over", async () => {
|
|
557
475
|
const prompt = await building("abc", undefined, undefined, () =>
|
|
558
|
-
implement({ reference: "abc", rebuild: { acceptance: { hash: "abc", tests: []
|
|
476
|
+
implement({ reference: "abc", rebuild: { acceptance: { hash: "abc", tests: [] } } }),
|
|
559
477
|
)
|
|
560
478
|
expect(prompt).not.toContain("Carry over")
|
|
561
479
|
expect(prompt).not.toContain("refactor note")
|