@pmelab/gtd 45.2.0 → 47.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 +15 -30
- package/dist/chunks/{reserved-FWrjy7zo.mjs → reserved-DF1HD6jL.mjs} +42 -48
- package/dist/chunks/{review-Dhl4vvDe.mjs → review-Bv40kZu9.mjs} +9 -23
- package/dist/chunks/{tail-B-Es6Hwj.mjs → tail-D89fU_wf.mjs} +68 -39
- package/dist/gtd.bundle.mjs +5 -7
- package/dist/testkit.mjs +5 -7
- 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 +14 -4
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +270 -429
- package/package.json +1 -6
- package/src/install/ConfigScaffold.ts +1 -2
- package/src/install/Install.ts +4 -5
- 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 +1 -3
- package/src/workflows/review/review.ts +15 -3
- package/src/workflows/shared/lenses.ts +30 -9
- package/src/workflows/shared/options.ts +1 -21
- package/src/workflows/shared/review.ts +5 -27
- package/src/workflows/shared/skills.test.ts +0 -2
- package/src/workflows/shared/skills.ts +0 -3
- package/src/workflows/shared/tail.ts +74 -57
- package/src/workflows/shared/text.ts +86 -33
- package/src/workflows/shared/vars.ts +0 -8
- package/dist/workflows/tests.mjs +0 -193
- package/src/workflows/feature/quality-unresolved.feature +0 -121
- package/src/workflows/tests/index.ts +0 -6
- package/src/workflows/tests/tests-options.feature +0 -52
- package/src/workflows/tests/tests.feature +0 -266
- package/src/workflows/tests/tests.test.ts +0 -51
- package/src/workflows/tests/tests.ts +0 -270
package/src/workflows/index.ts
CHANGED
|
@@ -5,13 +5,12 @@ import { feature } from "./feature/feature.js"
|
|
|
5
5
|
|
|
6
6
|
export { threads, type ThreadInfo } from "../flows/index.js"
|
|
7
7
|
export * from "./shared/reserved.js"
|
|
8
|
-
export { FlagOptions, ShippedOptions
|
|
8
|
+
export { FlagOptions, ShippedOptions } from "./shared/options.js"
|
|
9
9
|
export { agentWithSkills, type ReviewRange } from "./shared/text.js"
|
|
10
10
|
export * from "./shared/steps.js"
|
|
11
11
|
export * from "./shared/health.js"
|
|
12
12
|
export {
|
|
13
13
|
closeReview,
|
|
14
|
-
fixQualityFindings,
|
|
15
14
|
handOffReview,
|
|
16
15
|
handOffTail,
|
|
17
16
|
qualityLap,
|
|
@@ -38,7 +37,6 @@ export { base, review, reviewFlow } from "./review/review.js"
|
|
|
38
37
|
export { fix, fixFlow, fixReviewNotes } from "./fix/fix.js"
|
|
39
38
|
export { rollout, rolloutFlow } from "./rollout/rollout.js"
|
|
40
39
|
export { cleanup, cleanupFlow } from "./cleanup/cleanup.js"
|
|
41
|
-
export { tests, testsFlow } from "./tests/tests.js"
|
|
42
40
|
|
|
43
41
|
export default feature
|
|
44
42
|
|
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import { read, refuse, start, type WorkflowBase } from "../../flows/index.js"
|
|
2
|
-
import { buildTail,
|
|
1
|
+
import { read, refuse, removeScript, run, start, type WorkflowBase } from "../../flows/index.js"
|
|
2
|
+
import { buildTail, reUnwind } from "../shared/review.js"
|
|
3
3
|
import { gate } from "../shared/health.js"
|
|
4
4
|
import { shipped } from "../shared/options.js"
|
|
5
|
-
import { REQUIREMENTS } from "../shared/steps.js"
|
|
5
|
+
import { coder, REQUIREMENTS } from "../shared/steps.js"
|
|
6
6
|
import { refusePendingHandOff } from "../shared/tail.js"
|
|
7
7
|
import * as t from "../shared/text.js"
|
|
8
8
|
|
|
9
|
+
const fixFeedback = async (): Promise<void> => {
|
|
10
|
+
await t.agentWithSkills("feedback.fixing", t.reviewFeedbackFixPrompt(), {
|
|
11
|
+
label: "Fixing your review feedback",
|
|
12
|
+
file: REQUIREMENTS,
|
|
13
|
+
model: coder(),
|
|
14
|
+
system: t.finisherSystem(),
|
|
15
|
+
// An earlier nit fix may already have built the concern.
|
|
16
|
+
allowEmpty: true,
|
|
17
|
+
})
|
|
18
|
+
await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" })
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
/**
|
|
10
22
|
* The feature's review of everything since `reviewBase` — a green gate, the
|
|
11
23
|
* triaged quality lap, then the human review — with no design phase: a
|
|
@@ -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,17 +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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
readonly fixed: readonly (Finding & { readonly commit: string })[]
|
|
38
|
-
readonly noise: readonly (Finding & { readonly p: number })[]
|
|
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 })[]
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export const NO_FINDINGS: LensReport = { notes: [], fixed: [], noise: [] }
|
|
42
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
|
+
|
|
43
57
|
const UNTITLED = "Quality findings"
|
|
44
58
|
|
|
45
59
|
const below = (section: string): string => section.split("\n").slice(1).join("\n").trim()
|
|
@@ -133,6 +147,8 @@ export interface TriageOptions {
|
|
|
133
147
|
readonly guard?: Guard | undefined
|
|
134
148
|
readonly guardRests?: boolean | undefined
|
|
135
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
|
|
136
152
|
}
|
|
137
153
|
|
|
138
154
|
/**
|
|
@@ -143,7 +159,11 @@ export interface TriageOptions {
|
|
|
143
159
|
*/
|
|
144
160
|
export const triageFindings = async (options: TriageOptions): Promise<LensReport> => {
|
|
145
161
|
const { escalations, guard, guardRests, rules } = options
|
|
146
|
-
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)
|
|
147
167
|
if (findings.length === 0) return NO_FINDINGS
|
|
148
168
|
const { mechanical, noise, design } = await classify(findings)
|
|
149
169
|
// Rewritten whenever it is not one chunk per mechanical finding: the fixer deletes chunks.
|
|
@@ -164,7 +184,8 @@ export const triageFindings = async (options: TriageOptions): Promise<LensReport
|
|
|
164
184
|
const commit = head()
|
|
165
185
|
fixed.push(...turn.fixed.map((f) => ({ ...f, commit })))
|
|
166
186
|
unclear.push(...turn.unclear)
|
|
167
|
-
|
|
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 }))
|
|
168
189
|
}
|
|
169
190
|
if (mechanical.length > 0)
|
|
170
191
|
await healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules })
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
scope,
|
|
10
10
|
vars,
|
|
11
11
|
} from "../../flows/index.js"
|
|
12
|
-
import { defaultFlagRegistry, defaults, envDefaults
|
|
12
|
+
import { defaultFlagRegistry, defaults, envDefaults } from "./vars.js"
|
|
13
13
|
|
|
14
14
|
const setting = Schema.optionalWith(Schema.String, { exact: true })
|
|
15
15
|
|
|
@@ -107,23 +107,3 @@ export const flagShipped = (
|
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
109
|
)
|
|
110
|
-
|
|
111
|
-
export const TestsOptions = Schema.Struct({
|
|
112
|
-
...ShippedOptions.fields,
|
|
113
|
-
testFiles: setting,
|
|
114
|
-
skipPattern: setting,
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
export type TestsOptions = Schema.Schema.Encoded<typeof TestsOptions>
|
|
118
|
-
|
|
119
|
-
export const testsShipped = (flow: Flow): WorkflowFactory<TestsOptions> =>
|
|
120
|
-
defineWorkflow(
|
|
121
|
-
Schema.standardSchemaV1(TestsOptions, { onExcessProperty: "error", errors: "all" }),
|
|
122
|
-
({ contextLimit, ...options }) => ({
|
|
123
|
-
flow: limited(flow, contextLimit),
|
|
124
|
-
defaults: over({ ...defaults, ...testsDefaults }, options),
|
|
125
|
-
envDefaults: over(envDefaults, options),
|
|
126
|
-
doors: { tests: {} },
|
|
127
|
-
base: undefined,
|
|
128
|
-
}),
|
|
129
|
-
)
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
judge,
|
|
6
6
|
numeric,
|
|
7
7
|
read,
|
|
8
|
-
removeScript,
|
|
9
8
|
requireRevert,
|
|
10
9
|
restoreScript,
|
|
11
10
|
run,
|
|
@@ -23,12 +22,10 @@ import { NO_FINDINGS, type LensReport } from "./lenses.js"
|
|
|
23
22
|
import {
|
|
24
23
|
answerReviewQuestions,
|
|
25
24
|
awaitReview,
|
|
26
|
-
coder,
|
|
27
25
|
collecting,
|
|
28
26
|
fixCheck,
|
|
29
27
|
fixNits,
|
|
30
28
|
fixRisks,
|
|
31
|
-
QUALITY,
|
|
32
29
|
REQUIREMENTS,
|
|
33
30
|
REVIEW,
|
|
34
31
|
reviewMissing,
|
|
@@ -175,7 +172,7 @@ export const isCode = (path: string): boolean => path !== ".gtd" && !path.starts
|
|
|
175
172
|
|
|
176
173
|
/**
|
|
177
174
|
* gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
|
|
178
|
-
* turn
|
|
175
|
+
* turn. A re-review
|
|
179
176
|
* after fixed nits restores the doc as committed at `carry`, answers kept;
|
|
180
177
|
* a fresh one leaves out the notes the human already answered.
|
|
181
178
|
*/
|
|
@@ -201,7 +198,6 @@ const composing = (
|
|
|
201
198
|
undecided: parseStandards(read(STANDARDS) ?? "").undecided,
|
|
202
199
|
},
|
|
203
200
|
}),
|
|
204
|
-
[QUALITY],
|
|
205
201
|
)
|
|
206
202
|
: showScript(carry, REVIEW, REVIEW),
|
|
207
203
|
{ label: "Composing the review", file: REVIEW, base },
|
|
@@ -220,7 +216,7 @@ export const humanReview = (
|
|
|
220
216
|
escalations: EscalationCount = { rounds: 0 },
|
|
221
217
|
options: Omit<BuildTailOptions, "fixFirst" | "green"> = {},
|
|
222
218
|
): Promise<kit.ReviewOutcome> => {
|
|
223
|
-
const { guard, guardRests, fixAccess, rules
|
|
219
|
+
const { guard, guardRests, fixAccess, rules } = options
|
|
224
220
|
const feedback: FeedbackTarget = options.feedback ?? {
|
|
225
221
|
file: REQUIREMENTS,
|
|
226
222
|
collecting,
|
|
@@ -243,7 +239,7 @@ export const humanReview = (
|
|
|
243
239
|
},
|
|
244
240
|
fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
|
|
245
241
|
keepGreen: () => healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules }),
|
|
246
|
-
awaitReview
|
|
242
|
+
awaitReview,
|
|
247
243
|
reviewMissing,
|
|
248
244
|
collecting: feedback.collecting,
|
|
249
245
|
triage: triage(severities),
|
|
@@ -305,11 +301,9 @@ export interface BuildTailOptions extends TailOptions {
|
|
|
305
301
|
readonly lap?: t.ReviewRange | undefined
|
|
306
302
|
/** What the triaged quality lap left for the review; absent: nothing. */
|
|
307
303
|
readonly report?: LensReport | undefined
|
|
308
|
-
/** The review gate's message for the review base; absent: the quality lap's. */
|
|
309
|
-
readonly awaitMessage?: ((base: string) => string) | undefined
|
|
310
304
|
}
|
|
311
305
|
|
|
312
|
-
/** 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`. */
|
|
313
307
|
export function buildTail(base: string, options?: BuildTailOptions): Promise<kit.ReviewOutcome>
|
|
314
308
|
/** @deprecated Pass `buildTail(base, { fixFirst, green, ...options })`. */
|
|
315
309
|
export function buildTail(
|
|
@@ -333,10 +327,7 @@ export function buildTail(
|
|
|
333
327
|
)
|
|
334
328
|
}
|
|
335
329
|
const [base, form] = options
|
|
336
|
-
return tail((escalations, report) => humanReview(base, escalations, { ...form, report }),
|
|
337
|
-
...form,
|
|
338
|
-
triage: true,
|
|
339
|
-
})
|
|
330
|
+
return tail((escalations, report) => humanReview(base, escalations, { ...form, report }), form)
|
|
340
331
|
}
|
|
341
332
|
|
|
342
333
|
const isOptionalBoolean = (value: unknown): value is boolean | null | undefined =>
|
|
@@ -372,19 +363,6 @@ const callForm = (
|
|
|
372
363
|
return undefined
|
|
373
364
|
}
|
|
374
365
|
|
|
375
|
-
/** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
|
|
376
|
-
export const fixFeedback = async (): Promise<void> => {
|
|
377
|
-
await t.agentWithSkills("feedback.fixing", t.reviewFeedbackFixPrompt(), {
|
|
378
|
-
label: "Fixing your review feedback",
|
|
379
|
-
file: REQUIREMENTS,
|
|
380
|
-
model: coder(),
|
|
381
|
-
system: t.finisherSystem(),
|
|
382
|
-
// An earlier nit fix may already have built the concern.
|
|
383
|
-
allowEmpty: true,
|
|
384
|
-
})
|
|
385
|
-
await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" })
|
|
386
|
-
}
|
|
387
|
-
|
|
388
366
|
/**
|
|
389
367
|
* Undo the human's review-round code edits. A path changed since the human
|
|
390
368
|
* left it is not overwritten; requireRevert names it instead.
|
|
@@ -24,9 +24,6 @@ export const skills = (
|
|
|
24
24
|
notes: ["incremental-implementation", "code-simplification"],
|
|
25
25
|
// `review`'s turn building what a review round collected.
|
|
26
26
|
feedback: ["test-driven-development", "incremental-implementation"],
|
|
27
|
-
// The `tests` workflow's sweep and rewrites.
|
|
28
|
-
tests: ["test-audit", "test-driven-development"],
|
|
29
|
-
"tests.health": ["debugging-and-error-recovery"],
|
|
30
27
|
// The refactor's own turns load `ponytail`; `refactor.build.*` carry the `build.*` lists.
|
|
31
28
|
...build,
|
|
32
29
|
...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
|
|
@@ -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
|
+
})
|
|
@@ -117,18 +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
|
-
|
|
129
|
-
}[]
|
|
130
|
-
readonly fixed: readonly { readonly title: string; readonly commit: string }[]
|
|
131
|
-
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 })[]
|
|
132
136
|
/** `Then` lines a `.feature` file at the review base no longer carries. */
|
|
133
137
|
readonly weakened?: readonly RemovedAssertions[]
|
|
134
138
|
/** The commit that fixed the human's nits, on the review that follows it. */
|
|
@@ -156,6 +160,34 @@ const unboxed = (body: string): string =>
|
|
|
156
160
|
|
|
157
161
|
const short = (hash: string): string => hash.slice(0, 7)
|
|
158
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
|
+
|
|
159
191
|
/**
|
|
160
192
|
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
|
|
161
193
|
* answer boxes, then the fixed and noise lists. Chunk titles are made unique —
|
|
@@ -172,8 +204,7 @@ export const composedReview = (base: string, report: ComposedReview): string =>
|
|
|
172
204
|
}
|
|
173
205
|
const chunk = (title: string, body: string): string => `## ${title}\n\n${body.trim()}`
|
|
174
206
|
// The lead-in line keeps a body that ends in a list from absorbing the boxes.
|
|
175
|
-
const answerBoxes =
|
|
176
|
-
`${ANSWER_LEAD}\n\n- [ ] ${fix}\n- [ ] ${wontFix}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`
|
|
207
|
+
const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${ANSWER_FIX}\n- [ ] ${ANSWER_WONT_FIX}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`
|
|
177
208
|
const conflicts = (report.standards?.undecided ?? []).map((c) =>
|
|
178
209
|
chunk(unique(conflictTitle(c)), conflictBody(c)),
|
|
179
210
|
)
|
|
@@ -203,23 +234,14 @@ export const composedReview = (base: string, report: ComposedReview): string =>
|
|
|
203
234
|
]),
|
|
204
235
|
...conflicts,
|
|
205
236
|
...notes.map((n) =>
|
|
206
|
-
chunk(
|
|
207
|
-
n.title,
|
|
208
|
-
`${
|
|
209
|
-
n.unresolved === true
|
|
210
|
-
? `_The fixer left this unresolved._\n\n${unboxed(n.body)}`
|
|
211
|
-
: n.unclear === true
|
|
212
|
-
? `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${unboxed(n.body)}`
|
|
213
|
-
: unboxed(n.body) || "_No detail given._"
|
|
214
|
-
}\n\n${answerBoxes(n.answers)}`,
|
|
215
|
-
),
|
|
237
|
+
chunk(n.title, `${originLine(carriedOnly(n))}${noteBody(n)}\n\n${answerBoxes}`),
|
|
216
238
|
),
|
|
217
239
|
...(report.fixed.length === 0
|
|
218
240
|
? []
|
|
219
241
|
: [
|
|
220
242
|
chunk(
|
|
221
243
|
"Fixed automatically",
|
|
222
|
-
report.fixed.map((f) =>
|
|
244
|
+
report.fixed.map((f) => fixedLine(f, carriedOnly(f))).join("\n"),
|
|
223
245
|
),
|
|
224
246
|
]),
|
|
225
247
|
...(added.length === 0
|
|
@@ -232,7 +254,12 @@ export const composedReview = (base: string, report: ComposedReview): string =>
|
|
|
232
254
|
]),
|
|
233
255
|
...(report.noise.length === 0
|
|
234
256
|
? []
|
|
235
|
-
: [
|
|
257
|
+
: [
|
|
258
|
+
chunk(
|
|
259
|
+
"Dropped as noise",
|
|
260
|
+
report.noise.map((f) => noiseLine(f, carriedOnly(f))).join("\n"),
|
|
261
|
+
),
|
|
262
|
+
]),
|
|
236
263
|
]
|
|
237
264
|
const body =
|
|
238
265
|
chunks.length > 0
|
|
@@ -246,19 +273,6 @@ export const composedReview = (base: string, report: ComposedReview): string =>
|
|
|
246
273
|
return `# Review: ${short(base)}\n\n<!-- base: ${base} -->\n\n${body.join("\n\n")}\n`
|
|
247
274
|
}
|
|
248
275
|
|
|
249
|
-
export const qualityUnresolvedMessage = (): string =>
|
|
250
|
-
`The quality lap's fix turns hit their cap and \`.gtd/QUALITY.md\` still
|
|
251
|
-
holds findings. Nothing after this fixes them, so the process will not
|
|
252
|
-
go on with them open.
|
|
253
|
-
|
|
254
|
-
Fix what matters by hand, then delete or empty \`.gtd/QUALITY.md\` —
|
|
255
|
-
dropping a finding is your call to make.
|
|
256
|
-
|
|
257
|
-
What each change does next (then run \`gtd land\`):
|
|
258
|
-
- **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**).
|
|
259
|
-
- **Anything else** — rests here again.
|
|
260
|
-
`
|
|
261
|
-
|
|
262
276
|
export const startGateBlockedMessage = (): string =>
|
|
263
277
|
`The test baseline is red — gtd will not start new work on a broken suite.
|
|
264
278
|
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
@@ -1128,6 +1142,45 @@ const lapRange: Record<ReviewRange, string> = {
|
|
|
1128
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.",
|
|
1129
1143
|
}
|
|
1130
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
|
+
|
|
1131
1184
|
export const buildReviewReviewingPrompt = (
|
|
1132
1185
|
base: string,
|
|
1133
1186
|
lap: ReviewRange,
|