@pmelab/gtd 49.0.0 → 51.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.
Files changed (60) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +16 -3
  3. package/dist/chunks/{reserved-CYZ1bSBy.mjs → reserved-CUkk9XgL.mjs} +151 -5
  4. package/dist/chunks/{review-BewyJflS.mjs → review-BaCXlIT2.mjs} +84 -20
  5. package/dist/chunks/{steering-BXwjmocB.mjs → steering-DtUVUz9l.mjs} +44 -14
  6. package/dist/chunks/{tail-C1Oi2YEy.mjs → tail-CX6Pdsql.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +22 -5
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +1 -1
  10. package/dist/web/main.js +27 -3
  11. package/dist/workflow-kit.mjs +1 -1
  12. package/dist/workflows/cleanup.mjs +2 -2
  13. package/dist/workflows/feature.mjs +144 -11
  14. package/dist/workflows/fix.mjs +3 -3
  15. package/dist/workflows/review.mjs +3 -3
  16. package/dist/workflows/rollout.mjs +1 -1
  17. package/dist/workflows.mjs +415 -42
  18. package/package.json +1 -1
  19. package/src/steering/index.ts +1 -0
  20. package/src/steering/review.test.ts +60 -2
  21. package/src/steering/review.ts +66 -9
  22. package/src/workflows/feature/coverage.feature +1 -0
  23. package/src/workflows/feature/fast-path.feature +1 -0
  24. package/src/workflows/feature/feature.ts +4 -1
  25. package/src/workflows/feature/flag-mode.feature +1 -1
  26. package/src/workflows/feature/lens-models.feature +1 -0
  27. package/src/workflows/feature/lens-review.feature +17 -4
  28. package/src/workflows/feature/one-step-land.feature +1 -0
  29. package/src/workflows/feature/prototype-rounds.feature +152 -0
  30. package/src/workflows/feature/prototype.feature +419 -0
  31. package/src/workflows/feature/prototype.test.ts +43 -0
  32. package/src/workflows/feature/prototype.ts +181 -0
  33. package/src/workflows/feature/refactor-absorbs.feature +9 -1
  34. package/src/workflows/feature/refactor-first.feature +13 -3
  35. package/src/workflows/feature/refactor-rebuild.feature +29 -5
  36. package/src/workflows/feature/refactor-review.feature +111 -13
  37. package/src/workflows/feature/refactor.test.ts +14 -4
  38. package/src/workflows/feature/refactor.ts +2 -0
  39. package/src/workflows/feature/review-answers.feature +9 -1
  40. package/src/workflows/feature/review-gate.feature +21 -3
  41. package/src/workflows/feature/reviewer-walkthrough.feature +15 -3
  42. package/src/workflows/feature/seam-rejection.feature +1 -0
  43. package/src/workflows/feature/seam.feature +1 -0
  44. package/src/workflows/feature/seam.test.ts +3 -2
  45. package/src/workflows/feature/seam.ts +19 -5
  46. package/src/workflows/feature/severity-budget.feature +112 -0
  47. package/src/workflows/feature/severity-sorted-review.feature +198 -0
  48. package/src/workflows/feature/slice-findings.feature +15 -2
  49. package/src/workflows/feature/slicing-cap.feature +9 -0
  50. package/src/workflows/feature/slicing.feature +1 -0
  51. package/src/workflows/feature/standards.feature +5 -0
  52. package/src/workflows/shared/options.ts +1 -0
  53. package/src/workflows/shared/prose.ts +6 -0
  54. package/src/workflows/shared/review.ts +124 -18
  55. package/src/workflows/shared/skills.test.ts +4 -0
  56. package/src/workflows/shared/skills.ts +5 -0
  57. package/src/workflows/shared/steps.ts +1 -0
  58. package/src/workflows/shared/text.test.ts +90 -0
  59. package/src/workflows/shared/text.ts +181 -4
  60. package/src/workflows/shared/vars.ts +3 -0
@@ -0,0 +1,419 @@
1
+ @inmem
2
+ Feature: every criterion is tested by hand before the quality lap
3
+
4
+ Once coverage is judged, implement.prototype.testing uses the slice the way
5
+ a person would and writes .gtd/PROTOTYPE.md, one chunk per criterion headed
6
+ `## AC-<n>: pass|fail|blocked`, plus one per problem found beside them. A
7
+ failure goes to implement.prototype.fix.fixing and is tested again; only a clean
8
+ report closes the slice. Past `prototypeRounds` fix rounds, or with nothing a
9
+ fixer can take, a person decides at implement.prototype.open.
10
+
11
+ Background:
12
+ Given the shipped workflows, as gtd install scaffolds them
13
+ And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
14
+ And an environment variable "GTD_FLAGMODE" set to "false"
15
+ And a file "src/calc.ts" with:
16
+ """
17
+ export const add = (a: number, b: number) => a + b
18
+ """
19
+ And the working tree is committed as "feat: add calculator"
20
+ And a file "TODO.md" with:
21
+ """
22
+ # Add subtract
23
+
24
+ subtract(a, b) returns the difference.
25
+ """
26
+ And step "unwind" changes nothing
27
+ And step "start-gate.check" changes nothing
28
+ And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
29
+ """
30
+ # Requirements
31
+
32
+ ## Acceptance Criteria
33
+
34
+ - AC-1: subtract(a, b) returns a - b.
35
+ - AC-2: add is unchanged.
36
+ """
37
+ And step "design.gate.answer" changes nothing
38
+ And step "seam.naive" writes "src/calc.ts" with:
39
+ """
40
+ export const add = (a: number, b: number) => a + b
41
+ export const subtract = (a: number, b: number) => a + b
42
+ """
43
+ And step "seam.naive" writes "src/calc.test.ts" with:
44
+ """
45
+ it("[AC-1] subtracts", () => expect(subtract(3, 1)).toBe(2))
46
+ it("[AC-2] adds", () => expect(add(1, 1)).toBe(2))
47
+ """
48
+ And step "seam.check" changes nothing
49
+ And step "implement.applying" runs its script
50
+ And step "implement.health.check" changes nothing
51
+ And judge step "implement.coverage" answers:
52
+ """
53
+ [{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
54
+ """
55
+ And step "implement.closing" runs its script
56
+
57
+ Scenario: the testing turn is told to use every criterion for real, in a disposable place
58
+ When the process is driven
59
+ Then the process rests at "implement.prototype.testing"
60
+ And the last commit subject is "gtd(judge): implement.coverage → implement.prototype.testing"
61
+ And the resting step's content contains "Running the test suite is not this"
62
+ And the resting step's content contains "against the user's real data or this working tree"
63
+ And the resting step's content contains "`## AC-<n>: pass`"
64
+
65
+ Scenario: a clean report closes the slice and moves on to the quality lap
66
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
67
+ """
68
+ # Prototype test
69
+
70
+ ## AC-1: pass
71
+
72
+ Ran `node -e 'console.log(subtract(3, 1))'` in a scratch directory; printed 2.
73
+
74
+ ## AC-2: pass
75
+
76
+ Ran `node -e 'console.log(add(1, 1))'`; printed 2.
77
+ """
78
+ When the process is driven
79
+ Then the process rests at "build.quality.correctness.reviewing"
80
+ And the commit subjects end with:
81
+ """
82
+ gtd(judge): implement.coverage → implement.prototype.testing
83
+ gtd(agent): implement.prototype.testing → implement.closing
84
+ gtd(check): implement.closing → build.quality.correctness.reviewing
85
+ """
86
+ And ".gtd/PROTOTYPE.md" does not exist
87
+
88
+ Scenario: a failed criterion is fixed and re-tested before the quality lap
89
+ Given step "implement.prototype.testing", when prompted with "Find how this project", writes ".gtd/PROTOTYPE.md" with:
90
+ """
91
+ # Prototype test
92
+
93
+ ## AC-1: fail
94
+
95
+ Ran `subtract(3, 1)` in a scratch directory; printed 4. Expected 2.
96
+
97
+ ## AC-2: pass
98
+
99
+ Ran `add(1, 1)`; printed 2.
100
+ """
101
+ And step "implement.prototype.fix.fixing" writes "src/calc.ts" with:
102
+ """
103
+ export const add = (a: number, b: number) => a + b
104
+ export const subtract = (a: number, b: number) => a - b
105
+ """
106
+ And step "implement.prototype.fix.fixing" is prompted with "Read `.gtd/PROTOTYPE.md`: a hands-on test"
107
+ And step "implement.prototype.health.check" changes nothing
108
+ And step "implement.prototype.testing", when prompted with "This is a re-test", writes ".gtd/PROTOTYPE.md" with:
109
+ """
110
+ # Prototype test
111
+
112
+ ## AC-1: pass
113
+
114
+ Re-tested: `subtract(3, 1)` printed 2.
115
+
116
+ ## AC-2: pass
117
+
118
+ Re-ran `add(1, 1)`; printed 2.
119
+ """
120
+ When the process is driven
121
+ Then the process rests at "build.quality.correctness.reviewing"
122
+ And the commit subjects end with:
123
+ """
124
+ gtd(judge): implement.coverage → implement.prototype.testing
125
+ gtd(agent): implement.prototype.testing → implement.prototype.fix.fixing
126
+ gtd(agent): implement.prototype.fix.fixing → implement.prototype.health.check
127
+ gtd(check): implement.prototype.health.check → implement.prototype.testing
128
+ gtd(agent): implement.prototype.testing → implement.closing
129
+ gtd(check): implement.closing → build.quality.correctness.reviewing
130
+ """
131
+
132
+ Scenario: a problem outside any criterion is fixed too
133
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
134
+ """
135
+ ## AC-1: pass
136
+
137
+ Printed 2.
138
+
139
+ ## AC-2: pass
140
+
141
+ Printed 2.
142
+
143
+ ## Problem a warning on every call: fail
144
+
145
+ Every call printed a deprecation warning.
146
+ """
147
+ When the process is driven
148
+ Then the process rests at "implement.prototype.fix.fixing"
149
+
150
+ Scenario: a blocked criterion goes to a person, not a fixer
151
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
152
+ """
153
+ ## AC-1: blocked
154
+
155
+ The criterion expects a - b, but the sketch asked for the absolute difference.
156
+
157
+ ## AC-2: pass
158
+
159
+ Printed 2.
160
+ """
161
+ When the process is driven
162
+ Then the process rests at "implement.prototype.open"
163
+ And the resting step's content contains "- AC-1: blocked"
164
+
165
+ Scenario: overruling the open items goes on to the quality lap
166
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
167
+ """
168
+ ## AC-1: blocked
169
+
170
+ Could not tell.
171
+
172
+ ## AC-2: pass
173
+
174
+ Printed 2.
175
+ """
176
+ And step "implement.prototype.open" changes nothing
177
+ When the process is driven
178
+ Then the process rests at "build.quality.correctness.reviewing"
179
+ And the commit subjects end with:
180
+ """
181
+ gtd(agent): implement.prototype.testing → implement.prototype.open
182
+ gtd(human): implement.prototype.open → implement.closing
183
+ gtd(check): implement.closing → build.quality.correctness.reviewing
184
+ """
185
+
186
+ Scenario: with no testing instructions the step rests at a gate saying so
187
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
188
+ """
189
+ ## No testing instructions
190
+
191
+ No project skill, no manual testing section, no recognised project shape.
192
+ """
193
+ When the process is driven
194
+ Then the process rests at "implement.prototype.unguided"
195
+ And the resting step's content contains "no way to exercise this project for real"
196
+
197
+ Scenario: a report missing a criterion is refused
198
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
199
+ """
200
+ ## AC-1: pass
201
+
202
+ Printed 2.
203
+ """
204
+ Then driving the process is refused with "exactly one chunk headed `## <ID>: pass|fail|blocked` for AC-2"
205
+
206
+ Scenario: a testing turn that changes the working tree is refused
207
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
208
+ """
209
+ ## AC-1: pass
210
+
211
+ Printed 2.
212
+
213
+ ## AC-2: pass
214
+
215
+ Printed 2.
216
+ """
217
+ And step "implement.prototype.testing" writes "scratch.txt" with:
218
+ """
219
+ left behind
220
+ """
221
+ Then driving the process is refused with "change nothing but .gtd/PROTOTYPE.md"
222
+
223
+ Scenario: a fix turn may not change the acceptance criteria
224
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
225
+ """
226
+ ## AC-1: fail
227
+
228
+ Printed 4.
229
+
230
+ ## AC-2: pass
231
+
232
+ Printed 2.
233
+ """
234
+ And step "implement.prototype.fix.fixing" writes ".gtd/REQUIREMENTS.md" with:
235
+ """
236
+ # Requirements
237
+
238
+ ## Acceptance Criteria
239
+
240
+ - AC-2: add is unchanged.
241
+ """
242
+ Then driving the process is refused with "criteria-frozen"
243
+
244
+ Scenario: a fix turn may not edit the report it fixes from
245
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
246
+ """
247
+ ## AC-1: fail
248
+
249
+ Printed 4.
250
+
251
+ ## AC-2: pass
252
+
253
+ Printed 2.
254
+ """
255
+ And step "implement.prototype.fix.fixing" writes "src/calc.ts" with:
256
+ """
257
+ export const add = (a: number, b: number) => a + b
258
+ export const subtract = (a: number, b: number) => a - b
259
+ """
260
+ And step "implement.prototype.fix.fixing" writes ".gtd/PROTOTYPE.md" with:
261
+ """
262
+ ## AC-1: pass
263
+
264
+ Fixed.
265
+
266
+ ## AC-2: pass
267
+
268
+ Printed 2.
269
+ """
270
+ Then driving the process is refused with "prototype-fix: leave .gtd/PROTOTYPE.md as it is"
271
+
272
+ Scenario: a chunk naming no criterion of the change is still an open item
273
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
274
+ """
275
+ ## AC-1: pass
276
+
277
+ Printed 2.
278
+
279
+ ## AC-2: pass
280
+
281
+ Printed 2.
282
+
283
+ ## AC-9: fail
284
+
285
+ No such command yet.
286
+ """
287
+ When the process is driven
288
+ Then the process rests at "implement.prototype.fix.fixing"
289
+
290
+ Scenario: instructions added at the unguided gate run the suite, then a fresh test
291
+ Given step "implement.prototype.testing", when prompted with "Find how this project", writes ".gtd/PROTOTYPE.md" with:
292
+ """
293
+ ## No testing instructions
294
+
295
+ Nothing found.
296
+ """
297
+ And step "implement.prototype.unguided" writes "AGENTS.md" with:
298
+ """
299
+ ## Manual testing
300
+
301
+ Run `node -e` against the calculator in a scratch directory.
302
+ """
303
+ And step "implement.prototype.health.check" changes nothing
304
+ And step "implement.prototype.testing" is prompted with "Find how this project"
305
+ When the process is driven
306
+ # The fresh test still finds no way in; the gate's second visit adds nothing new and goes on.
307
+ Then the process rests at "build.quality.correctness.reviewing"
308
+ And the commit subjects end with:
309
+ """
310
+ gtd(agent): implement.prototype.testing → implement.prototype.unguided
311
+ gtd(human): implement.prototype.unguided → implement.prototype.health.check
312
+ gtd(check): implement.prototype.health.check → implement.prototype.testing
313
+ gtd(agent): implement.prototype.testing → implement.prototype.unguided
314
+ gtd(human): implement.prototype.unguided → implement.closing
315
+ gtd(check): implement.closing → build.quality.correctness.reviewing
316
+ """
317
+
318
+ Scenario: a clean land at the unguided gate goes on untested
319
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
320
+ """
321
+ ## No testing instructions
322
+
323
+ Nothing found.
324
+ """
325
+ And step "implement.prototype.unguided" changes nothing
326
+ When the process is driven
327
+ Then the process rests at "build.quality.correctness.reviewing"
328
+ And the commit subjects end with:
329
+ """
330
+ gtd(agent): implement.prototype.testing → implement.prototype.unguided
331
+ gtd(human): implement.prototype.unguided → implement.closing
332
+ gtd(check): implement.closing → build.quality.correctness.reviewing
333
+ """
334
+
335
+ Scenario: a no-instructions chunk beside tested criteria is read as an ordinary report
336
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
337
+ """
338
+ ## AC-1: fail
339
+
340
+ Printed 4.
341
+
342
+ ## AC-2: pass
343
+
344
+ Printed 2.
345
+
346
+ ## No testing instructions
347
+
348
+ Found none for the edge cases; used the CLI.
349
+ """
350
+ When the process is driven
351
+ Then the process rests at "implement.prototype.fix.fixing"
352
+
353
+ Scenario: a fix turn that finds nothing to fix goes on to the re-test
354
+ Given step "implement.prototype.testing", when prompted with "Find how this project", writes ".gtd/PROTOTYPE.md" with:
355
+ """
356
+ ## AC-1: fail
357
+
358
+ Printed 4, I think.
359
+
360
+ ## AC-2: pass
361
+
362
+ Printed 2.
363
+ """
364
+ And step "implement.prototype.fix.fixing" changes nothing
365
+ And step "implement.prototype.health.check" changes nothing
366
+ And step "implement.prototype.testing", when prompted with "This is a re-test", writes ".gtd/PROTOTYPE.md" with:
367
+ """
368
+ ## AC-1: pass
369
+
370
+ Looked again: printed 2.
371
+
372
+ ## AC-2: pass
373
+
374
+ Printed 2.
375
+ """
376
+ When the process is driven
377
+ Then the process rests at "build.quality.correctness.reviewing"
378
+
379
+ Scenario: a problem heading that misses the shape is refused, not dropped
380
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
381
+ """
382
+ ## AC-1: pass
383
+
384
+ Printed 2.
385
+
386
+ ## AC-2: pass
387
+
388
+ Printed 2.
389
+
390
+ ## Problem every call warns — fail
391
+
392
+ Each call printed a deprecation warning.
393
+ """
394
+ Then driving the process is refused with "these miss it:"
395
+
396
+ Scenario: a re-test that finds no way to test is refused, keeping the open items in view
397
+ Given step "implement.prototype.testing", when prompted with "Find how this project", writes ".gtd/PROTOTYPE.md" with:
398
+ """
399
+ ## AC-1: fail
400
+
401
+ Printed 4.
402
+
403
+ ## AC-2: pass
404
+
405
+ Printed 2.
406
+ """
407
+ And step "implement.prototype.fix.fixing" writes "src/calc.ts" with:
408
+ """
409
+ export const add = (a: number, b: number) => a + b
410
+ export const subtract = (a: number, b: number) => a - b
411
+ """
412
+ And step "implement.prototype.health.check" changes nothing
413
+ And step "implement.prototype.testing", when prompted with "This is a re-test", writes ".gtd/PROTOTYPE.md" with:
414
+ """
415
+ ## No testing instructions
416
+
417
+ The CLI is gone.
418
+ """
419
+ Then driving the process is refused with "needs exactly one chunk"
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { reportOf } from "./prototype.js"
3
+
4
+ describe("reportOf", () => {
5
+ it("reads each criterion and problem chunk with its verdict, any case", () => {
6
+ expect(
7
+ reportOf(
8
+ "# Prototype test\n\n## AC-1: pass\n\nok\n\n## AC-2 : FAIL\n\n## Problem double replies: blocked\n",
9
+ ),
10
+ ).toEqual({
11
+ unguided: false,
12
+ items: [
13
+ { subject: "AC-1", verdict: "pass" },
14
+ { subject: "AC-2", verdict: "fail" },
15
+ { subject: "Problem double replies", verdict: "blocked" },
16
+ ],
17
+ malformed: [],
18
+ })
19
+ })
20
+
21
+ it("skips fenced lines, deeper headings and prose headings", () => {
22
+ expect(reportOf("```md\n## AC-1: fail\n```\n### AC-2: fail\n## Notes\n")).toEqual({
23
+ unguided: false,
24
+ items: [],
25
+ malformed: [],
26
+ })
27
+ })
28
+
29
+ it("lists an item-like heading that misses the shape", () => {
30
+ expect(
31
+ reportOf("## Problem crash — fail\n## AC-2: failed\n## Problems: fail\n## AC-3: maybe\n")
32
+ .malformed,
33
+ ).toEqual(["Problem crash — fail", "AC-2: failed", "Problems: fail", "AC-3: maybe"])
34
+ })
35
+
36
+ it("upper-cases a criterion ID written in lower case", () => {
37
+ expect(reportOf("## ac-1: pass\n").items).toEqual([{ subject: "AC-1", verdict: "pass" }])
38
+ })
39
+
40
+ it("reads the no-instructions chunk", () => {
41
+ expect(reportOf("## No testing instructions\n\nNo skill found.\n").unguided).toBe(true)
42
+ })
43
+ })
@@ -0,0 +1,181 @@
1
+ import { changes, human, read, refuse, scope, vars } from "../../flows/index.js"
2
+ import { HEADING, markFenced } from "../shared/fences.js"
3
+ import { coder, PROTOTYPE, REQUIREMENTS } from "../shared/steps.js"
4
+ import * as t from "../shared/text.js"
5
+ import { defaults } from "../shared/vars.js"
6
+ import type { Guard } from "../shared/health.js"
7
+ import { green } from "./coverage.js"
8
+ import { criterionIds, requireCriteriaFrozen, sliceIds, type CriteriaSlice } from "./criteria.js"
9
+
10
+ /** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
11
+ export const prototypeRounds = (): number => {
12
+ const value = vars.prototypeRounds ?? defaults.prototypeRounds!
13
+ if (!/^(0|[1-9]\d*)$/.test(value))
14
+ refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`)
15
+ return Number(value)
16
+ }
17
+
18
+ export type Verdict = "pass" | "fail" | "blocked"
19
+
20
+ export interface Item {
21
+ /** A criterion ID, or `Problem <title>`. */
22
+ readonly subject: string
23
+ readonly verdict: Verdict
24
+ }
25
+
26
+ export interface Report {
27
+ readonly unguided: boolean
28
+ readonly items: readonly Item[]
29
+ /** Headings that read as an item but miss the shape: a failure there would go unseen. */
30
+ readonly malformed: readonly string[]
31
+ }
32
+
33
+ const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i
34
+ const UNGUIDED = /^no testing instructions$/i
35
+
36
+ /** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
37
+ export const reportOf = (text: string): Report => {
38
+ let unguided = false
39
+ const items: Item[] = []
40
+ const malformed: string[] = []
41
+ for (const { line, fenced } of markFenced(text)) {
42
+ const heading = fenced ? null : HEADING.exec(line)
43
+ if (heading?.[1] !== "##") continue
44
+ if (UNGUIDED.test(heading[2]!)) unguided = true
45
+ const chunk = CHUNK.exec(heading[2]!)
46
+ if (chunk === null && /^(ac-\d|problem)/i.test(heading[2]!)) malformed.push(heading[2]!)
47
+ if (chunk !== null)
48
+ items.push({
49
+ subject: /^ac-/i.test(chunk[1]!) ? chunk[1]!.toUpperCase() : chunk[1]!,
50
+ verdict: chunk[2]!.toLowerCase() as Verdict,
51
+ })
52
+ }
53
+ return { unguided, items, malformed }
54
+ }
55
+
56
+ /**
57
+ * The report the testing turn left, refused unless it is the only change and
58
+ * names every criterion once, with its open items: another slice's criteria
59
+ * are not this slice's to fix.
60
+ */
61
+ const requireReport = (
62
+ slice: CriteriaSlice | undefined,
63
+ retest: boolean,
64
+ ): { readonly unguided: boolean; readonly open: readonly Item[] } => {
65
+ const others = changes()
66
+ .map((c) => c.path)
67
+ .filter((path) => path !== PROTOTYPE)
68
+ if (others.length > 0)
69
+ refuse(
70
+ `gtd land: prototype-report: test in a disposable place and change nothing but ${PROTOTYPE} — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}`,
71
+ )
72
+ const text = read(PROTOTYPE)
73
+ if (text === undefined) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`)
74
+ const report = reportOf(text)
75
+ if (report.malformed.length > 0)
76
+ refuse(
77
+ `gtd land: prototype-report: head each chunk \`## <ID>: pass|fail|blocked\` or \`## Problem <title>: pass|fail|blocked\` — these miss it:\n${report.malformed.map((h) => ` - ${h}`).join("\n")}`,
78
+ )
79
+ // Only a first test with nothing else in it: a chunk beside it was tested after all, and a
80
+ // re-test's open items must reach a person, not slip past as untested.
81
+ if (report.unguided && report.items.length === 0 && !retest) return { unguided: true, open: [] }
82
+ const requirements = read(REQUIREMENTS) ?? ""
83
+ const ids = criterionIds(requirements, slice)
84
+ const elsewhere = criterionIds(requirements).filter((id) => !ids.includes(id))
85
+ const count = (id: string): number => report.items.filter((i) => i.subject === id).length
86
+ const wrong = ids.filter((id) => count(id) !== 1)
87
+ if (wrong.length > 0)
88
+ refuse(
89
+ `gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`,
90
+ )
91
+ // A mistyped ID stays open: only another slice's criteria are not this one's to fix.
92
+ const open = report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
93
+ return { unguided: false, open }
94
+ }
95
+
96
+ const frozen = (): void => requireCriteriaFrozen(REQUIREMENTS)
97
+
98
+ const testing = async (
99
+ slice: CriteriaSlice | undefined,
100
+ retest: boolean,
101
+ ): Promise<ReturnType<typeof requireReport>> => {
102
+ await t.agentWithSkills("testing", t.prototypeTestingPrompt(sliceIds(slice), retest), {
103
+ label: retest ? "Re-testing the slice by hand" : "Testing the slice by hand",
104
+ file: PROTOTYPE,
105
+ model: coder(),
106
+ system: t.testerSystem(),
107
+ // A test that saw exactly what the last round saw leaves the report as it was.
108
+ allowEmpty: true,
109
+ })
110
+ return requireReport(slice, retest)
111
+ }
112
+
113
+ const fixing = async (slice: CriteriaSlice | undefined, guard?: Guard): Promise<void> => {
114
+ await scope("fix", () =>
115
+ t.agentWithSkills("fixing", t.prototypeFixingPrompt(sliceIds(slice)), {
116
+ label: "Fixing what the hands-on test found",
117
+ file: PROTOTYPE,
118
+ model: coder(),
119
+ system: t.builderSystem(),
120
+ // A `fail` the tester got wrong has nothing to fix: the re-test says so, or the cap ends it.
121
+ allowEmpty: true,
122
+ }),
123
+ )
124
+ // The re-test may land empty: a report the fixer edited would pass untested.
125
+ if (changes().some((c) => c.path === PROTOTYPE))
126
+ refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`)
127
+ frozen()
128
+ guard?.()
129
+ }
130
+
131
+ /** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
132
+ const askPerson = async (name: string, message: string, label: string): Promise<boolean> => {
133
+ await human(name, { message, label, file: PROTOTYPE, acceptClean: true })
134
+ // A note in the report alone tests nothing new.
135
+ if (!changes().some((c) => c.path !== PROTOTYPE)) return false
136
+ frozen()
137
+ return true
138
+ }
139
+
140
+ /**
141
+ * Use the slice by hand: a testing turn reports on every criterion, a fix turn
142
+ * takes each failure, and the slice is tested again — until the report is
143
+ * clean, or `prototypeRounds` fixes are spent and a person decides. `guard`
144
+ * holds every fix turn, as it holds the implementation's.
145
+ */
146
+ export const prototypeTest = async (
147
+ slice: CriteriaSlice | undefined,
148
+ guard?: Guard,
149
+ ): Promise<void> => {
150
+ const cap = prototypeRounds()
151
+ if (cap === 0) return
152
+ await scope("prototype", async () => {
153
+ let fixes = 0
154
+ let retest = false
155
+ for (;;) {
156
+ const { unguided, open } = await testing(slice, retest)
157
+ retest = !unguided
158
+ if (unguided) {
159
+ const instructed = await askPerson(
160
+ "unguided",
161
+ t.prototypeUnguidedMessage(),
162
+ "No way to test by hand",
163
+ )
164
+ if (!instructed) return
165
+ } else if (open.length === 0) return
166
+ else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
167
+ fixes++
168
+ await fixing(slice, guard)
169
+ } else {
170
+ const changed = await askPerson(
171
+ "open",
172
+ t.prototypeOpenMessage(open, fixes),
173
+ "Hands-on test still failing",
174
+ )
175
+ if (!changed) return
176
+ fixes = 0
177
+ }
178
+ await green(guard)
179
+ }
180
+ })
181
+ }
@@ -10,6 +10,7 @@ Feature: a refactor that carried the whole feature still gets a quality lap over
10
10
 
11
11
  Background:
12
12
  Given the shipped workflows, as gtd install scaffolds them
13
+ And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
13
14
  And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
14
15
  And an environment variable "GTD_REFACTORQUALITYREVIEWS" set to ""
15
16
  And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
@@ -69,7 +70,12 @@ Feature: a refactor that carried the whole feature still gets a quality lap over
69
70
  """
70
71
  And step "refactor.health.check" changes nothing
71
72
  And step "refactor.closing" runs its script
72
- And step "refactor.build.review.composing" writes ".gtd/REVIEW.md" with:
73
+ And judge step "refactor.build.review.ranking" answers:
74
+ """
75
+ []
76
+ """
77
+ And step "refactor.build.review.carrying" runs its script
78
+ And step "refactor.build.review.reviewing" writes ".gtd/REVIEW.md" with:
73
79
  """
74
80
  # Review: abc1234
75
81
 
@@ -92,6 +98,8 @@ Feature: a refactor that carried the whole feature still gets a quality lap over
92
98
 
93
99
  ## calc
94
100
 
101
+ _critical: judge unsure_
102
+
95
103
  - [x] ./src/calc.ts#1-2
96
104
  subtract next to add
97
105
  """