@pmelab/gtd 46.0.0 → 48.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +21 -19
- package/dist/chunks/{reserved-BENPy6IJ.mjs → reserved-DF1HD6jL.mjs} +41 -22
- package/dist/chunks/{review-DcbgUwr7.mjs → review-Bv40kZu9.mjs} +5 -8
- package/dist/chunks/{tail-CDNMZ-2E.mjs → tail-D89fU_wf.mjs} +68 -39
- package/dist/gtd.bundle.mjs +203 -867
- package/dist/testkit.mjs +89 -53
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +26 -22
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +257 -205
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +0 -2
- package/src/cli/Cli.ts +0 -15
- package/src/cli/Program.test.ts +0 -1
- package/src/cli/Program.ts +0 -8
- package/src/cli/ToolCommands.ts +1 -2
- package/src/install/Install.ts +89 -35
- package/src/testkit/Layers.ts +0 -1
- package/src/wire/ContextLimit.ts +1 -1
- package/src/workflows/cleanup/cleanup.test.ts +11 -9
- package/src/workflows/feature/feature.ts +38 -28
- package/src/workflows/feature/lens-review.feature +3 -1
- package/src/workflows/feature/slice-findings.feature +198 -0
- package/src/workflows/fix/fix-lens-origin.feature +101 -0
- package/src/workflows/fix/fix-quality.feature +131 -0
- package/src/workflows/index.ts +0 -1
- package/src/workflows/shared/lenses.ts +30 -5
- package/src/workflows/shared/review.ts +3 -8
- package/src/workflows/shared/tail.ts +74 -57
- package/src/workflows/shared/text.ts +85 -29
- package/src/workflows/feature/quality-unresolved.feature +0 -121
|
@@ -117,16 +117,22 @@ export const qualityFindingsFile = (
|
|
|
117
117
|
): string =>
|
|
118
118
|
`# Quality\n\n${findings.map((f) => `## ${f.title}\n\n${f.body}`.trim()).join("\n\n")}\n`
|
|
119
119
|
|
|
120
|
+
/** Where a reported finding came from; absent when unknown or, for `slice`, a single-slice build. */
|
|
121
|
+
export interface Origin {
|
|
122
|
+
readonly lens?: string | undefined
|
|
123
|
+
readonly slice?: string | undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
120
126
|
export interface ComposedReview {
|
|
121
|
-
readonly notes: readonly {
|
|
127
|
+
readonly notes: readonly (Origin & {
|
|
122
128
|
readonly title: string
|
|
123
129
|
readonly body: string
|
|
124
130
|
readonly unresolved?: true
|
|
125
131
|
/** A fix turn removed its chunk while renaming another. */
|
|
126
132
|
readonly unclear?: true
|
|
127
|
-
}[]
|
|
128
|
-
readonly fixed: readonly { readonly title: string; readonly commit: string }[]
|
|
129
|
-
readonly noise: readonly { readonly title: string; readonly p: number }[]
|
|
133
|
+
})[]
|
|
134
|
+
readonly fixed: readonly (Origin & { readonly title: string; readonly commit: string })[]
|
|
135
|
+
readonly noise: readonly (Origin & { readonly title: string; readonly p: number })[]
|
|
130
136
|
/** `Then` lines a `.feature` file at the review base no longer carries. */
|
|
131
137
|
readonly weakened?: readonly RemovedAssertions[]
|
|
132
138
|
/** The commit that fixed the human's nits, on the review that follows it. */
|
|
@@ -154,6 +160,34 @@ const unboxed = (body: string): string =>
|
|
|
154
160
|
|
|
155
161
|
const short = (hash: string): string => hash.slice(0, 7)
|
|
156
162
|
|
|
163
|
+
/** `slice 2, lens tests`; empty when neither is known. */
|
|
164
|
+
const originOf = (f: Origin): string =>
|
|
165
|
+
[
|
|
166
|
+
...(f.slice === undefined ? [] : [`slice ${f.slice}`]),
|
|
167
|
+
...(f.lens === undefined ? [] : [`lens ${f.lens}`]),
|
|
168
|
+
].join(", ")
|
|
169
|
+
|
|
170
|
+
const originLine = (f: Origin): string => {
|
|
171
|
+
const origin = originOf(f)
|
|
172
|
+
return origin === "" ? "" : `_From ${origin}._\n\n`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const withOrigin = (f: Origin, rest: string): string =>
|
|
176
|
+
[rest, originOf(f)].filter((s) => s !== "").join(", ")
|
|
177
|
+
|
|
178
|
+
type LapNote = ComposedReview["notes"][number]
|
|
179
|
+
|
|
180
|
+
const noteBody = (n: LapNote): string => {
|
|
181
|
+
const body = unboxed(n.body) || "_No detail given._"
|
|
182
|
+
if (n.unresolved === true) return `_The fixer left this unresolved._\n\n${body}`
|
|
183
|
+
if (n.unclear === true)
|
|
184
|
+
return `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${body}`
|
|
185
|
+
return body
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// A one-slice review needs no provenance: only carried findings name their slice and lens.
|
|
189
|
+
const carriedOnly = (f: Origin): Origin => (f.slice === undefined ? {} : f)
|
|
190
|
+
|
|
157
191
|
/**
|
|
158
192
|
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
|
|
159
193
|
* answer boxes, then the fixed and noise lists. Chunk titles are made unique —
|
|
@@ -200,23 +234,14 @@ export const composedReview = (base: string, report: ComposedReview): string =>
|
|
|
200
234
|
]),
|
|
201
235
|
...conflicts,
|
|
202
236
|
...notes.map((n) =>
|
|
203
|
-
chunk(
|
|
204
|
-
n.title,
|
|
205
|
-
`${
|
|
206
|
-
n.unresolved === true
|
|
207
|
-
? `_The fixer left this unresolved._\n\n${unboxed(n.body)}`
|
|
208
|
-
: n.unclear === true
|
|
209
|
-
? `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${unboxed(n.body)}`
|
|
210
|
-
: unboxed(n.body) || "_No detail given._"
|
|
211
|
-
}\n\n${answerBoxes}`,
|
|
212
|
-
),
|
|
237
|
+
chunk(n.title, `${originLine(carriedOnly(n))}${noteBody(n)}\n\n${answerBoxes}`),
|
|
213
238
|
),
|
|
214
239
|
...(report.fixed.length === 0
|
|
215
240
|
? []
|
|
216
241
|
: [
|
|
217
242
|
chunk(
|
|
218
243
|
"Fixed automatically",
|
|
219
|
-
report.fixed.map((f) =>
|
|
244
|
+
report.fixed.map((f) => fixedLine(f, carriedOnly(f))).join("\n"),
|
|
220
245
|
),
|
|
221
246
|
]),
|
|
222
247
|
...(added.length === 0
|
|
@@ -229,7 +254,12 @@ export const composedReview = (base: string, report: ComposedReview): string =>
|
|
|
229
254
|
]),
|
|
230
255
|
...(report.noise.length === 0
|
|
231
256
|
? []
|
|
232
|
-
: [
|
|
257
|
+
: [
|
|
258
|
+
chunk(
|
|
259
|
+
"Dropped as noise",
|
|
260
|
+
report.noise.map((f) => noiseLine(f, carriedOnly(f))).join("\n"),
|
|
261
|
+
),
|
|
262
|
+
]),
|
|
233
263
|
]
|
|
234
264
|
const body =
|
|
235
265
|
chunks.length > 0
|
|
@@ -243,19 +273,6 @@ export const composedReview = (base: string, report: ComposedReview): string =>
|
|
|
243
273
|
return `# Review: ${short(base)}\n\n<!-- base: ${base} -->\n\n${body.join("\n\n")}\n`
|
|
244
274
|
}
|
|
245
275
|
|
|
246
|
-
export const qualityUnresolvedMessage = (): string =>
|
|
247
|
-
`The quality lap's fix turns hit their cap and \`.gtd/QUALITY.md\` still
|
|
248
|
-
holds findings. Nothing after this fixes them, so the process will not
|
|
249
|
-
go on with them open.
|
|
250
|
-
|
|
251
|
-
Fix what matters by hand, then delete or empty \`.gtd/QUALITY.md\` —
|
|
252
|
-
dropping a finding is your call to make.
|
|
253
|
-
|
|
254
|
-
What each change does next (then run \`gtd land\`):
|
|
255
|
-
- **Delete or empty \`.gtd/QUALITY.md\`** — the suite runs on your edits, then the next slice is built, or after the last one you review the change (**build.review.reviewing**).
|
|
256
|
-
- **Anything else** — rests here again.
|
|
257
|
-
`
|
|
258
|
-
|
|
259
276
|
export const startGateBlockedMessage = (): string =>
|
|
260
277
|
`The test baseline is red — gtd will not start new work on a broken suite.
|
|
261
278
|
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
@@ -1125,6 +1142,45 @@ const lapRange: Record<ReviewRange, string> = {
|
|
|
1125
1142
|
"it covers the whole refactor since it started, on every round. A refactor may update an existing test's call sites only: give any test-file hunk its own chunk, and say so if it changes a test, an assertion or an expected value.",
|
|
1126
1143
|
}
|
|
1127
1144
|
|
|
1145
|
+
/** What the quality lap left for a reviewer-written review: its notes, fixes and noise. */
|
|
1146
|
+
export type LapFindings = Pick<ComposedReview, "notes" | "fixed" | "noise">
|
|
1147
|
+
|
|
1148
|
+
const fixedLine = (f: ComposedReview["fixed"][number], origin: Origin = f): string =>
|
|
1149
|
+
`- ${f.title} (${withOrigin(origin, short(f.commit))})`
|
|
1150
|
+
|
|
1151
|
+
const noiseLine = (f: ComposedReview["noise"][number], origin: Origin = f): string =>
|
|
1152
|
+
`- ${f.title} (${withOrigin(origin, `p=${f.p}`)})`
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* `review` with the lap's notes appended as prose chunks — no pointer, no
|
|
1156
|
+
* answer boxes — then what it fixed and dropped. A title the review already
|
|
1157
|
+
* holds gets a numbered suffix, as notes are told apart by title.
|
|
1158
|
+
*/
|
|
1159
|
+
export const withLapFindings = (review: string, { notes, fixed, noise }: LapFindings): string => {
|
|
1160
|
+
const used = new Set(
|
|
1161
|
+
review
|
|
1162
|
+
.split("\n")
|
|
1163
|
+
.filter((line) => line.startsWith("## "))
|
|
1164
|
+
.map((line) => line.slice(3).trim()),
|
|
1165
|
+
)
|
|
1166
|
+
const unique = (title: string): string => {
|
|
1167
|
+
let name = title
|
|
1168
|
+
for (let n = 2; used.has(name); n++) name = `${title} (${n})`
|
|
1169
|
+
used.add(name)
|
|
1170
|
+
return name
|
|
1171
|
+
}
|
|
1172
|
+
const chunks = [
|
|
1173
|
+
...notes.map((n) => `## ${unique(n.title)}\n\n${originLine(n)}${noteBody(n)}`),
|
|
1174
|
+
...(fixed.length === 0
|
|
1175
|
+
? []
|
|
1176
|
+
: [`## ${unique("Fixed automatically")}\n\n${fixed.map((f) => fixedLine(f)).join("\n")}`]),
|
|
1177
|
+
...(noise.length === 0
|
|
1178
|
+
? []
|
|
1179
|
+
: [`## ${unique("Dropped as noise")}\n\n${noise.map((f) => noiseLine(f)).join("\n")}`]),
|
|
1180
|
+
]
|
|
1181
|
+
return chunks.length === 0 ? review : `${review.trimEnd()}\n\n${chunks.join("\n\n")}\n`
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1128
1184
|
export const buildReviewReviewingPrompt = (
|
|
1129
1185
|
base: string,
|
|
1130
1186
|
lap: ReviewRange,
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
@inmem
|
|
2
|
-
Feature: quality findings left unresolved between slices hold the next slice back
|
|
3
|
-
|
|
4
|
-
Between slices no review reads the quality lap's leftovers, so when the fix
|
|
5
|
-
cap leaves `.gtd/QUALITY.md` behind the process rests for a person before
|
|
6
|
-
the next slice. The last slice's leftovers become review notes instead (see
|
|
7
|
-
`lens-review.feature`).
|
|
8
|
-
|
|
9
|
-
Background:
|
|
10
|
-
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_FLAGMODE" set to "false"
|
|
13
|
-
And an environment variable "GTD_SLICEMAXCRITERIA" set to "1"
|
|
14
|
-
And a file "src/calc.ts" with:
|
|
15
|
-
"""
|
|
16
|
-
export const add = (a: number, b: number) => a + b
|
|
17
|
-
"""
|
|
18
|
-
And the working tree is committed as "feat: add calculator"
|
|
19
|
-
And a file "TODO.md" with:
|
|
20
|
-
"""
|
|
21
|
-
# More arithmetic
|
|
22
|
-
|
|
23
|
-
subtract and multiply.
|
|
24
|
-
"""
|
|
25
|
-
And step "unwind" changes nothing
|
|
26
|
-
And step "start-gate.check" changes nothing
|
|
27
|
-
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
28
|
-
"""
|
|
29
|
-
# Requirements
|
|
30
|
-
|
|
31
|
-
## Acceptance Criteria
|
|
32
|
-
|
|
33
|
-
- AC-1: subtract(a, b) returns a - b.
|
|
34
|
-
- AC-2: multiply(a, b) returns a * b.
|
|
35
|
-
"""
|
|
36
|
-
And step "design.gate.answer" changes nothing
|
|
37
|
-
And step "design.slicing" writes ".gtd/SLICES.md" with:
|
|
38
|
-
"""
|
|
39
|
-
# Slices
|
|
40
|
-
|
|
41
|
-
## Subtract
|
|
42
|
-
|
|
43
|
-
- AC-1
|
|
44
|
-
|
|
45
|
-
## Multiply
|
|
46
|
-
|
|
47
|
-
- AC-2
|
|
48
|
-
"""
|
|
49
|
-
And step "seam.naive", when prompted with "This slice builds AC-1 only", writes "src/sub.test.ts" with:
|
|
50
|
-
"""
|
|
51
|
-
it("[AC-1] subtracts", () => {})
|
|
52
|
-
"""
|
|
53
|
-
And step "seam.naive", when prompted with "This slice builds AC-2 only", writes "src/mul.test.ts" with:
|
|
54
|
-
"""
|
|
55
|
-
it("[AC-2] multiplies", () => {})
|
|
56
|
-
"""
|
|
57
|
-
And step "seam.check" changes nothing
|
|
58
|
-
And step "implement.applying" runs its script
|
|
59
|
-
And step "implement.health.check" changes nothing
|
|
60
|
-
And step "implement.closing" runs its script
|
|
61
|
-
And judge step "implement.coverage", when its questions contain "AC-1", answers:
|
|
62
|
-
"""
|
|
63
|
-
[{"id":"AC-1","answer":"yes","p":0.9}]
|
|
64
|
-
"""
|
|
65
|
-
And judge step "implement.coverage", when its questions contain "AC-2", answers:
|
|
66
|
-
"""
|
|
67
|
-
[{"id":"AC-2","answer":"yes","p":0.9}]
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
Scenario: findings the fix cap left after a slice rest at build.quality-unresolved
|
|
71
|
-
Given step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
|
|
72
|
-
"""
|
|
73
|
-
# Quality
|
|
74
|
-
|
|
75
|
-
## subtract has no negative test
|
|
76
|
-
|
|
77
|
-
Add a test subtracting a larger number.
|
|
78
|
-
"""
|
|
79
|
-
And step "build.fix.quality.fixing" from now on changes nothing
|
|
80
|
-
And step "build.health.describe" writes ".gtd/ESCALATION.md" with:
|
|
81
|
-
"""
|
|
82
|
-
# Escalation
|
|
83
|
-
|
|
84
|
-
The fix turn changed nothing.
|
|
85
|
-
"""
|
|
86
|
-
When the process is driven
|
|
87
|
-
Then the process rests at "build.health.stop"
|
|
88
|
-
Given step "build.fix" writes "src/calc.ts" with:
|
|
89
|
-
"""
|
|
90
|
-
export const add = (a: number, b: number) => a + b
|
|
91
|
-
// checked
|
|
92
|
-
"""
|
|
93
|
-
And step "build.health.check" changes nothing
|
|
94
|
-
When gtd lands
|
|
95
|
-
And the process is driven
|
|
96
|
-
Then the process rests at "build.quality-unresolved"
|
|
97
|
-
And ".gtd/QUALITY.md" contains "subtract has no negative test"
|
|
98
|
-
|
|
99
|
-
When gtd lands
|
|
100
|
-
And the process is driven
|
|
101
|
-
Then the process rests at "build.quality-unresolved"
|
|
102
|
-
|
|
103
|
-
Given the file ".gtd/QUALITY.md" is deleted
|
|
104
|
-
And step "build.quality.correctness.reviewing" from now on changes nothing
|
|
105
|
-
And step "build.review.composing" runs its script
|
|
106
|
-
When gtd lands
|
|
107
|
-
And the process is driven
|
|
108
|
-
Then the process rests at "build.review.await-review"
|
|
109
|
-
And the commit subjects end with:
|
|
110
|
-
"""
|
|
111
|
-
gtd(human): build.quality-unresolved → build.health.check
|
|
112
|
-
gtd(check): build.health.check → seam.naive
|
|
113
|
-
gtd(agent): seam.naive → seam.check
|
|
114
|
-
gtd(check): seam.check → implement.applying
|
|
115
|
-
gtd(check): implement.applying → implement.health.check
|
|
116
|
-
gtd(check): implement.health.check → implement.coverage
|
|
117
|
-
gtd(judge): implement.coverage → implement.closing
|
|
118
|
-
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
119
|
-
gtd(agent): build.quality.correctness.reviewing → build.review.composing
|
|
120
|
-
gtd(check): build.review.composing → build.review.await-review
|
|
121
|
-
"""
|