@pmelab/gtd 54.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-CXzUXJHL.mjs → review-BJGKwlYv.mjs} +2 -2
- 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 +97 -126
- package/package.json +1 -1
- 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/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/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/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
|
@@ -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
|
"""
|
|
@@ -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")
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
agent,
|
|
4
4
|
applyScript,
|
|
5
5
|
changes,
|
|
6
|
-
changesSince,
|
|
7
6
|
check,
|
|
8
7
|
codeChanges,
|
|
9
8
|
env,
|
|
@@ -92,43 +91,19 @@ const reaches = (imports: ReturnType<typeof importsOf>, added: string): boolean
|
|
|
92
91
|
imports.some(({ path, tail }) => name === path || (tail && name.endsWith(`/${path}`))),
|
|
93
92
|
)
|
|
94
93
|
|
|
95
|
-
const named = (id: string, text: string): boolean =>
|
|
96
|
-
new RegExp(`(?<![\\w-])${id}(?!\\d)`).test(text)
|
|
97
|
-
|
|
98
|
-
/** The lines `change` added: an ID an earlier change left in the file does not count. */
|
|
99
|
-
const addedLines = (change: Change): string => {
|
|
100
|
-
const before = new Set((change.before ?? "").split("\n"))
|
|
101
|
-
return change
|
|
102
|
-
.after!.split("\n")
|
|
103
|
-
.filter((line) => !before.has(line))
|
|
104
|
-
.join("\n")
|
|
105
|
-
}
|
|
106
|
-
|
|
107
94
|
const acceptanceTests = (changes: readonly Change[]): readonly Change[] =>
|
|
108
95
|
changes.filter((c) => c.after !== undefined && isCode(c.path) && isTestFile(c.path))
|
|
109
96
|
|
|
110
|
-
const idsOf = (file: string): string[] => criterionIds(read(file) ?? "")
|
|
111
|
-
|
|
112
|
-
// One rule for both gates, so a naive test carried over verbatim always satisfies the rebuild's.
|
|
113
|
-
const unnamed = (ids: readonly string[], tests: readonly Change[]): string[] => {
|
|
114
|
-
const lines = tests.map(addedLines)
|
|
115
|
-
return ids.filter((id) => !lines.some((text) => named(id, text)))
|
|
116
|
-
}
|
|
117
|
-
|
|
118
97
|
/**
|
|
119
|
-
* Why `tip`'s tests fall short, or `undefined`: a
|
|
120
|
-
*
|
|
121
|
-
*
|
|
98
|
+
* Why `tip`'s tests fall short, or `undefined`: a test importing a file the
|
|
99
|
+
* pass added — naive structure a reset and refactor replaces, which the tests
|
|
100
|
+
* must survive.
|
|
122
101
|
*/
|
|
123
|
-
export const acceptanceFault = (
|
|
124
|
-
const tests = acceptanceTests(tip.changes)
|
|
125
|
-
const untested = unnamed(idsOf(file), tests)
|
|
126
|
-
if (untested.length > 0)
|
|
127
|
-
return `acceptance-tests: no test of the naive pass names ${untested.join(", ")} — write a behaviour-level test for each criterion with its ID in the test or scenario name, e.g. "[${untested[0]}] …"`
|
|
102
|
+
export const acceptanceFault = (tip: KeptTip): string | undefined => {
|
|
128
103
|
const added = tip.changes
|
|
129
104
|
.filter((c) => c.status === "added" && isCode(c.path) && !isTestFile(c.path))
|
|
130
105
|
.map((c) => c.path)
|
|
131
|
-
const reaching =
|
|
106
|
+
const reaching = acceptanceTests(tip.changes)
|
|
132
107
|
.filter((test) => holdsCallSites(test.path))
|
|
133
108
|
.flatMap((test) => {
|
|
134
109
|
const imports = importsOf(test)
|
|
@@ -141,48 +116,11 @@ export const acceptanceFault = (file: string, tip: KeptTip): string | undefined
|
|
|
141
116
|
return undefined
|
|
142
117
|
}
|
|
143
118
|
|
|
144
|
-
/** What a rebuild carries over from the naive pass `tip`:
|
|
145
|
-
export const naiveAcceptance = (tip: KeptTip): t.Acceptance => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return { path: c.path, ids: ids.filter((id) => named(id, lines)) }
|
|
150
|
-
})
|
|
151
|
-
return {
|
|
152
|
-
hash: tip.hash,
|
|
153
|
-
tests: perTest.filter((c) => c.ids.length > 0).map((c) => c.path),
|
|
154
|
-
ids: ids.filter((id) => perTest.some((c) => c.ids.includes(id))),
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Refuse a rebuild whose test changes since `since` leave out an ID the naive
|
|
160
|
-
* pass's tests named. A skipped test still counts: the gate reads text, never runs it.
|
|
161
|
-
*/
|
|
162
|
-
export const requireAcceptance = (acceptance: t.Acceptance, since: string): void => {
|
|
163
|
-
const missing = unnamed(acceptance.ids, acceptanceTests(changesSince(since)))
|
|
164
|
-
if (missing.length === 0) return
|
|
165
|
-
refuse(
|
|
166
|
-
`gtd land: rebuild-acceptance: the rebuild's tests leave out ${missing.join(", ")} — carry over the naive pass's acceptance tests (\`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`), each with its ID in the test or scenario name`,
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Refuse a fix turn that, on its own, drops an ID the naive pass's tests named
|
|
172
|
-
* from a test file. Turn-local, so a person's edit at an escalation rest never
|
|
173
|
-
* wedges later fix turns.
|
|
174
|
-
*/
|
|
175
|
-
export const requireAcceptanceKept = (acceptance: t.Acceptance): void => {
|
|
176
|
-
const dropped = acceptance.ids.filter((id) =>
|
|
177
|
-
changes()
|
|
178
|
-
.filter((c) => isCode(c.path) && isTestFile(c.path))
|
|
179
|
-
.some((c) => named(id, c.before ?? "") && !named(id, c.after ?? "")),
|
|
180
|
-
)
|
|
181
|
-
if (dropped.length === 0) return
|
|
182
|
-
refuse(
|
|
183
|
-
`gtd land: rebuild-acceptance: this turn drops ${dropped.join(", ")} from the tests — the naive pass's acceptance tests stay; fix the code, not them`,
|
|
184
|
-
)
|
|
185
|
-
}
|
|
119
|
+
/** What a rebuild carries over from the naive pass `tip`: every test file it added or changed. */
|
|
120
|
+
export const naiveAcceptance = (tip: KeptTip): t.Acceptance => ({
|
|
121
|
+
hash: tip.hash,
|
|
122
|
+
tests: acceptanceTests(tip.changes).map((c) => c.path),
|
|
123
|
+
})
|
|
186
124
|
|
|
187
125
|
const restore = (path: string): string =>
|
|
188
126
|
`if git cat-file -e ${quote(`HEAD:${path}`)} 2>/dev/null; then git checkout -q HEAD -- ${quote(path)}; else rm -f -- ${quote(path)}; fi`
|
|
@@ -243,12 +181,6 @@ const applied = async (tip: string): Promise<boolean> => {
|
|
|
243
181
|
return !wrote(FEEDBACK) && codeChanges().length > 0
|
|
244
182
|
}
|
|
245
183
|
|
|
246
|
-
/** `carried` limited to the criteria `.gtd/REQUIREMENTS.md` lists now; a file listing none narrows nothing. */
|
|
247
|
-
const settled = (carried: readonly string[]): readonly string[] => {
|
|
248
|
-
const listed = idsOf(REQUIREMENTS)
|
|
249
|
-
return listed.length === 0 ? carried : carried.filter((id) => listed.includes(id))
|
|
250
|
-
}
|
|
251
|
-
|
|
252
184
|
const build = async (options: ImplementOptions): Promise<void> => {
|
|
253
185
|
await t.agentWithSkills(
|
|
254
186
|
"building",
|
|
@@ -290,19 +222,11 @@ export async function implement(
|
|
|
290
222
|
/** `implement`, resolving with the criterion IDs the requirements listed when it closed. */
|
|
291
223
|
export const implementing = (options: ImplementOptions): Promise<readonly string[]> =>
|
|
292
224
|
scope("implement", async () => {
|
|
293
|
-
const since = head()
|
|
294
|
-
const acceptance = options.rebuild?.acceptance
|
|
295
225
|
const built = options.naive === undefined || !(await applied(options.naive))
|
|
296
226
|
if (built) await build(options)
|
|
297
227
|
else requireCriteriaFrozen(REQUIREMENTS)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
requireAcceptance({ ...acceptance, ids: settled(acceptance.ids) }, since)
|
|
301
|
-
// Fix turns are held to the carried tests too: deleting a red one must not buy a green suite.
|
|
302
|
-
const keepsTests =
|
|
303
|
-
built && acceptance !== undefined ? () => requireAcceptanceKept(acceptance) : undefined
|
|
304
|
-
await green(keepsTests)
|
|
305
|
-
await prototypeTest(keepsTests)
|
|
228
|
+
await green()
|
|
229
|
+
await prototypeTest()
|
|
306
230
|
const ids = criterionIds(read(REQUIREMENTS) ?? "")
|
|
307
231
|
await run("closing", removeScript([REQUIREMENTS, REFACTOR_NOTE, PROTOTYPE]), {
|
|
308
232
|
label: "Closing out the implementation",
|
|
@@ -360,7 +284,7 @@ interface Vetting {
|
|
|
360
284
|
|
|
361
285
|
/** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
|
|
362
286
|
const vetted = async (tip: KeptTip, vetting: Vetting): Promise<Verdict | undefined> => {
|
|
363
|
-
const fault = vetting.waived ? undefined : acceptanceFault(
|
|
287
|
+
const fault = vetting.waived ? undefined : acceptanceFault(tip)
|
|
364
288
|
if (fault !== undefined) {
|
|
365
289
|
const verdict = await vetting.again({ fault, hash: tip.hash })
|
|
366
290
|
if (verdict.kind !== "overruled") return verdict
|
|
@@ -4,11 +4,12 @@ Feature: fix's quality lap is triaged, and its leftovers go to the hand-off revi
|
|
|
4
4
|
A judge sorts the lap's findings: mechanical ones are fixed for up to three
|
|
5
5
|
turns, noise is dropped, and design ones — with whatever the fix turns left —
|
|
6
6
|
are added to the reviewer's `.gtd/REVIEW.md` by gtd, never to a person's
|
|
7
|
-
gate before the review.
|
|
7
|
+
gate before the review. With no requirements to check against, the
|
|
8
|
+
`criteria-coverage` lens runs no turn.
|
|
8
9
|
|
|
9
10
|
Background:
|
|
10
11
|
Given the shipped workflows, as gtd install scaffolds them
|
|
11
|
-
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
|
|
12
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to "criteria-coverage, correctness"
|
|
12
13
|
And a file "src/calc.ts" with:
|
|
13
14
|
"""
|
|
14
15
|
export const add = (a: number, b: number) => a + b
|
|
@@ -81,6 +82,7 @@ Feature: fix's quality lap is triaged, and its leftovers go to the hand-off revi
|
|
|
81
82
|
Then the process rests at "build.review.await-review"
|
|
82
83
|
And ".gtd/QUALITY.md" does not exist
|
|
83
84
|
And no commit body contains "build.health.describe"
|
|
85
|
+
And no commit body contains "build.quality.criteria-coverage"
|
|
84
86
|
And ".gtd/REVIEW.md" contains:
|
|
85
87
|
"""
|
|
86
88
|
## sum has no negative test
|
|
@@ -19,6 +19,7 @@ describe("the bundled workflow's access export", () => {
|
|
|
19
19
|
"build.quality.test-audit": { write: [] },
|
|
20
20
|
"build.quality.conventions": { write: [] },
|
|
21
21
|
"build.quality.spec-challenge": { write: [] },
|
|
22
|
+
"build.quality.criteria-coverage": { write: [] },
|
|
22
23
|
"refactor.build.review": { write: [".gtd/REVIEW.md"] },
|
|
23
24
|
"refactor.build.quality.correctness": { write: [] },
|
|
24
25
|
"refactor.build.quality.ponytail-review": { write: [] },
|
|
@@ -575,7 +575,7 @@ describe("review verdict routing", () => {
|
|
|
575
575
|
})
|
|
576
576
|
|
|
577
577
|
describe("the default quality lenses", () => {
|
|
578
|
-
it("are the
|
|
578
|
+
it("are the seven lenses in the settled order", () => {
|
|
579
579
|
expect(bundled.defaults.qualityReviews!.split(",").map((l) => l.trim())).toEqual([
|
|
580
580
|
"correctness",
|
|
581
581
|
"owasp-security",
|
|
@@ -583,6 +583,7 @@ describe("the default quality lenses", () => {
|
|
|
583
583
|
"test-audit",
|
|
584
584
|
"conventions",
|
|
585
585
|
"spec-challenge",
|
|
586
|
+
"criteria-coverage",
|
|
586
587
|
])
|
|
587
588
|
})
|
|
588
589
|
|
|
@@ -590,6 +591,7 @@ describe("the default quality lenses", () => {
|
|
|
590
591
|
expect(Object.keys(bundled.builtInLenses).sort()).toEqual([
|
|
591
592
|
"conventions",
|
|
592
593
|
"correctness",
|
|
594
|
+
"criteria-coverage",
|
|
593
595
|
"spec-challenge",
|
|
594
596
|
])
|
|
595
597
|
})
|