@pmelab/gtd 51.0.1 → 52.1.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 +46 -40
- package/dist/chunks/{reserved-CUkk9XgL.mjs → reserved-DxB5CROj.mjs} +73 -38
- package/dist/chunks/{review-BaCXlIT2.mjs → review-CKMSdvgW.mjs} +11 -9
- package/dist/chunks/{steering-DtUVUz9l.mjs → steering-CLhu8C7q.mjs} +262 -52
- package/dist/chunks/{tail-CX6Pdsql.mjs → tail-QZc7Uz2U.mjs} +1 -1
- package/dist/gtd.bundle.mjs +198 -43
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +1 -1
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +510 -147
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +228 -222
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +570 -317
- package/package.json +5 -4
- package/src/steering/FORMATS.md +4 -1
- package/src/steering/SteeringFormat.ts +7 -1
- package/src/steering/contract.test.ts +2 -1
- package/src/steering/fixtures/review/sectioned.md +31 -0
- package/src/steering/fixtures/review/sectioned.read.json +119 -0
- package/src/steering/index.ts +4 -0
- package/src/steering/qa.ts +1 -1
- package/src/steering/review.test.ts +270 -4
- package/src/steering/review.ts +328 -69
- package/src/testkit/steps.ts +13 -0
- package/src/workflows/feature/acceptance-criteria.feature +159 -3
- package/src/workflows/feature/continuation-cap.feature +4 -0
- package/src/workflows/feature/continuation-rebuild.feature +4 -0
- package/src/workflows/feature/continuation-slices.feature +4 -0
- package/src/workflows/feature/continuation.feature +4 -0
- package/src/workflows/feature/criteria.test.ts +60 -1
- package/src/workflows/feature/criteria.ts +87 -8
- package/src/workflows/feature/fast-path.feature +5 -6
- package/src/workflows/feature/feature.test.ts +6 -5
- package/src/workflows/feature/feature.ts +1 -2
- package/src/workflows/feature/flag-mode.feature +5 -6
- package/src/workflows/feature/green.ts +22 -0
- package/src/workflows/feature/hands-on-skip.feature +384 -0
- package/src/workflows/feature/lens-models.feature +4 -4
- package/src/workflows/feature/lens-review.feature +10 -8
- package/src/workflows/feature/naive-acceptance.feature +4 -0
- package/src/workflows/feature/one-step-land.feature +5 -6
- package/src/workflows/feature/planning.ts +2 -1
- package/src/workflows/feature/prototype-rounds.feature +5 -1
- package/src/workflows/feature/prototype.feature +18 -9
- package/src/workflows/feature/prototype.ts +48 -17
- package/src/workflows/feature/refactor-absorbs.feature +7 -8
- package/src/workflows/feature/refactor-first.feature +7 -8
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +11 -12
- package/src/workflows/feature/refactor-review.feature +72 -72
- package/src/workflows/feature/review-answers.feature +94 -5
- package/src/workflows/feature/review-gate.feature +23 -53
- package/src/workflows/feature/reviewer-walkthrough.feature +33 -12
- package/src/workflows/feature/risk-tier.feature +32 -0
- package/src/workflows/feature/seam-rejection.feature +9 -6
- package/src/workflows/feature/seam.feature +9 -6
- package/src/workflows/feature/seam.test.ts +5 -3
- package/src/workflows/feature/seam.ts +2 -3
- package/src/workflows/feature/severity-budget.feature +11 -7
- package/src/workflows/feature/severity-sorted-review.feature +54 -54
- package/src/workflows/feature/slice-findings.feature +9 -15
- package/src/workflows/feature/slicing.feature +36 -24
- package/src/workflows/feature/standards.feature +4 -4
- package/src/workflows/feature/tier-pin.feature +4 -0
- package/src/workflows/shared/review.ts +26 -6
- package/src/workflows/shared/text.test.ts +85 -11
- package/src/workflows/shared/text.ts +95 -42
- package/src/workflows/feature/coverage.feature +0 -165
- package/src/workflows/feature/coverage.ts +0 -120
|
@@ -30,11 +30,15 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
30
30
|
greeting says "Helo"; make it "Hello".
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
Scenario: triage writes ID'd criteria that reach the naive pass through the design gate
|
|
33
|
+
Scenario: [AC-6] triage writes ID'd criteria that reach the naive pass through the design gate
|
|
34
34
|
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
35
35
|
"""
|
|
36
36
|
# Requirements
|
|
37
37
|
|
|
38
|
+
## Impact
|
|
39
|
+
|
|
40
|
+
- the greeting module
|
|
41
|
+
|
|
38
42
|
## Acceptance Criteria
|
|
39
43
|
|
|
40
44
|
- AC-1: `greeting` reads "Hello, world".
|
|
@@ -45,6 +49,7 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
45
49
|
Then the process rests at "seam.naive"
|
|
46
50
|
And ".gtd/REQUIREMENTS.md" contains "- AC-1: `greeting` reads \"Hello, world\"."
|
|
47
51
|
And ".gtd/REQUIREMENTS.md" contains "- AC-2: no other export changes."
|
|
52
|
+
And the resting step's content contains "maps where the change lands"
|
|
48
53
|
And the resting step's content contains "Acceptance Criteria"
|
|
49
54
|
And the commit subjects end with:
|
|
50
55
|
"""
|
|
@@ -58,6 +63,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
58
63
|
"""
|
|
59
64
|
# Requirements
|
|
60
65
|
|
|
66
|
+
## Impact
|
|
67
|
+
|
|
68
|
+
- the greeting module
|
|
69
|
+
|
|
61
70
|
## Acceptance Criteria
|
|
62
71
|
|
|
63
72
|
- `greeting` reads "Hello, world".
|
|
@@ -69,6 +78,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
69
78
|
"""
|
|
70
79
|
# Requirements
|
|
71
80
|
|
|
81
|
+
## Impact
|
|
82
|
+
|
|
83
|
+
- the greeting module
|
|
84
|
+
|
|
72
85
|
## Acceptance Criteria
|
|
73
86
|
|
|
74
87
|
- AC-1: `greeting` reads "Hello, world".
|
|
@@ -81,6 +94,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
81
94
|
"""
|
|
82
95
|
# Requirements
|
|
83
96
|
|
|
97
|
+
## Impact
|
|
98
|
+
|
|
99
|
+
- the greeting module
|
|
100
|
+
|
|
84
101
|
## Acceptance Criteria
|
|
85
102
|
|
|
86
103
|
- AC-1: `greeting` reads "Hello, world".
|
|
@@ -110,15 +127,114 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
110
127
|
"""
|
|
111
128
|
# Requirements
|
|
112
129
|
|
|
130
|
+
## Impact
|
|
131
|
+
|
|
132
|
+
- the greeting module
|
|
133
|
+
|
|
134
|
+
## Acceptance Criteria
|
|
135
|
+
|
|
136
|
+
- AC-1: `greeting` reads "Hi".
|
|
137
|
+
"""
|
|
138
|
+
Then driving the process is refused with "criteria-frozen"
|
|
139
|
+
|
|
140
|
+
Rule: impact
|
|
141
|
+
|
|
142
|
+
Scenario: [AC-1] a triage turn with no Impact section is refused, naming it
|
|
143
|
+
Given a file "TODO.md" with:
|
|
144
|
+
"""
|
|
145
|
+
# Fix the typo
|
|
146
|
+
|
|
147
|
+
greeting says "Helo"; make it "Hello".
|
|
148
|
+
"""
|
|
149
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
150
|
+
"""
|
|
151
|
+
# Requirements
|
|
152
|
+
|
|
153
|
+
## Acceptance Criteria
|
|
154
|
+
|
|
155
|
+
- AC-1: `greeting` reads "Hello, world".
|
|
156
|
+
"""
|
|
157
|
+
Then driving the process is refused with "impact: .gtd/REQUIREMENTS.md has no \"## Impact\" section"
|
|
158
|
+
|
|
159
|
+
Scenario: [AC-9] a design.impact turn that changes the criteria is refused
|
|
160
|
+
Given a file "TODO.md" with:
|
|
161
|
+
"""
|
|
162
|
+
# Fix the typo
|
|
163
|
+
|
|
164
|
+
## Acceptance criteria
|
|
165
|
+
|
|
166
|
+
- `greeting` reads "Hello, world".
|
|
167
|
+
"""
|
|
168
|
+
And judge step "design.clear" answers:
|
|
169
|
+
"""
|
|
170
|
+
[{"id":"clear","answer":"yes","p":0.9}]
|
|
171
|
+
"""
|
|
172
|
+
And step "design.normalise" runs its script
|
|
173
|
+
And step "design.impact" writes ".gtd/REQUIREMENTS.md" with:
|
|
174
|
+
"""
|
|
175
|
+
# Requirements
|
|
176
|
+
|
|
177
|
+
## Impact
|
|
178
|
+
|
|
179
|
+
- the greeting module
|
|
180
|
+
|
|
113
181
|
## Acceptance Criteria
|
|
114
182
|
|
|
115
183
|
- AC-1: `greeting` reads "Hi".
|
|
116
184
|
"""
|
|
117
185
|
Then driving the process is refused with "criteria-frozen"
|
|
118
186
|
|
|
187
|
+
Scenario: [AC-9] a design.impact turn that adds no Impact section is refused
|
|
188
|
+
Given a file "TODO.md" with:
|
|
189
|
+
"""
|
|
190
|
+
# Fix the typo
|
|
191
|
+
|
|
192
|
+
## Acceptance criteria
|
|
193
|
+
|
|
194
|
+
- `greeting` reads "Hello, world".
|
|
195
|
+
"""
|
|
196
|
+
And judge step "design.clear" answers:
|
|
197
|
+
"""
|
|
198
|
+
[{"id":"clear","answer":"yes","p":0.9}]
|
|
199
|
+
"""
|
|
200
|
+
And step "design.normalise" runs its script
|
|
201
|
+
And step "design.impact" appends to ".gtd/REQUIREMENTS.md":
|
|
202
|
+
"""
|
|
203
|
+
Touches the greeting module.
|
|
204
|
+
"""
|
|
205
|
+
Then driving the process is refused with "impact: .gtd/REQUIREMENTS.md has no \"## Impact\" section"
|
|
206
|
+
|
|
207
|
+
Scenario: [AC-9] a design.impact turn that drops the sketch pointer is refused
|
|
208
|
+
Given a file "TODO.md" with:
|
|
209
|
+
"""
|
|
210
|
+
# Fix the typo
|
|
211
|
+
|
|
212
|
+
## Acceptance criteria
|
|
213
|
+
|
|
214
|
+
- `greeting` reads "Hello, world".
|
|
215
|
+
"""
|
|
216
|
+
And judge step "design.clear" answers:
|
|
217
|
+
"""
|
|
218
|
+
[{"id":"clear","answer":"yes","p":0.9}]
|
|
219
|
+
"""
|
|
220
|
+
And step "design.normalise" runs its script
|
|
221
|
+
And step "design.impact" writes ".gtd/REQUIREMENTS.md" with:
|
|
222
|
+
"""
|
|
223
|
+
# Requirements
|
|
224
|
+
|
|
225
|
+
## Impact
|
|
226
|
+
|
|
227
|
+
- the greeting module
|
|
228
|
+
|
|
229
|
+
## Acceptance Criteria
|
|
230
|
+
|
|
231
|
+
- AC-1: `greeting` reads "Hello, world".
|
|
232
|
+
"""
|
|
233
|
+
Then driving the process is refused with "impact-only"
|
|
234
|
+
|
|
119
235
|
Rule: the skip path
|
|
120
236
|
|
|
121
|
-
Scenario: clear stated criteria are normalised into ID'd requirements with no triage and no design gate
|
|
237
|
+
Scenario: [AC-9] [AC-10] clear stated criteria are normalised into ID'd requirements with no triage and no design gate
|
|
122
238
|
Given a file "TODO.md" with:
|
|
123
239
|
"""
|
|
124
240
|
# Fix the typo
|
|
@@ -133,18 +249,52 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
133
249
|
[{"id":"clear","answer":"yes","p":0.9}]
|
|
134
250
|
"""
|
|
135
251
|
And step "design.normalise" runs its script
|
|
252
|
+
And step "design.impact" appends to ".gtd/REQUIREMENTS.md":
|
|
253
|
+
"""
|
|
254
|
+
## Impact
|
|
255
|
+
|
|
256
|
+
- the greeting module
|
|
257
|
+
"""
|
|
136
258
|
When the process is driven
|
|
137
259
|
Then the process rests at "seam.naive"
|
|
138
260
|
And ".gtd/REQUIREMENTS.md" contains "- AC-1: `greeting` reads \"Hello, world\"."
|
|
139
261
|
And ".gtd/REQUIREMENTS.md" contains "- AC-2: no other export changes."
|
|
262
|
+
And ".gtd/REQUIREMENTS.md" contains "Stated in `TODO.md` by the sketch"
|
|
263
|
+
And the resting step's content contains "maps where the change lands"
|
|
140
264
|
And the commit subjects end with:
|
|
141
265
|
"""
|
|
142
266
|
gtd(human): idle → unwind
|
|
143
267
|
gtd(check): unwind → start-gate.check
|
|
144
268
|
gtd(check): start-gate.check → design.clear
|
|
145
269
|
gtd(judge): design.clear → design.normalise
|
|
146
|
-
gtd(check): design.normalise →
|
|
270
|
+
gtd(check): design.normalise → design.impact
|
|
271
|
+
gtd(agent): design.impact → seam.naive
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
Scenario: a fast-path process recorded before design.impact existed fails replay
|
|
275
|
+
Given a file "TODO.md" with:
|
|
276
|
+
"""
|
|
277
|
+
# Fix the typo
|
|
278
|
+
|
|
279
|
+
## Acceptance criteria
|
|
280
|
+
|
|
281
|
+
- `greeting` reads "Hello, world".
|
|
282
|
+
"""
|
|
283
|
+
And judge step "design.clear" answers:
|
|
284
|
+
"""
|
|
285
|
+
[{"id":"clear","answer":"yes","p":0.9}]
|
|
286
|
+
"""
|
|
287
|
+
And step "design.normalise" runs its script
|
|
288
|
+
When the process is driven
|
|
289
|
+
Then the process rests at "design.impact"
|
|
290
|
+
Given the working tree is committed with the message:
|
|
291
|
+
"""
|
|
292
|
+
gtd(agent): seam.naive → seam.check
|
|
293
|
+
|
|
294
|
+
Gtd-Step: seam.naive#1
|
|
295
|
+
Gtd-Format: 1
|
|
147
296
|
"""
|
|
297
|
+
Then driving the process is refused with "the workflow changed under this process"
|
|
148
298
|
|
|
149
299
|
Scenario: IDs the sketch already gave its criteria are kept
|
|
150
300
|
Given a file "TODO.md" with:
|
|
@@ -159,6 +309,12 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
159
309
|
[{"id":"clear","answer":"yes","p":0.9}]
|
|
160
310
|
"""
|
|
161
311
|
And step "design.normalise" runs its script
|
|
312
|
+
And step "design.impact" appends to ".gtd/REQUIREMENTS.md":
|
|
313
|
+
"""
|
|
314
|
+
## Impact
|
|
315
|
+
|
|
316
|
+
- the greeting module
|
|
317
|
+
"""
|
|
162
318
|
When the process is driven
|
|
163
319
|
Then the process rests at "seam.naive"
|
|
164
320
|
And ".gtd/REQUIREMENTS.md" contains "- AC-3: `greeting` reads \"Hello, world\"."
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import type { Change } from "../../flows/index.js"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
criteriaOf,
|
|
5
|
+
criterionIds,
|
|
6
|
+
handsOnIds,
|
|
7
|
+
hasImpact,
|
|
8
|
+
statedCriteria,
|
|
9
|
+
unstableIds,
|
|
10
|
+
} from "./criteria.js"
|
|
4
11
|
|
|
5
12
|
const added = (path: string, after: string): Change => ({
|
|
6
13
|
path,
|
|
@@ -48,6 +55,17 @@ describe("criteriaOf", () => {
|
|
|
48
55
|
).toEqual([{ id: "AC-1", text: "exits 2 when config is bad - missing file" }])
|
|
49
56
|
})
|
|
50
57
|
|
|
58
|
+
it("reads the (no hands-on) tag between an ID and its colon", () => {
|
|
59
|
+
expect(
|
|
60
|
+
criteriaOf(
|
|
61
|
+
"## Acceptance Criteria\n\n- AC-1: runs\n- AC-2 (no hands-on): the prompt says so\n",
|
|
62
|
+
),
|
|
63
|
+
).toEqual([
|
|
64
|
+
{ id: "AC-1", text: "runs" },
|
|
65
|
+
{ id: "AC-2", text: "the prompt says so", noHandsOn: true },
|
|
66
|
+
])
|
|
67
|
+
})
|
|
68
|
+
|
|
51
69
|
it("is undefined without the section", () => {
|
|
52
70
|
expect(criteriaOf("# R\n\n- AC-1: x\n")).toBeUndefined()
|
|
53
71
|
})
|
|
@@ -66,6 +84,19 @@ describe("statedCriteria", () => {
|
|
|
66
84
|
})
|
|
67
85
|
})
|
|
68
86
|
|
|
87
|
+
it("keeps a stated (no hands-on) tag, renumbered or not", () => {
|
|
88
|
+
const tagged = "## Acceptance criteria\n\n- AC-1: a\n- AC-2 (no hands-on): b\n"
|
|
89
|
+
expect(statedCriteria([added("TODO.md", tagged)], "abc")?.criteria).toEqual([
|
|
90
|
+
{ id: "AC-1", text: "a" },
|
|
91
|
+
{ id: "AC-2", text: "b", noHandsOn: true },
|
|
92
|
+
])
|
|
93
|
+
const partial = "## Acceptance criteria\n\n- a\n- AC-7 (no hands-on): b\n"
|
|
94
|
+
expect(statedCriteria([added("TODO.md", partial)], "abc")?.criteria).toEqual([
|
|
95
|
+
{ id: "AC-1", text: "a" },
|
|
96
|
+
{ id: "AC-2", text: "b", noHandsOn: true },
|
|
97
|
+
])
|
|
98
|
+
})
|
|
99
|
+
|
|
69
100
|
it("renumbers when the stated IDs are partial or clash", () => {
|
|
70
101
|
const sketch = [added("TODO.md", "## Acceptance criteria\n\n- AC-4: a\n- b\n")]
|
|
71
102
|
expect(statedCriteria(sketch, "abc")?.criteria).toEqual([
|
|
@@ -121,6 +152,20 @@ describe("criterionIds", () => {
|
|
|
121
152
|
})
|
|
122
153
|
})
|
|
123
154
|
|
|
155
|
+
describe("handsOnIds", () => {
|
|
156
|
+
const requirements =
|
|
157
|
+
"## Acceptance Criteria\n\n- AC-1: a\n- AC-2 (no hands-on): b\n- AC-3: c\n- AC-4 (no hands-on): d\n"
|
|
158
|
+
|
|
159
|
+
it("leaves out every criterion tagged (no hands-on)", () => {
|
|
160
|
+
expect(handsOnIds(requirements)).toEqual(["AC-1", "AC-3"])
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("keeps to the slice", () => {
|
|
164
|
+
expect(handsOnIds(requirements, { only: ["AC-2", "AC-3"] })).toEqual(["AC-3"])
|
|
165
|
+
expect(handsOnIds(requirements, { only: ["AC-2", "AC-4"] })).toEqual([])
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
124
169
|
describe("unstableIds", () => {
|
|
125
170
|
const ids = { closed: ["AC-1", "AC-3"], used: ["AC-1", "AC-2", "AC-3"] }
|
|
126
171
|
|
|
@@ -139,3 +184,17 @@ describe("unstableIds", () => {
|
|
|
139
184
|
expect(unstableIds(["AC-0", "AC-2"], ids)).toMatch(/^numbers AC-0, AC-2 at or below AC-3/)
|
|
140
185
|
})
|
|
141
186
|
})
|
|
187
|
+
|
|
188
|
+
describe("hasImpact", () => {
|
|
189
|
+
it.each([
|
|
190
|
+
["a level-2 heading", "# R\n\n## Impact\n\nx\n", true],
|
|
191
|
+
["a mixed-case heading", "## impact\n", true],
|
|
192
|
+
["a level-3 heading", "### Impact\n", false],
|
|
193
|
+
["a level-1 heading", "# Impact\n", false],
|
|
194
|
+
["a longer heading", "## Impactful\n", false],
|
|
195
|
+
["a fenced heading", "```md\n## Impact\n```\n", false],
|
|
196
|
+
["no heading", "# R\n\nImpact\n", false],
|
|
197
|
+
])("%s", (_name, text, expected) => {
|
|
198
|
+
expect(hasImpact(text)).toBe(expected)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
answered,
|
|
3
3
|
changes,
|
|
4
|
+
head,
|
|
4
5
|
judge,
|
|
5
6
|
read,
|
|
6
7
|
refuse,
|
|
8
|
+
requireProgress,
|
|
7
9
|
run,
|
|
8
10
|
scope,
|
|
9
11
|
type Change,
|
|
10
12
|
} from "../../flows/index.js"
|
|
11
13
|
import { HEADING, ITEM, markFenced } from "../shared/fences.js"
|
|
12
|
-
import { REQUIREMENTS } from "../shared/steps.js"
|
|
14
|
+
import { planner, REQUIREMENTS } from "../shared/steps.js"
|
|
13
15
|
import * as t from "../shared/text.js"
|
|
14
16
|
|
|
15
17
|
export interface Criterion {
|
|
16
18
|
readonly id: string
|
|
17
19
|
readonly text: string
|
|
20
|
+
/** Tagged `(no hands-on)`: its tests prove it, so the hands-on test skips it. */
|
|
21
|
+
readonly noHandsOn?: true
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/** Acceptance criteria the human's sketch stated outright, ready to normalise. */
|
|
@@ -29,7 +33,13 @@ export interface StatedCriteria {
|
|
|
29
33
|
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
30
34
|
const CLEAR_MIN_P = 0.8
|
|
31
35
|
|
|
32
|
-
const ID = /^(AC-\d+)
|
|
36
|
+
const ID = /^(AC-\d+)(\s*\(no hands-on\))?:\s*(.*)$/
|
|
37
|
+
|
|
38
|
+
const criterion = (id: RegExpExecArray, as = id[1]!): Criterion => ({
|
|
39
|
+
id: as,
|
|
40
|
+
text: id[3]!,
|
|
41
|
+
...(id[2] === undefined ? {} : { noHandsOn: true }),
|
|
42
|
+
})
|
|
33
43
|
|
|
34
44
|
/** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
|
|
35
45
|
const listUnder = (text: string, title: RegExp, level?: number): string[] | undefined => {
|
|
@@ -66,7 +76,7 @@ const listUnder = (text: string, title: RegExp, level?: number): string[] | unde
|
|
|
66
76
|
export const criteriaOf = (text: string): readonly Criterion[] | undefined =>
|
|
67
77
|
listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
|
|
68
78
|
const id = ID.exec(item)
|
|
69
|
-
return id === null ? { id: "", text: item } :
|
|
79
|
+
return id === null ? { id: "", text: item } : criterion(id)
|
|
70
80
|
})
|
|
71
81
|
|
|
72
82
|
/**
|
|
@@ -90,6 +100,12 @@ export const criterionIds = (text: string, slice?: CriteriaSlice): string[] =>
|
|
|
90
100
|
!(slice?.except ?? []).includes(id),
|
|
91
101
|
)
|
|
92
102
|
|
|
103
|
+
/** The ID'd criteria of `text` (`slice`'s when given) a person can try by hand: none tagged `(no hands-on)`. */
|
|
104
|
+
export const handsOnIds = (text: string, slice?: CriteriaSlice): string[] => {
|
|
105
|
+
const tagged = (criteriaOf(text) ?? []).filter((c) => c.noHandsOn).map((c) => c.id)
|
|
106
|
+
return criterionIds(text, slice).filter((id) => !tagged.includes(id))
|
|
107
|
+
}
|
|
108
|
+
|
|
93
109
|
/** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
|
|
94
110
|
export const sliceIds = (slice: CriteriaSlice | undefined): string[] | undefined =>
|
|
95
111
|
slice === undefined ? undefined : criterionIds(read(REQUIREMENTS) ?? "", slice)
|
|
@@ -98,11 +114,11 @@ export const sliceIds = (slice: CriteriaSlice | undefined): string[] | undefined
|
|
|
98
114
|
const numbered = (items: readonly string[]): Criterion[] => {
|
|
99
115
|
const ids = items.map((item) => ID.exec(item))
|
|
100
116
|
const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id![1])).size === ids.length
|
|
101
|
-
return items.map((item, i) =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
117
|
+
return items.map((item, i) => {
|
|
118
|
+
const id = ids[i]
|
|
119
|
+
if (own) return criterion(id!)
|
|
120
|
+
return id === null ? { id: `AC-${i + 1}`, text: item } : criterion(id!, `AC-${i + 1}`)
|
|
121
|
+
})
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
const STATED = /^acceptance criteria:?$/i
|
|
@@ -151,6 +167,54 @@ export const requireCriteria = (file: string): void => {
|
|
|
151
167
|
)
|
|
152
168
|
}
|
|
153
169
|
|
|
170
|
+
const isImpactHeading = (h: RegExpExecArray | null): boolean =>
|
|
171
|
+
h !== null && h[1]!.length === 2 && /^impact$/i.test(h[2]!.trim())
|
|
172
|
+
|
|
173
|
+
/** Whether `text` has an unfenced, level-2 `## Impact` heading. */
|
|
174
|
+
export const hasImpact = (text: string): boolean =>
|
|
175
|
+
markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)))
|
|
176
|
+
|
|
177
|
+
/** `text` without its `## Impact` section, whitespace collapsed. */
|
|
178
|
+
const withoutImpact = (text: string): string => {
|
|
179
|
+
let inImpact = false
|
|
180
|
+
const kept = markFenced(text).filter((l) => {
|
|
181
|
+
const h = l.fenced ? null : HEADING.exec(l.line)
|
|
182
|
+
if (h !== null && h[1]!.length <= 2) inImpact = isImpactHeading(h)
|
|
183
|
+
return !inImpact
|
|
184
|
+
})
|
|
185
|
+
return kept
|
|
186
|
+
.map((l) => l.line)
|
|
187
|
+
.join(" ")
|
|
188
|
+
.replace(/\s+/g, " ")
|
|
189
|
+
.trim()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Refuse a turn that leaves `file` without a `## Impact` section. Only the
|
|
194
|
+
* pending turn (`head()` is `""`): a triage lap recorded before Impact existed
|
|
195
|
+
* replays without one, and refusing there would fail every in-flight process.
|
|
196
|
+
* A human's edit at a later gate is not checked — the human owns that file then.
|
|
197
|
+
*/
|
|
198
|
+
export const requireImpact = (file: string): void => {
|
|
199
|
+
if (head() !== "") return
|
|
200
|
+
const text = read(file)
|
|
201
|
+
if (text === undefined) return
|
|
202
|
+
if (hasImpact(text)) return
|
|
203
|
+
refuse(
|
|
204
|
+
`gtd land: impact: ${file} has no "## Impact" section — explore the codebase and add "## Impact" naming the units, public surfaces and surprises this change touches`,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Refuse a turn that changed `file` beyond its `## Impact` section. */
|
|
209
|
+
export const requireImpactOnly = (file: string): void => {
|
|
210
|
+
const change = changes().get(file)
|
|
211
|
+
if (change === undefined) return
|
|
212
|
+
if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return
|
|
213
|
+
refuse(
|
|
214
|
+
`gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`,
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
154
218
|
/** The IDs a review's feedback lap must respect. */
|
|
155
219
|
export interface StableIds {
|
|
156
220
|
/** Listed when the build closed: each may stay, reworded or not, or go. */
|
|
@@ -197,6 +261,20 @@ const clear = async (stated: StatedCriteria): Promise<boolean> => {
|
|
|
197
261
|
return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P)
|
|
198
262
|
}
|
|
199
263
|
|
|
264
|
+
/** The fast path's impact-only turn: add `## Impact`, leave the criteria alone. */
|
|
265
|
+
const impact = async (): Promise<void> => {
|
|
266
|
+
await t.agentWithSkills("impact", t.designImpactPrompt(), {
|
|
267
|
+
label: "Mapping the change's impact",
|
|
268
|
+
file: REQUIREMENTS,
|
|
269
|
+
model: planner(),
|
|
270
|
+
system: t.designSystem(),
|
|
271
|
+
})
|
|
272
|
+
requireProgress(REQUIREMENTS)
|
|
273
|
+
requireCriteriaFrozen(REQUIREMENTS)
|
|
274
|
+
requireImpact(REQUIREMENTS)
|
|
275
|
+
requireImpactOnly(REQUIREMENTS)
|
|
276
|
+
}
|
|
277
|
+
|
|
200
278
|
/**
|
|
201
279
|
* The skip path: when the sketch stated clear, checkable criteria, write them
|
|
202
280
|
* to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
|
|
@@ -211,6 +289,7 @@ export const skipDesign = (stated: StatedCriteria | undefined): Promise<boolean>
|
|
|
211
289
|
label: "Recording the stated acceptance criteria",
|
|
212
290
|
file: REQUIREMENTS,
|
|
213
291
|
})
|
|
292
|
+
await impact()
|
|
214
293
|
return true
|
|
215
294
|
})
|
|
216
295
|
|
|
@@ -27,10 +27,6 @@ Feature: a trivial change takes the same few beats at either risk tier
|
|
|
27
27
|
And step "design.gate.answer" changes nothing
|
|
28
28
|
And step "implement.health.check" changes nothing
|
|
29
29
|
And step "implement.closing" runs its script
|
|
30
|
-
And judge step "implement.coverage" answers:
|
|
31
|
-
"""
|
|
32
|
-
[{"id":"AC-1","answer":"yes","p":0.9}]
|
|
33
|
-
"""
|
|
34
30
|
And step "build.quality.correctness.reviewing" changes nothing
|
|
35
31
|
|
|
36
32
|
Scenario Outline: a one-line fix runs from idle to the review gate with the same beats at either tier
|
|
@@ -42,6 +38,10 @@ Feature: a trivial change takes the same few beats at either risk tier
|
|
|
42
38
|
|
|
43
39
|
<tier> — one-line copy fix
|
|
44
40
|
|
|
41
|
+
## Impact
|
|
42
|
+
|
|
43
|
+
- the greeting module
|
|
44
|
+
|
|
45
45
|
## Acceptance Criteria
|
|
46
46
|
|
|
47
47
|
- AC-1: the change behaves as described.
|
|
@@ -78,8 +78,7 @@ Feature: a trivial change takes the same few beats at either risk tier
|
|
|
78
78
|
gtd(check): seam.check → seam.judge
|
|
79
79
|
gtd(judge): seam.judge → implement.applying
|
|
80
80
|
gtd(check): implement.applying → implement.health.check
|
|
81
|
-
gtd(check): implement.health.check → implement.
|
|
82
|
-
gtd(judge): implement.coverage → implement.closing
|
|
81
|
+
gtd(check): implement.health.check → implement.closing
|
|
83
82
|
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
84
83
|
gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
|
|
85
84
|
"""
|
|
@@ -99,7 +99,7 @@ describe("afterTail feedback lap", () => {
|
|
|
99
99
|
"build.review.collecting": (_, r) => {
|
|
100
100
|
r.write(
|
|
101
101
|
".gtd/REQUIREMENTS.md",
|
|
102
|
-
"# Requirements\n\n- multiply\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
102
|
+
"# Requirements\n\n- multiply\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
103
103
|
)
|
|
104
104
|
r.write(".gtd/REVIEW.md", `${review("also export multiply")} - A: noted.\n`)
|
|
105
105
|
},
|
|
@@ -115,7 +115,7 @@ describe("afterTail feedback lap", () => {
|
|
|
115
115
|
"design.triage": (_: unknown, r: ReturnType<typeof testRepo>) =>
|
|
116
116
|
r.write(
|
|
117
117
|
".gtd/REQUIREMENTS.md",
|
|
118
|
-
"# Requirements\n\n- multiply, designed\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
118
|
+
"# Requirements\n\n- multiply, designed\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
119
119
|
),
|
|
120
120
|
"design.gate.answer": () => undefined,
|
|
121
121
|
}
|
|
@@ -214,7 +214,7 @@ describe("an in-flight 28.0.0 naive-pass process", () => {
|
|
|
214
214
|
"design.triage": (_, r) =>
|
|
215
215
|
r.write(
|
|
216
216
|
".gtd/REQUIREMENTS.md",
|
|
217
|
-
"# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
217
|
+
"# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
218
218
|
),
|
|
219
219
|
"design.gate.answer": () => undefined,
|
|
220
220
|
"seam.naive": (_, r) => {
|
|
@@ -258,6 +258,7 @@ describe("an in-flight 28.0.0 naive-pass process", () => {
|
|
|
258
258
|
})
|
|
259
259
|
|
|
260
260
|
describe("an in-flight process from before the architecture path was removed", () => {
|
|
261
|
+
// No `## Impact`: no version before it wrote one, and replaying its triage must not refuse.
|
|
261
262
|
const REQUIREMENTS =
|
|
262
263
|
"# Requirements\n\n## Concerns\n\n### Subtract\n\n- `subtract`\n\n## Acceptance Criteria\n\n- AC-1: done\n"
|
|
263
264
|
|
|
@@ -321,7 +322,7 @@ describe("flagMode", () => {
|
|
|
321
322
|
"design.triage": (_, r) =>
|
|
322
323
|
r.write(
|
|
323
324
|
".gtd/REQUIREMENTS.md",
|
|
324
|
-
"# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
325
|
+
"# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
325
326
|
),
|
|
326
327
|
"design.gate.answer": () => undefined,
|
|
327
328
|
})
|
|
@@ -342,7 +343,7 @@ describe("flagMode", () => {
|
|
|
342
343
|
"design.triage": (_, r) =>
|
|
343
344
|
r.write(
|
|
344
345
|
".gtd/REQUIREMENTS.md",
|
|
345
|
-
"# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
346
|
+
"# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
346
347
|
),
|
|
347
348
|
"design.gate.answer": () => undefined,
|
|
348
349
|
"seam.naive": (_, r) => {
|
|
@@ -123,7 +123,6 @@ const buildSlice = async (sliceBase: string, build: SliceBuild): Promise<void> =
|
|
|
123
123
|
naive: seam.land === true ? seam.reference : undefined,
|
|
124
124
|
rebuild: seam.rebuild,
|
|
125
125
|
slice: build.criteria,
|
|
126
|
-
base: build.criteria === undefined ? undefined : sliceBase,
|
|
127
126
|
more: build.more,
|
|
128
127
|
})
|
|
129
128
|
// Closing kept the criteria exactly when there is more to build, so this reads as it did.
|
|
@@ -179,7 +178,7 @@ const reviewedTail = (
|
|
|
179
178
|
)
|
|
180
179
|
|
|
181
180
|
// seam.naive / seam.judge never run on a feedback lap: its design → implement is built unjudged
|
|
182
|
-
// and unrefactored
|
|
181
|
+
// and unrefactored.
|
|
183
182
|
const featureFeedback = async (
|
|
184
183
|
first: ReviewOutcome,
|
|
185
184
|
lensBase: string,
|
|
@@ -37,6 +37,10 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
|
|
|
37
37
|
|
|
38
38
|
- `subtract(a, b)` returns `a - b`.
|
|
39
39
|
|
|
40
|
+
## Impact
|
|
41
|
+
|
|
42
|
+
- the greeting module
|
|
43
|
+
|
|
40
44
|
## Acceptance Criteria
|
|
41
45
|
|
|
42
46
|
- AC-1: the change behaves as described.
|
|
@@ -61,10 +65,6 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
|
|
|
61
65
|
And step "implement.applying" runs its script
|
|
62
66
|
And step "implement.health.check" changes nothing
|
|
63
67
|
And step "implement.closing" runs its script
|
|
64
|
-
And judge step "implement.coverage" answers:
|
|
65
|
-
"""
|
|
66
|
-
[{"id":"AC-1","answer":"yes","p":0.9}]
|
|
67
|
-
"""
|
|
68
68
|
And step "build.quality.correctness.reviewing" changes nothing
|
|
69
69
|
When the process is driven
|
|
70
70
|
Then the process rests at "build.review.reviewing"
|
|
@@ -82,8 +82,7 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
|
|
|
82
82
|
gtd(agent): seam.naive → seam.check
|
|
83
83
|
gtd(check): seam.check → implement.applying
|
|
84
84
|
gtd(check): implement.applying → implement.health.check
|
|
85
|
-
gtd(check): implement.health.check → implement.
|
|
86
|
-
gtd(judge): implement.coverage → implement.closing
|
|
85
|
+
gtd(check): implement.health.check → implement.closing
|
|
87
86
|
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
88
87
|
gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
|
|
89
88
|
"""
|