@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.
Files changed (31) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +56 -50
  3. package/dist/chunks/{reserved-CQ7amcKG.mjs → reserved-DHafcW7t.mjs} +47 -27
  4. package/dist/chunks/{review-CXzUXJHL.mjs → review-BJGKwlYv.mjs} +2 -2
  5. package/dist/chunks/{tail-JZ9PAOwp.mjs → tail-B9Hw2AFB.mjs} +12 -8
  6. package/dist/workflows/cleanup.mjs +2 -2
  7. package/dist/workflows/feature.mjs +41 -94
  8. package/dist/workflows/fix.mjs +2 -2
  9. package/dist/workflows/review.mjs +3 -3
  10. package/dist/workflows/rollout.mjs +1 -1
  11. package/dist/workflows.mjs +97 -126
  12. package/package.json +1 -1
  13. package/src/workflows/feature/criteria-coverage.feature +109 -0
  14. package/src/workflows/feature/feature.ts +9 -4
  15. package/src/workflows/feature/green.ts +2 -5
  16. package/src/workflows/feature/naive-acceptance.feature +31 -32
  17. package/src/workflows/feature/prototype.ts +5 -8
  18. package/src/workflows/feature/refactor-rebuild.feature +16 -12
  19. package/src/workflows/feature/seam.test.ts +19 -101
  20. package/src/workflows/feature/seam.ts +13 -89
  21. package/src/workflows/fix/fix-quality.feature +4 -2
  22. package/src/workflows/shared/access.test.ts +1 -0
  23. package/src/workflows/shared/review.test.ts +3 -1
  24. package/src/workflows/shared/skills.test.ts +3 -1
  25. package/src/workflows/shared/steps.test.ts +9 -6
  26. package/src/workflows/shared/steps.ts +12 -3
  27. package/src/workflows/shared/tail.test.ts +20 -2
  28. package/src/workflows/shared/tail.ts +20 -7
  29. package/src/workflows/shared/text.test.ts +19 -6
  30. package/src/workflows/shared/text.ts +50 -29
  31. package/src/workflows/shared/vars.ts +1 -1
@@ -1,11 +1,10 @@
1
1
  @inmem
2
- Feature: a naive pass ends only when a test names every criterion and the suite is green
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` writes behaviour-level acceptance tests, each naming its
5
- criterion's ID. A pass whose tests leave an ID out or import a file the
6
- pass added is sent back; `seam.check` then runs the suite with the kept
7
- pass applied, and a red run sends it back too, before the judge sees it.
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 whose tests leave a criterion's ID out is sent back, and an overrule runs the check and sticks
48
- Given step "seam.naive" writes "tests/calc.test.ts" with:
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
- import { add, subtract } from "../src/calc.js"
51
- it("[AC-1] subtracts", () => expect(subtract(5, 3)).toBe(2))
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 "acceptance-tests: no test of the naive pass names AC-2"
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("[AC-1] subtracts", () => expect(subtract(5, 3)).toBe(2))
110
- it("[AC-2] still adds", () => expect(add(1, 1)).toBe(2))
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 > [AC-1] subtracts
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 "[AC-1] subtracts"
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("[AC-1] subtracts", () => expect(subtract(5, 3)).toBe(2))
132
- it("[AC-2] still adds", () => expect(add(2, 2)).toBe(4))
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 (guard?: Guard): Promise<void> => {
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. `guard`
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 (guard?: Guard): Promise<void> => {
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(guard)
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(guard)
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 and lands only once it carries the naive pass's
9
- acceptance tests and the suite is green. The note reaches both turns.
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("[AC-1] subtracts", () => {})
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 for AC-1"
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 only once it carries the naive pass's acceptance tests, green
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 and requireAcceptance", () => {
112
- const requirements =
113
- "# R\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: adds.\n- AC-2: subtracts.\n"
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
- file("src/a.test.ts", 'it("[AC-2] old")', 'it("[AC-2] old")\nit("[AC-1] adds")'),
138
- file("src/b.test.ts", undefined, 'it("helper")'),
139
- file("src/calc.ts", "x", "AC-2"),
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"], ids: ["AC-1"] })
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
- ...fixtureContext({}),
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("[AC-1] adds")\nit("[AC-2] subtracts")'
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: [], ids: [] } },
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: [], ids: [] } },
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: [], ids: [] } },
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"], ids: ["AC-1"] },
466
+ acceptance: { hash: "abc", tests: ["src/a.test.ts"] },
547
467
  },
548
- // The fixture's turn carries no test, so the acceptance guard refuses it after the prompt.
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 tests named no ID asks to carry nothing over", async () => {
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: [], ids: [] } } }),
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 criterion ID of `file` no
120
- * test line of the pass names, or a test importing a file the pass added —
121
- * naive structure a reset and refactor replaces, which the tests must survive.
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 = (file: string, tip: KeptTip): string | undefined => {
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 = tests
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`: the IDs its tests name, and those test files. */
145
- export const naiveAcceptance = (tip: KeptTip): t.Acceptance => {
146
- const ids = idsOf(REQUIREMENTS)
147
- const perTest = acceptanceTests(tip.changes).map((c) => {
148
- const lines = addedLines(c)
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
- // Only the criteria the requirements still list.
299
- if (built && acceptance !== undefined)
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(REQUIREMENTS, tip)
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 six lenses in the settled order", () => {
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
  })