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