@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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: each finding the reviewer is handed names the lens that wrote it
|
|
3
|
+
|
|
4
|
+
Lenses append to `.gtd/QUALITY.md` in turn, so a finding belongs to the lens
|
|
5
|
+
whose turn added it — even when two lenses give findings the same title,
|
|
6
|
+
which the review then tells apart by a numbered suffix.
|
|
7
|
+
|
|
8
|
+
Background:
|
|
9
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
10
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness,tests"
|
|
11
|
+
And a file "src/calc.ts" with:
|
|
12
|
+
"""
|
|
13
|
+
export const add = (a: number, b: number) => a + b
|
|
14
|
+
"""
|
|
15
|
+
And the working tree is committed as "feat: add calculator"
|
|
16
|
+
And a file ".gtd/REVIEW.orig.md" with:
|
|
17
|
+
"""
|
|
18
|
+
# Review: abc1234
|
|
19
|
+
|
|
20
|
+
<!-- base: 0000000 -->
|
|
21
|
+
|
|
22
|
+
## calc
|
|
23
|
+
|
|
24
|
+
- [ ] ./src/calc.ts#1-1
|
|
25
|
+
new add function
|
|
26
|
+
"""
|
|
27
|
+
And a file ".gtd/REVIEW.md" with:
|
|
28
|
+
"""
|
|
29
|
+
# Review: abc1234
|
|
30
|
+
|
|
31
|
+
<!-- base: 0000000 -->
|
|
32
|
+
|
|
33
|
+
## calc
|
|
34
|
+
|
|
35
|
+
- [ ] ./src/calc.ts#1-1
|
|
36
|
+
new add function — rename it to sum
|
|
37
|
+
"""
|
|
38
|
+
And the working tree is committed as "chore: hand off a review"
|
|
39
|
+
And step "notes.fixing" writes "src/calc.ts" with:
|
|
40
|
+
"""
|
|
41
|
+
export const sum = (a: number, b: number) => a + b
|
|
42
|
+
"""
|
|
43
|
+
And step "notes.closing" runs its script
|
|
44
|
+
And step "build.health.check" changes nothing
|
|
45
|
+
And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
|
|
46
|
+
"""
|
|
47
|
+
# Quality
|
|
48
|
+
|
|
49
|
+
## sum is untested
|
|
50
|
+
|
|
51
|
+
From correctness.
|
|
52
|
+
"""
|
|
53
|
+
And step "build.quality.tests.reviewing" writes ".gtd/QUALITY.md" with:
|
|
54
|
+
"""
|
|
55
|
+
# Quality
|
|
56
|
+
|
|
57
|
+
## sum is untested
|
|
58
|
+
|
|
59
|
+
From correctness.
|
|
60
|
+
|
|
61
|
+
## sum is untested
|
|
62
|
+
|
|
63
|
+
From tests.
|
|
64
|
+
"""
|
|
65
|
+
And step "build.quality.sorting" runs its script
|
|
66
|
+
And step "build.quality.closing" runs its script
|
|
67
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
68
|
+
"""
|
|
69
|
+
# Review: abc1234
|
|
70
|
+
|
|
71
|
+
<!-- base: 0000000 -->
|
|
72
|
+
|
|
73
|
+
## calc
|
|
74
|
+
|
|
75
|
+
- [ ] ./src/calc.ts#1-1
|
|
76
|
+
renamed to sum
|
|
77
|
+
"""
|
|
78
|
+
And step "build.review.carrying" runs its script
|
|
79
|
+
|
|
80
|
+
Scenario: two lenses' findings with one title each name their own lens
|
|
81
|
+
Given judge step "build.quality.triage" answers:
|
|
82
|
+
"""
|
|
83
|
+
[{"id":"Q1","answer":"design","p":0.9},{"id":"Q2","answer":"design","p":0.9}]
|
|
84
|
+
"""
|
|
85
|
+
When gtd opens door "fix"
|
|
86
|
+
And the process is driven
|
|
87
|
+
Then the process rests at "build.review.await-review"
|
|
88
|
+
And ".gtd/REVIEW.md" contains:
|
|
89
|
+
"""
|
|
90
|
+
## sum is untested
|
|
91
|
+
|
|
92
|
+
_From lens correctness._
|
|
93
|
+
|
|
94
|
+
From correctness.
|
|
95
|
+
|
|
96
|
+
## sum is untested (2)
|
|
97
|
+
|
|
98
|
+
_From lens tests._
|
|
99
|
+
|
|
100
|
+
From tests.
|
|
101
|
+
"""
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: fix's quality lap is triaged, and its leftovers go to the hand-off review
|
|
3
|
+
|
|
4
|
+
A judge sorts the lap's findings: mechanical ones are fixed for up to three
|
|
5
|
+
turns, noise is dropped, and design ones — with whatever the fix turns left —
|
|
6
|
+
are added to the reviewer's `.gtd/REVIEW.md` by gtd, never to a person's
|
|
7
|
+
gate before the review.
|
|
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 a file "src/calc.ts" with:
|
|
13
|
+
"""
|
|
14
|
+
export const add = (a: number, b: number) => a + b
|
|
15
|
+
"""
|
|
16
|
+
And the working tree is committed as "feat: add calculator"
|
|
17
|
+
And a file ".gtd/REVIEW.orig.md" with:
|
|
18
|
+
"""
|
|
19
|
+
# Review: abc1234
|
|
20
|
+
|
|
21
|
+
<!-- base: 0000000 -->
|
|
22
|
+
|
|
23
|
+
## calc
|
|
24
|
+
|
|
25
|
+
- [ ] ./src/calc.ts#1-1
|
|
26
|
+
new add function
|
|
27
|
+
"""
|
|
28
|
+
And a file ".gtd/REVIEW.md" with:
|
|
29
|
+
"""
|
|
30
|
+
# Review: abc1234
|
|
31
|
+
|
|
32
|
+
<!-- base: 0000000 -->
|
|
33
|
+
|
|
34
|
+
## calc
|
|
35
|
+
|
|
36
|
+
- [ ] ./src/calc.ts#1-1
|
|
37
|
+
new add function — rename it to sum
|
|
38
|
+
"""
|
|
39
|
+
And the working tree is committed as "chore: hand off a review"
|
|
40
|
+
And step "notes.fixing" writes "src/calc.ts" with:
|
|
41
|
+
"""
|
|
42
|
+
export const sum = (a: number, b: number) => a + b
|
|
43
|
+
"""
|
|
44
|
+
And step "notes.closing" runs its script
|
|
45
|
+
And step "build.health.check" changes nothing
|
|
46
|
+
And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
|
|
47
|
+
"""
|
|
48
|
+
# Quality
|
|
49
|
+
|
|
50
|
+
## sum has no negative test
|
|
51
|
+
|
|
52
|
+
Add a test summing negative numbers.
|
|
53
|
+
|
|
54
|
+
## sum should be named plus
|
|
55
|
+
|
|
56
|
+
The glossary says plus.
|
|
57
|
+
"""
|
|
58
|
+
And step "build.quality.sorting" runs its script
|
|
59
|
+
And step "build.quality.closing" runs its script
|
|
60
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
61
|
+
"""
|
|
62
|
+
# Review: abc1234
|
|
63
|
+
|
|
64
|
+
<!-- base: 0000000 -->
|
|
65
|
+
|
|
66
|
+
## calc
|
|
67
|
+
|
|
68
|
+
- [ ] ./src/calc.ts#1-1
|
|
69
|
+
renamed to sum
|
|
70
|
+
"""
|
|
71
|
+
And step "build.review.carrying" runs its script
|
|
72
|
+
|
|
73
|
+
Scenario: a design finding and one the fix cap left reach the reviewer, never an escalation
|
|
74
|
+
Given judge step "build.quality.triage" answers:
|
|
75
|
+
"""
|
|
76
|
+
[{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"design","p":0.9}]
|
|
77
|
+
"""
|
|
78
|
+
And step "build.fix.quality.fixing" changes nothing
|
|
79
|
+
When gtd opens door "fix"
|
|
80
|
+
And the process is driven
|
|
81
|
+
Then the process rests at "build.review.await-review"
|
|
82
|
+
And ".gtd/QUALITY.md" does not exist
|
|
83
|
+
And no commit body contains "build.health.describe"
|
|
84
|
+
And ".gtd/REVIEW.md" contains:
|
|
85
|
+
"""
|
|
86
|
+
## sum has no negative test
|
|
87
|
+
|
|
88
|
+
_From lens correctness._
|
|
89
|
+
|
|
90
|
+
_The fixer left this unresolved._
|
|
91
|
+
"""
|
|
92
|
+
And ".gtd/REVIEW.md" contains:
|
|
93
|
+
"""
|
|
94
|
+
## sum should be named plus
|
|
95
|
+
|
|
96
|
+
_From lens correctness._
|
|
97
|
+
|
|
98
|
+
The glossary says plus.
|
|
99
|
+
"""
|
|
100
|
+
And the commit subjects end with:
|
|
101
|
+
"""
|
|
102
|
+
gtd(agent): build.quality.correctness.reviewing → build.quality.triage
|
|
103
|
+
gtd(judge): build.quality.triage → build.quality.sorting
|
|
104
|
+
gtd(check): build.quality.sorting → build.fix.quality.fixing
|
|
105
|
+
gtd(agent): build.fix.quality.fixing
|
|
106
|
+
gtd(agent): build.fix.quality.fixing
|
|
107
|
+
gtd(agent): build.fix.quality.fixing → build.health.check
|
|
108
|
+
gtd(check): build.health.check → build.quality.closing
|
|
109
|
+
gtd(check): build.quality.closing → build.review.reviewing
|
|
110
|
+
gtd(agent): build.review.reviewing → build.review.carrying
|
|
111
|
+
gtd(check): build.review.carrying → build.review.await-review
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
Scenario: a fixed finding and a noise one are listed in the review
|
|
115
|
+
Given judge step "build.quality.triage" answers:
|
|
116
|
+
"""
|
|
117
|
+
[{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"noise","p":0.9}]
|
|
118
|
+
"""
|
|
119
|
+
And step "build.fix.quality.fixing" writes "src/calc.test.ts" with:
|
|
120
|
+
"""
|
|
121
|
+
it("sums negative numbers", () => {})
|
|
122
|
+
"""
|
|
123
|
+
And step "build.fix.quality.fixing" deletes ".gtd/QUALITY.md"
|
|
124
|
+
When gtd opens door "fix"
|
|
125
|
+
And the process is driven
|
|
126
|
+
Then the process rests at "build.review.await-review"
|
|
127
|
+
And no commit body contains "build.quality.closing"
|
|
128
|
+
And ".gtd/REVIEW.md" contains "## Fixed automatically"
|
|
129
|
+
And ".gtd/REVIEW.md" contains "- sum has no negative test ("
|
|
130
|
+
And ".gtd/REVIEW.md" contains the short hash of the commit "gtd(agent): build.fix.quality.fixing → build.health.check"
|
|
131
|
+
And ".gtd/REVIEW.md" contains "- sum should be named plus (p=0.9, lens correctness)"
|
package/src/workflows/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { FIX_CAP, guarded, healthy, type EscalationCount, type Guard } from "./health.js"
|
|
16
16
|
import { fixCheck, fixQuality, QUALITY } from "./steps.js"
|
|
17
17
|
import * as t from "./text.js"
|
|
18
|
+
import type { Origin } from "./text.js"
|
|
18
19
|
|
|
19
20
|
/** One `## ` section of `.gtd/QUALITY.md`. */
|
|
20
21
|
export interface Finding {
|
|
@@ -29,13 +30,30 @@ type FindingKind = "mechanical" | "noise" | "design"
|
|
|
29
30
|
/** What the triaged quality lap did with its findings, for the review to list. */
|
|
30
31
|
export interface LensReport {
|
|
31
32
|
/** Design findings, then the mechanical ones the fix cap left. */
|
|
32
|
-
readonly notes: readonly (Finding &
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
readonly notes: readonly (Finding &
|
|
34
|
+
Origin & {
|
|
35
|
+
readonly unresolved?: true
|
|
36
|
+
readonly unclear?: true
|
|
37
|
+
})[]
|
|
38
|
+
readonly fixed: readonly (Finding & Origin & { readonly commit: string })[]
|
|
39
|
+
readonly noise: readonly (Finding & Origin & { readonly p: number })[]
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
export const NO_FINDINGS: LensReport = { notes: [], fixed: [], noise: [] }
|
|
38
43
|
|
|
44
|
+
/** `reports` as one, each finding labelled with the slice it came from, in slice order. */
|
|
45
|
+
export const joinReports = (
|
|
46
|
+
reports: readonly { readonly slice: string; readonly report: LensReport }[],
|
|
47
|
+
): LensReport => {
|
|
48
|
+
const tag = <F extends Origin>(slice: string, fs: readonly F[]): F[] =>
|
|
49
|
+
fs.map((f) => ({ ...f, slice }))
|
|
50
|
+
return {
|
|
51
|
+
notes: reports.flatMap((r) => tag(r.slice, r.report.notes)),
|
|
52
|
+
fixed: reports.flatMap((r) => tag(r.slice, r.report.fixed)),
|
|
53
|
+
noise: reports.flatMap((r) => tag(r.slice, r.report.noise)),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
const UNTITLED = "Quality findings"
|
|
40
58
|
|
|
41
59
|
const below = (section: string): string => section.split("\n").slice(1).join("\n").trim()
|
|
@@ -129,6 +147,8 @@ export interface TriageOptions {
|
|
|
129
147
|
readonly guard?: Guard | undefined
|
|
130
148
|
readonly guardRests?: boolean | undefined
|
|
131
149
|
readonly rules?: string | undefined
|
|
150
|
+
/** The lens that wrote a finding, when known; asked once per finding, in file order. */
|
|
151
|
+
readonly lensOf?: ((f: Finding) => string | undefined) | undefined
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
/**
|
|
@@ -139,7 +159,11 @@ export interface TriageOptions {
|
|
|
139
159
|
*/
|
|
140
160
|
export const triageFindings = async (options: TriageOptions): Promise<LensReport> => {
|
|
141
161
|
const { escalations, guard, guardRests, rules } = options
|
|
142
|
-
const
|
|
162
|
+
const withLens = (f: Finding): Finding & Origin => {
|
|
163
|
+
const lens = options.lensOf?.(f)
|
|
164
|
+
return lens === undefined ? f : { ...f, lens }
|
|
165
|
+
}
|
|
166
|
+
const findings = findingsOf(read(QUALITY) ?? "").map(withLens)
|
|
143
167
|
if (findings.length === 0) return NO_FINDINGS
|
|
144
168
|
const { mechanical, noise, design } = await classify(findings)
|
|
145
169
|
// Rewritten whenever it is not one chunk per mechanical finding: the fixer deletes chunks.
|
|
@@ -160,7 +184,8 @@ export const triageFindings = async (options: TriageOptions): Promise<LensReport
|
|
|
160
184
|
const commit = head()
|
|
161
185
|
fixed.push(...turn.fixed.map((f) => ({ ...f, commit })))
|
|
162
186
|
unclear.push(...turn.unclear)
|
|
163
|
-
|
|
187
|
+
// A turn that renamed a chunk hands back fresh parses: keep the lens a same-titled finding had.
|
|
188
|
+
open = turn.open.map((f) => ({ ...mechanical.find((m) => m.title === f.title), ...f }))
|
|
164
189
|
}
|
|
165
190
|
if (mechanical.length > 0)
|
|
166
191
|
await healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules })
|
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
fixCheck,
|
|
27
27
|
fixNits,
|
|
28
28
|
fixRisks,
|
|
29
|
-
QUALITY,
|
|
30
29
|
REQUIREMENTS,
|
|
31
30
|
REVIEW,
|
|
32
31
|
reviewMissing,
|
|
@@ -173,7 +172,7 @@ export const isCode = (path: string): boolean => path !== ".gtd" && !path.starts
|
|
|
173
172
|
|
|
174
173
|
/**
|
|
175
174
|
* gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
|
|
176
|
-
* turn
|
|
175
|
+
* turn. A re-review
|
|
177
176
|
* after fixed nits restores the doc as committed at `carry`, answers kept;
|
|
178
177
|
* a fresh one leaves out the notes the human already answered.
|
|
179
178
|
*/
|
|
@@ -199,7 +198,6 @@ const composing = (
|
|
|
199
198
|
undecided: parseStandards(read(STANDARDS) ?? "").undecided,
|
|
200
199
|
},
|
|
201
200
|
}),
|
|
202
|
-
[QUALITY],
|
|
203
201
|
)
|
|
204
202
|
: showScript(carry, REVIEW, REVIEW),
|
|
205
203
|
{ label: "Composing the review", file: REVIEW, base },
|
|
@@ -305,7 +303,7 @@ export interface BuildTailOptions extends TailOptions {
|
|
|
305
303
|
readonly report?: LensReport | undefined
|
|
306
304
|
}
|
|
307
305
|
|
|
308
|
-
/** The build tail: fix (when entered red), keep green unless `green`, the
|
|
306
|
+
/** The build tail: fix (when entered red), keep green unless `green`, the quality lap, then human review since `base`. */
|
|
309
307
|
export function buildTail(base: string, options?: BuildTailOptions): Promise<kit.ReviewOutcome>
|
|
310
308
|
/** @deprecated Pass `buildTail(base, { fixFirst, green, ...options })`. */
|
|
311
309
|
export function buildTail(
|
|
@@ -329,10 +327,7 @@ export function buildTail(
|
|
|
329
327
|
)
|
|
330
328
|
}
|
|
331
329
|
const [base, form] = options
|
|
332
|
-
return tail((escalations, report) => humanReview(base, escalations, { ...form, report }),
|
|
333
|
-
...form,
|
|
334
|
-
triage: true,
|
|
335
|
-
})
|
|
330
|
+
return tail((escalations, report) => humanReview(base, escalations, { ...form, report }), form)
|
|
336
331
|
}
|
|
337
332
|
|
|
338
333
|
const isOptionalBoolean = (value: unknown): value is boolean | null | undefined =>
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { read, refuse, removeScript, run, scope, showScript, vars } from "../../flows/index.js"
|
|
2
|
-
import * as kit from "../../workflow-kit/index.js"
|
|
3
1
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
read,
|
|
3
|
+
refuse,
|
|
4
|
+
removeScript,
|
|
5
|
+
run,
|
|
6
|
+
scope,
|
|
7
|
+
showScript,
|
|
8
|
+
vars,
|
|
9
|
+
writeScript,
|
|
10
|
+
} from "../../flows/index.js"
|
|
11
|
+
import * as kit from "../../workflow-kit/index.js"
|
|
12
|
+
import { guarded, healthy, type EscalationCount, type Guard } from "./health.js"
|
|
11
13
|
import {
|
|
12
14
|
awaitReview,
|
|
13
15
|
fixCheck,
|
|
14
|
-
fixQuality,
|
|
15
16
|
fixRisks,
|
|
16
17
|
QUALITY,
|
|
17
18
|
REVIEW,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
reviewMissing,
|
|
20
21
|
reviewQuality,
|
|
21
22
|
} from "./steps.js"
|
|
22
|
-
import { NO_FINDINGS, triageFindings, type LensReport } from "./lenses.js"
|
|
23
|
+
import { findingsOf, NO_FINDINGS, triageFindings, type Finding, type LensReport } from "./lenses.js"
|
|
23
24
|
import * as t from "./text.js"
|
|
24
25
|
|
|
25
26
|
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
@@ -34,29 +35,30 @@ export const qualityLap = (
|
|
|
34
35
|
guard?: Guard,
|
|
35
36
|
lenses?: readonly string[],
|
|
36
37
|
base?: string,
|
|
38
|
+
): Promise<"clean" | "findings"> => lensLap(guard, lenses, base, () => {})
|
|
39
|
+
|
|
40
|
+
// `seen` gets each lens and every finding in the file once its turn is done.
|
|
41
|
+
const lensLap = (
|
|
42
|
+
guard: Guard | undefined,
|
|
43
|
+
lenses: readonly string[] | undefined,
|
|
44
|
+
base: string | undefined,
|
|
45
|
+
seen: (lens: string, findings: readonly Finding[]) => void,
|
|
37
46
|
): Promise<"clean" | "findings"> =>
|
|
38
47
|
kit.qualityLap(
|
|
39
48
|
lenses ?? kit.lensEntriesOf(vars.qualityReviews ?? ""),
|
|
40
|
-
guarded((entry: string) => {
|
|
49
|
+
guarded(async (entry: string) => {
|
|
41
50
|
const { lens, model } = kit.lensOf(entry)
|
|
42
|
-
|
|
51
|
+
await reviewQuality(lens, base, model)
|
|
52
|
+
seen(lens, findingsOf(read(QUALITY) ?? ""))
|
|
43
53
|
}, guard),
|
|
44
54
|
QUALITY,
|
|
45
55
|
)
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
rules?: string,
|
|
53
|
-
): Promise<boolean> =>
|
|
54
|
-
kit.fixFindings({
|
|
55
|
-
fix: guarded(() => fixQuality(rules), guard),
|
|
56
|
-
findings: QUALITY,
|
|
57
|
-
cap: FIX_CAP,
|
|
58
|
-
escalate: () => escalation(escalations, guard, guardRests, rules),
|
|
59
|
-
})
|
|
57
|
+
// What the fixer left is in the lap's report by now; the next lap must not append to it.
|
|
58
|
+
const closeQuality = async (): Promise<void> => {
|
|
59
|
+
if (read(QUALITY) === undefined) return
|
|
60
|
+
await run("quality.closing", removeScript([QUALITY]), { label: "Clearing quality findings" })
|
|
61
|
+
}
|
|
60
62
|
|
|
61
63
|
/** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
|
|
62
64
|
export const closeReview = (round: string): Promise<void> =>
|
|
@@ -89,17 +91,12 @@ export interface TailOptions {
|
|
|
89
91
|
readonly lensBase?: string | undefined
|
|
90
92
|
/** Appended to every fix and describe prompt of the tail. */
|
|
91
93
|
readonly rules?: string | undefined
|
|
92
|
-
/**
|
|
93
|
-
* Judge the lap's findings: fix the mechanical ones, drop noise, and hand
|
|
94
|
-
* `finish` the rest as notes for the human review; default `false`. `finish`
|
|
95
|
-
* must remove `.gtd/QUALITY.md`, which still holds what the fixer left.
|
|
96
|
-
*/
|
|
97
|
-
readonly triage?: boolean | undefined
|
|
98
94
|
}
|
|
99
95
|
|
|
100
96
|
/**
|
|
101
|
-
* The build tail: fix (when entered red), keep green, the quality lap
|
|
102
|
-
*
|
|
97
|
+
* The build tail: fix (when entered red), keep green, the quality lap — a
|
|
98
|
+
* judge sorts its findings, the mechanical ones are fixed, noise dropped —
|
|
99
|
+
* then `finish` with the rest as notes: a human review, or carrying them on.
|
|
103
100
|
*/
|
|
104
101
|
export function tail<T>(
|
|
105
102
|
finish: (escalations: EscalationCount, report: LensReport) => Promise<T>,
|
|
@@ -108,7 +105,7 @@ export function tail<T>(
|
|
|
108
105
|
/** @deprecated Pass `tail(finish, { fixFirst, green, guard, lenses, guardRests, lensBase, rules })`. */
|
|
109
106
|
export function tail<T>(
|
|
110
107
|
fixFirst: boolean,
|
|
111
|
-
finish: (escalations: EscalationCount) => Promise<T>,
|
|
108
|
+
finish: (escalations: EscalationCount, report: LensReport) => Promise<T>,
|
|
112
109
|
green?: boolean,
|
|
113
110
|
guard?: Guard,
|
|
114
111
|
lenses?: readonly string[],
|
|
@@ -164,29 +161,34 @@ const runTail = <T>(finish: Finish<T>, options: TailOptions): Promise<T> =>
|
|
|
164
161
|
const guardRests = options.guardRests === undefined ? true : options.guardRests
|
|
165
162
|
const escalations: EscalationCount = { rounds: 0 }
|
|
166
163
|
const fix = () => fixCheck(rules)
|
|
167
|
-
const repair = async () => {
|
|
168
|
-
await guarded(fix, guard)()
|
|
169
|
-
await healthy(fix, { fixesSoFar: 1, escalations, guard, guardRests, rules })
|
|
170
|
-
}
|
|
171
164
|
// fixFirst's own fix + healthy below already is the full run.
|
|
172
165
|
// Truthy, not `true`: an untyped config's flags read as they always did.
|
|
173
|
-
if (options.fixFirst)
|
|
174
|
-
|
|
166
|
+
if (options.fixFirst) {
|
|
167
|
+
await guarded(fix, guard)()
|
|
168
|
+
await healthy(fix, { fixesSoFar: 1, escalations, guard, guardRests, rules })
|
|
169
|
+
} else if (!options.green) await healthy(fix, { escalations, guard, guardRests, rules })
|
|
175
170
|
// The lap runs once a tail: after its findings are fixed, `finish` follows.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
171
|
+
// Lenses append, so a title the file did not hold before a lens's turn is that lens's.
|
|
172
|
+
const byTitle: { title: string; lens: string }[] = []
|
|
173
|
+
const lap = await lensLap(guard, lenses, lensBase, (lens, findings) => {
|
|
174
|
+
const before = byTitle.map((f) => f.title)
|
|
175
|
+
for (const { title } of findings) {
|
|
176
|
+
const at = before.indexOf(title)
|
|
177
|
+
if (at >= 0) before.splice(at, 1)
|
|
178
|
+
else byTitle.push({ title, lens })
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
// Asked once per finding in file order, so a title two lenses wrote goes to each in turn.
|
|
182
|
+
const lensOf = (f: Finding): string | undefined => {
|
|
183
|
+
const at = byTitle.findIndex((e) => e.title === f.title)
|
|
184
|
+
return at < 0 ? undefined : byTitle.splice(at, 1)[0]!.lens
|
|
188
185
|
}
|
|
189
|
-
|
|
186
|
+
const report =
|
|
187
|
+
lap === "findings"
|
|
188
|
+
? await triageFindings({ escalations, guard, guardRests, rules, lensOf })
|
|
189
|
+
: NO_FINDINGS
|
|
190
|
+
await closeQuality()
|
|
191
|
+
return finish(escalations, report)
|
|
190
192
|
})
|
|
191
193
|
|
|
192
194
|
/**
|
|
@@ -198,10 +200,22 @@ export const handOffReview = (
|
|
|
198
200
|
base: string,
|
|
199
201
|
escalations: EscalationCount,
|
|
200
202
|
guard?: Guard,
|
|
203
|
+
report: LensReport = NO_FINDINGS,
|
|
201
204
|
): Promise<void> =>
|
|
202
205
|
kit.reviewToHandOff(base, {
|
|
203
206
|
file: REVIEW,
|
|
204
|
-
reviewing: guarded((b, carry) =>
|
|
207
|
+
reviewing: guarded(async (b, carry) => {
|
|
208
|
+
await reviewing(b, carry)
|
|
209
|
+
// gtd adds the lap's chunks itself: .gtd/QUALITY.md is gone, so a reviewer leaving one out would lose it.
|
|
210
|
+
const review = read(REVIEW)
|
|
211
|
+
if (review === undefined) return
|
|
212
|
+
const full = t.withLapFindings(review, report)
|
|
213
|
+
if (full !== review) {
|
|
214
|
+
await run("review.carrying", writeScript(REVIEW, full), {
|
|
215
|
+
label: "Adding the quality lap's findings",
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
}, guard),
|
|
205
219
|
fixRisks: guarded(fixRisks, guard),
|
|
206
220
|
keepGreen: () => healthy(fixCheck, { escalations, guard }),
|
|
207
221
|
awaitReview: (b) => awaitReview(b, t.handOffAwaitReviewMessage(b)),
|
|
@@ -220,6 +234,9 @@ export const handOffReview = (
|
|
|
220
234
|
"gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix.",
|
|
221
235
|
})
|
|
222
236
|
|
|
223
|
-
/** The tail the fix workflow runs: ends at the hand-off review since `base
|
|
237
|
+
/** The tail the fix workflow runs: ends at the hand-off review since `base`, which gets what the lap left. */
|
|
224
238
|
export const handOffTail = (fixFirst: boolean, base: string, guard?: Guard): Promise<void> =>
|
|
225
|
-
tail((escalations) => handOffReview(base, escalations, guard), {
|
|
239
|
+
tail((escalations, report) => handOffReview(base, escalations, guard, report), {
|
|
240
|
+
fixFirst,
|
|
241
|
+
guard,
|
|
242
|
+
})
|