@pmelab/gtd 55.0.0 → 56.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 +60 -61
- package/dist/chunks/{reserved-DHafcW7t.mjs → reserved-B8EC-WE8.mjs} +77 -203
- package/dist/chunks/{review-BJGKwlYv.mjs → review-HdrX0FJ7.mjs} +30 -125
- package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
- package/dist/chunks/{tail-B9Hw2AFB.mjs → tail-BSLah2mA.mjs} +144 -56
- package/dist/gtd.bundle.mjs +259 -22
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +6 -6
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +551 -66
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +14 -16
- 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 +522 -453
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +3 -3
- package/src/cli/Cli.ts +5 -5
- package/src/edge/Edge.test.ts +1 -1
- package/src/steering/FORMATS.md +5 -1
- package/src/steering/SteeringFormats.test.ts +2 -2
- package/src/steering/contract.test.ts +1 -0
- package/src/steering/findings.test.ts +47 -0
- package/src/steering/findings.ts +350 -0
- package/src/steering/fixtures/findings/gate.md +34 -0
- package/src/steering/fixtures/findings/gate.read.json +87 -0
- package/src/steering/index.test.ts +2 -2
- package/src/steering/index.ts +4 -3
- package/src/steering/review.test.ts +1 -75
- package/src/steering/review.ts +1 -74
- package/src/workflow/compile.test.ts +1 -0
- package/src/workflows/feature/criteria-coverage.feature +1 -6
- package/src/workflows/feature/feature.test.ts +1 -1
- package/src/workflows/feature/feature.ts +12 -20
- package/src/workflows/feature/lens-review.feature +323 -171
- package/src/workflows/feature/refactor-absorbs.feature +1 -7
- package/src/workflows/feature/refactor-first.feature +2 -10
- package/src/workflows/feature/refactor-rebuild.feature +4 -24
- package/src/workflows/feature/refactor-review.feature +33 -117
- package/src/workflows/feature/refactor.test.ts +0 -2
- package/src/workflows/feature/refactor.ts +0 -1
- package/src/workflows/feature/review-gate.feature +6 -22
- package/src/workflows/feature/reviewer-walkthrough.feature +16 -103
- package/src/workflows/feature/standards.feature +75 -34
- package/src/workflows/fix/fix-lens-origin.feature +20 -7
- package/src/workflows/fix/fix-quality.feature +102 -23
- package/src/workflows/review/review-door.feature +62 -59
- package/src/workflows/shared/learn.test.ts +0 -14
- package/src/workflows/shared/learn.ts +1 -6
- package/src/workflows/shared/lenses.ts +149 -43
- package/src/workflows/shared/review.test.ts +27 -26
- package/src/workflows/shared/review.ts +29 -196
- package/src/workflows/shared/tail.ts +18 -9
- package/src/workflows/shared/text.test.ts +103 -288
- package/src/workflows/shared/text.ts +131 -329
- package/src/workflows/feature/review-answers.feature +0 -324
- package/src/workflows/feature/severity-budget.feature +0 -116
- package/src/workflows/feature/severity-sorted-review.feature +0 -198
|
@@ -47,7 +47,7 @@ interface Drive {
|
|
|
47
47
|
readonly vars?: Readonly<Record<string, string>>
|
|
48
48
|
/** The REVIEW.md the human leaves; defaults to `notes` laid over the baseline. */
|
|
49
49
|
readonly after?: string
|
|
50
|
-
/** The REVIEW.md each `review.
|
|
50
|
+
/** The REVIEW.md each `review.reviewing` turn writes, in order; also raises the review-turn stop to one past the last. */
|
|
51
51
|
readonly reviewDocs?: readonly (string | undefined)[]
|
|
52
52
|
/** Let the run pass the second gate, stopping at the first range read after it. */
|
|
53
53
|
readonly passSecondGate?: boolean
|
|
@@ -94,7 +94,7 @@ const drive = async (d: Drive): Promise<Run> => {
|
|
|
94
94
|
let awaits = 0
|
|
95
95
|
let reviews = 0
|
|
96
96
|
const effects: Record<string, () => void> = {
|
|
97
|
-
"review.
|
|
97
|
+
"review.reviewing": () => {
|
|
98
98
|
const docs = d.reviewDocs
|
|
99
99
|
if (++reviews > (docs?.length ?? 0) + (docs === undefined ? 1 : 0)) throw new Stop()
|
|
100
100
|
const written = docs?.[reviews - 1]
|
|
@@ -184,10 +184,10 @@ describe("the risk-fix pass", () => {
|
|
|
184
184
|
it("a marked risk is fixed, kept green, re-reviewed, then rests at the gate", async () => {
|
|
185
185
|
const run = await drive({ notes: [], reviewDocs: [risky, clean] })
|
|
186
186
|
expect(run.log.slice(0, 5)).toEqual([
|
|
187
|
-
"review.
|
|
187
|
+
"review.reviewing",
|
|
188
188
|
"review.fix.risks.fixing",
|
|
189
189
|
"health.check",
|
|
190
|
-
"review.
|
|
190
|
+
"review.reviewing",
|
|
191
191
|
"review.await-review",
|
|
192
192
|
])
|
|
193
193
|
expect(run.prompts.get("review.fix.risks.fixing")).toContain("risk-1")
|
|
@@ -199,10 +199,10 @@ describe("the risk-fix pass", () => {
|
|
|
199
199
|
const run = await drive({ notes: [], reviewDocs: [risky, risky] })
|
|
200
200
|
expect(run.log.filter((n) => n === "review.fix.risks.fixing")).toHaveLength(1)
|
|
201
201
|
expect(run.log.slice(0, 5)).toEqual([
|
|
202
|
-
"review.
|
|
202
|
+
"review.reviewing",
|
|
203
203
|
"review.fix.risks.fixing",
|
|
204
204
|
"health.check",
|
|
205
|
-
"review.
|
|
205
|
+
"review.reviewing",
|
|
206
206
|
"review.await-review",
|
|
207
207
|
])
|
|
208
208
|
})
|
|
@@ -232,7 +232,7 @@ describe("the risk-fix pass", () => {
|
|
|
232
232
|
it("no marker means no fix-risks step", async () => {
|
|
233
233
|
const run = await drive({ notes: [], reviewDocs: [clean] })
|
|
234
234
|
expect(run.log).not.toContain("review.fix.risks.fixing")
|
|
235
|
-
expect(run.log[0]).toBe("review.
|
|
235
|
+
expect(run.log[0]).toBe("review.reviewing")
|
|
236
236
|
expect(run.log[1]).toBe("review.await-review")
|
|
237
237
|
})
|
|
238
238
|
|
|
@@ -243,16 +243,16 @@ describe("the risk-fix pass", () => {
|
|
|
243
243
|
reviewDocs: [undefined, risky, clean],
|
|
244
244
|
})
|
|
245
245
|
expect(run.log).toEqual([
|
|
246
|
-
"review.
|
|
246
|
+
"review.reviewing",
|
|
247
247
|
"review.await-review",
|
|
248
248
|
"review.triage",
|
|
249
249
|
"review.fix.nits.fixing",
|
|
250
250
|
"health.check",
|
|
251
251
|
"review.closing",
|
|
252
|
-
"review.
|
|
252
|
+
"review.reviewing",
|
|
253
253
|
"review.fix.risks.fixing",
|
|
254
254
|
"health.check",
|
|
255
|
-
"review.
|
|
255
|
+
"review.reviewing",
|
|
256
256
|
"review.await-review",
|
|
257
257
|
])
|
|
258
258
|
})
|
|
@@ -264,8 +264,9 @@ describe("the guard", () => {
|
|
|
264
264
|
|
|
265
265
|
it("runs right after the risk-fix turn", async () => {
|
|
266
266
|
const run = await drive({ notes: [], reviewDocs: [risky, clean], guarded: true })
|
|
267
|
-
expect(run.log.slice(0,
|
|
268
|
-
"review.
|
|
267
|
+
expect(run.log.slice(0, 5)).toEqual([
|
|
268
|
+
"review.reviewing",
|
|
269
|
+
"guard",
|
|
269
270
|
"review.fix.risks.fixing",
|
|
270
271
|
"guard",
|
|
271
272
|
"health.check",
|
|
@@ -278,7 +279,7 @@ describe("the guard", () => {
|
|
|
278
279
|
answers: { "note-1": verdict("nit") },
|
|
279
280
|
guarded: true,
|
|
280
281
|
})
|
|
281
|
-
expect(run.log.slice(
|
|
282
|
+
expect(run.log.slice(3, 7)).toEqual([
|
|
282
283
|
"review.triage",
|
|
283
284
|
"review.fix.nits.fixing",
|
|
284
285
|
"guard",
|
|
@@ -443,7 +444,7 @@ describe("review verdict routing", () => {
|
|
|
443
444
|
it("an unanswered note counts as edit", async () => {
|
|
444
445
|
const run = await drive({ notes: ["add — typo", "sub", "mul"], answers: {} })
|
|
445
446
|
expect(run.log).toEqual([
|
|
446
|
-
"review.
|
|
447
|
+
"review.reviewing",
|
|
447
448
|
"review.await-review",
|
|
448
449
|
"review.triage",
|
|
449
450
|
"review.closing",
|
|
@@ -478,7 +479,7 @@ describe("review verdict routing", () => {
|
|
|
478
479
|
it("only praise closes and signs off with no agent turn", async () => {
|
|
479
480
|
const run = await drive({ notes, answers: { "note-1": verdict("praise") } })
|
|
480
481
|
expect(run.log).toEqual([
|
|
481
|
-
"review.
|
|
482
|
+
"review.reviewing",
|
|
482
483
|
"review.await-review",
|
|
483
484
|
"review.triage",
|
|
484
485
|
"review.closing",
|
|
@@ -492,7 +493,7 @@ describe("review verdict routing", () => {
|
|
|
492
493
|
answers: { "note-1": verdict("question"), "note-2": verdict("question") },
|
|
493
494
|
})
|
|
494
495
|
expect(run.log).toEqual([
|
|
495
|
-
"review.
|
|
496
|
+
"review.reviewing",
|
|
496
497
|
"review.await-review",
|
|
497
498
|
"review.triage",
|
|
498
499
|
"review.answer-review-questions",
|
|
@@ -508,17 +509,17 @@ describe("review verdict routing", () => {
|
|
|
508
509
|
answers: { "note-1": verdict("question"), "note-2": verdict("nit") },
|
|
509
510
|
})
|
|
510
511
|
expect(run.log).toEqual([
|
|
511
|
-
"review.
|
|
512
|
+
"review.reviewing",
|
|
512
513
|
"review.await-review",
|
|
513
514
|
"review.triage",
|
|
514
515
|
"review.answer-review-questions",
|
|
515
516
|
"review.fix.nits.fixing",
|
|
516
517
|
"health.check",
|
|
517
518
|
"review.closing",
|
|
518
|
-
"review.
|
|
519
|
+
"review.reviewing",
|
|
519
520
|
])
|
|
520
521
|
expect(run.prompts.get("review.fix.nits.fixing")).not.toContain("why?")
|
|
521
|
-
expect(run.prompts.get("review.
|
|
522
|
+
expect(run.prompts.get("review.reviewing")).toContain("Run: git show c4")
|
|
522
523
|
expect(run.result).toBe("stopped")
|
|
523
524
|
})
|
|
524
525
|
|
|
@@ -527,8 +528,8 @@ describe("review verdict routing", () => {
|
|
|
527
528
|
notes: ["add — typo", "sub", "mul"],
|
|
528
529
|
answers: { "note-1": verdict("nit") },
|
|
529
530
|
})
|
|
530
|
-
expect(run.prompts.get("review.
|
|
531
|
-
expect(run.prompts.get("review.
|
|
531
|
+
expect(run.prompts.get("review.reviewing")).not.toContain("git show")
|
|
532
|
+
expect(run.prompts.get("review.reviewing")).toContain("worked on the human's nits")
|
|
532
533
|
})
|
|
533
534
|
|
|
534
535
|
it("edits run after questions and nits, and the capture names only the edits and the answering commit", async () => {
|
|
@@ -537,7 +538,7 @@ describe("review verdict routing", () => {
|
|
|
537
538
|
answers: { "note-2": verdict("question"), "note-3": verdict("nit") },
|
|
538
539
|
})
|
|
539
540
|
expect(run.log).toEqual([
|
|
540
|
-
"review.
|
|
541
|
+
"review.reviewing",
|
|
541
542
|
"review.await-review",
|
|
542
543
|
"review.triage",
|
|
543
544
|
"review.answer-review-questions",
|
|
@@ -557,7 +558,7 @@ describe("review verdict routing", () => {
|
|
|
557
558
|
it("a noted round with no extractable note collects as one edit, without judging", async () => {
|
|
558
559
|
const run = await drive({ notes: [], after: BEFORE + "\nSome stray prose.\n" })
|
|
559
560
|
expect(run.log).toEqual([
|
|
560
|
-
"review.
|
|
561
|
+
"review.reviewing",
|
|
561
562
|
"review.await-review",
|
|
562
563
|
"review.closing",
|
|
563
564
|
"review.collecting",
|
|
@@ -682,7 +683,7 @@ describe("severity routing", () => {
|
|
|
682
683
|
answers: { "note-1": verdict("edit"), ...sev("note-1", could) },
|
|
683
684
|
})
|
|
684
685
|
expect(run.log).toEqual([
|
|
685
|
-
"review.
|
|
686
|
+
"review.reviewing",
|
|
686
687
|
"review.await-review",
|
|
687
688
|
"review.triage",
|
|
688
689
|
"review.closing",
|
|
@@ -762,7 +763,7 @@ describe("BuildTailOptions.lensBase", () => {
|
|
|
762
763
|
if (request.kind === "restart") return Promise.resolve()
|
|
763
764
|
if (request.kind === "agent" && request.name.startsWith("quality."))
|
|
764
765
|
prompts.push(request.prompt)
|
|
765
|
-
if (request.name === "review.
|
|
766
|
+
if (request.name === "review.reviewing") throw new Stop()
|
|
766
767
|
return Promise.resolve()
|
|
767
768
|
},
|
|
768
769
|
pushScope: () => undefined,
|
|
@@ -1000,7 +1001,7 @@ describe("buildTail without rules", () => {
|
|
|
1000
1001
|
if (request.kind === "agent") prompts.set(request.name, request.prompt)
|
|
1001
1002
|
if (request.name === "health.check") checks++
|
|
1002
1003
|
if (request.name === "fix.quality.fixing") files.delete(".gtd/QUALITY.md")
|
|
1003
|
-
if (request.name === "review.
|
|
1004
|
+
if (request.name === "review.reviewing" || request.name === "health.stop")
|
|
1004
1005
|
throw new Stop()
|
|
1005
1006
|
return Promise.resolve(
|
|
1006
1007
|
request.kind === "judge"
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
requireRevert,
|
|
10
10
|
restoreScript,
|
|
11
11
|
run,
|
|
12
|
-
showScript,
|
|
13
12
|
vars,
|
|
14
13
|
writeScript,
|
|
15
14
|
type JudgeQuestion,
|
|
@@ -21,7 +20,6 @@ import {
|
|
|
21
20
|
reviewChunks,
|
|
22
21
|
reviewRisks,
|
|
23
22
|
reviewSections,
|
|
24
|
-
sectionedReview,
|
|
25
23
|
type ReviewNote,
|
|
26
24
|
} from "../../steering/index.js"
|
|
27
25
|
import * as kit from "../../workflow-kit/index.js"
|
|
@@ -195,214 +193,59 @@ const triage =
|
|
|
195
193
|
)
|
|
196
194
|
}
|
|
197
195
|
|
|
198
|
-
/** Review order: security, one-way doors, the judge unsure, then should, then could. */
|
|
199
|
-
const chunkKey = (s: Severity): number =>
|
|
200
|
-
s.tier === "could" ? 4 : s.tier === "should" ? 3 : s.security === true ? 0 : s.rank === 0 ? 1 : 2
|
|
201
|
-
|
|
202
|
-
type Pointer = ReturnType<typeof reviewChunks>[number]["files"][number]
|
|
203
|
-
|
|
204
|
-
/** A chunk's text, then the code each of its pointers covers: a bare path the whole file. */
|
|
205
|
-
const chunkEvidence = (text: string, files: readonly Pointer[]): string =>
|
|
206
|
-
[
|
|
207
|
-
text,
|
|
208
|
-
...files.map(({ path, line, rangeEnd }) => {
|
|
209
|
-
const where = `${path}${line === undefined ? "" : `#${line}${rangeEnd === undefined ? "" : `-${rangeEnd}`}`}`
|
|
210
|
-
const lines = read(path.replace(/^\.\//, ""))?.split("\n")
|
|
211
|
-
if (lines === undefined) return `${where}: no longer exists`
|
|
212
|
-
const code = line === undefined ? lines : lines.slice(line - 1, rangeEnd ?? line)
|
|
213
|
-
return `${where}:\n${code.join("\n")}`
|
|
214
|
-
}),
|
|
215
|
-
].join("\n\n")
|
|
216
|
-
|
|
217
|
-
const bytes = (text: string): number => new TextEncoder().encode(text).length
|
|
218
|
-
|
|
219
|
-
/** Fills each batch while the budget's even share still holds its widest evidence, so nothing is cut that fits alone. */
|
|
220
|
-
const batched = <T extends { readonly evidence: string }>(
|
|
221
|
-
items: readonly T[],
|
|
222
|
-
budget: number,
|
|
223
|
-
): T[][] => {
|
|
224
|
-
const batches: T[][] = []
|
|
225
|
-
let batch: T[] = []
|
|
226
|
-
let widest = 0
|
|
227
|
-
for (const item of items) {
|
|
228
|
-
const size = bytes(item.evidence)
|
|
229
|
-
if (batch.length > 0 && Math.floor(budget / (batch.length + 1)) < Math.max(widest, size)) {
|
|
230
|
-
batches.push(batch)
|
|
231
|
-
batch = []
|
|
232
|
-
widest = 0
|
|
233
|
-
}
|
|
234
|
-
batch.push(item)
|
|
235
|
-
widest = Math.max(widest, size)
|
|
236
|
-
}
|
|
237
|
-
return batch.length === 0 ? batches : [...batches, batch]
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Rates every chunk the reviewer wrote and sorts the \`findings\` among them
|
|
242
|
-
* first in \`full\`, each led by its tier and reason; a walkthrough chunk
|
|
243
|
-
* keeps its place and carries no tier. It is still rated, so a review with no
|
|
244
|
-
* findings still reaches \`review.ranking\` and older histories replay.
|
|
245
|
-
*/
|
|
246
|
-
const ranked = async (
|
|
247
|
-
review: string,
|
|
248
|
-
full: string,
|
|
249
|
-
findings: ReadonlySet<string>,
|
|
250
|
-
): Promise<string> => {
|
|
251
|
-
const texts = t.chunkTexts(full)
|
|
252
|
-
const chunks = reviewChunks(review).map((c, i) => ({
|
|
253
|
-
id: `C${i + 1}`,
|
|
254
|
-
title: c.title,
|
|
255
|
-
evidence: chunkEvidence(texts.get(c.title) ?? "", c.files),
|
|
256
|
-
}))
|
|
257
|
-
if (chunks.length === 0) return full
|
|
258
|
-
const minP = numeric(vars.reviewSeverityMinP, Infinity)
|
|
259
|
-
const batches = batched(chunks, numeric(vars.judgeBudgetBytes, 32768))
|
|
260
|
-
const severities = new Map<string, Severity>()
|
|
261
|
-
for (const [i, batch] of batches.entries()) {
|
|
262
|
-
const judgment = await judge("review.ranking", {
|
|
263
|
-
questions: batch.flatMap((c) =>
|
|
264
|
-
severityQuestions(
|
|
265
|
-
c.id,
|
|
266
|
-
`the change review chunk "${c.id}" ("${c.title}") walks through`,
|
|
267
|
-
`the change in review chunk "${c.id}"`,
|
|
268
|
-
),
|
|
269
|
-
),
|
|
270
|
-
evidence: Object.fromEntries(batch.map((c) => [c.id, c.evidence])),
|
|
271
|
-
message: t.reviewRankingMessage(),
|
|
272
|
-
label:
|
|
273
|
-
batches.length === 1
|
|
274
|
-
? "Ranking the review"
|
|
275
|
-
: `Ranking the review (${i + 1}/${batches.length})`,
|
|
276
|
-
})
|
|
277
|
-
for (const c of batch) severities.set(c.id, severityOf(c, judgment, minP))
|
|
278
|
-
}
|
|
279
|
-
return t.rankedReview(
|
|
280
|
-
full,
|
|
281
|
-
chunks
|
|
282
|
-
.map((c, i) => ({ c, i, s: severities.get(c.id)! }))
|
|
283
|
-
.filter(({ c }) => findings.has(c.title))
|
|
284
|
-
.sort((x, y) => chunkKey(x.s) - chunkKey(y.s) || x.i - y.i)
|
|
285
|
-
.map(({ c, s }) => ({ title: c.title, label: s.reason })),
|
|
286
|
-
)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
196
|
export const isCode = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
|
|
290
197
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
report: LensReport,
|
|
294
|
-
nitFix: string | undefined,
|
|
295
|
-
answered: ReadonlySet<string>,
|
|
296
|
-
): t.ComposedReview => ({
|
|
297
|
-
...report,
|
|
298
|
-
nitFix,
|
|
299
|
-
answered,
|
|
300
|
-
standards: {
|
|
301
|
-
added: standardsAddedSince(base),
|
|
302
|
-
undecided: parseStandards(read(STANDARDS) ?? "").undecided,
|
|
303
|
-
},
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
|
|
308
|
-
* turn. A re-review
|
|
309
|
-
* after fixed nits restores the doc as committed at `carry`, answers kept;
|
|
310
|
-
* a fresh one leaves out the notes the human already answered.
|
|
311
|
-
*/
|
|
312
|
-
const composing = (
|
|
313
|
-
base: string,
|
|
314
|
-
report: LensReport,
|
|
315
|
-
carry?: string,
|
|
316
|
-
nitFix?: string,
|
|
317
|
-
answered: ReadonlySet<string> = new Set(),
|
|
318
|
-
): Promise<void> =>
|
|
319
|
-
run(
|
|
320
|
-
"review.composing",
|
|
321
|
-
carry === undefined
|
|
322
|
-
? writeScript(
|
|
323
|
-
REVIEW,
|
|
324
|
-
t.composedReview(base, {
|
|
325
|
-
...composedOf(base, report, nitFix, answered),
|
|
326
|
-
weakened: kit.removedAssertions(changesSince(base)),
|
|
327
|
-
}),
|
|
328
|
-
)
|
|
329
|
-
: showScript(carry, REVIEW, REVIEW),
|
|
330
|
-
{ label: "Composing the review", file: REVIEW, base },
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Refuses a review with a chunk pointing at no code, or a lap note without a
|
|
335
|
-
* chunk of its own. A range with no code change has nothing to point at, so
|
|
336
|
-
* there only a note's chunk needs a pointer.
|
|
337
|
-
*/
|
|
338
|
-
const requirePointers = (
|
|
339
|
-
review: string,
|
|
340
|
-
notes: readonly { title: string }[],
|
|
341
|
-
codeChanged: boolean,
|
|
342
|
-
): void => {
|
|
198
|
+
/** Refuses a review with a chunk pointing at no code; a range with no code change has nothing to point at. */
|
|
199
|
+
const requirePointers = (review: string, codeChanged: boolean): void => {
|
|
343
200
|
const chunks = reviewChunks(review)
|
|
344
|
-
const titles = new Set(chunks.map((c) => c.title))
|
|
345
|
-
const noted = new Set(notes.map((n) => n.title))
|
|
346
201
|
const problems = [
|
|
347
202
|
...chunks
|
|
348
|
-
.filter((c) => c.pointers === 0 &&
|
|
203
|
+
.filter((c) => c.pointers === 0 && codeChanged)
|
|
349
204
|
.map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — points at no code`),
|
|
350
205
|
...chunks
|
|
351
206
|
.filter((c) => isConflictTitle(c.title))
|
|
352
207
|
.map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — gtd writes conflicts; leave it out`),
|
|
353
|
-
...notes.filter((n) => !titles.has(n.title)).map((n) => ` - ${n.title} — no chunk of its own`),
|
|
354
208
|
// Notes and answers are matched to their chunk by title.
|
|
355
209
|
...chunks
|
|
356
210
|
.filter((c, i) => chunks.findIndex((d) => d.title === c.title) !== i)
|
|
357
211
|
.map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — a second chunk with this title`),
|
|
358
212
|
...reviewSections(review).map(
|
|
359
213
|
(s) =>
|
|
360
|
-
` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} —
|
|
214
|
+
` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} — write \`## \` chunks only`,
|
|
361
215
|
),
|
|
362
|
-
// A risk is fixed before the human sees it; a note is the human's to decide.
|
|
363
|
-
...reviewRisks(review)
|
|
364
|
-
.filter((r) => [...noted].some((title) => r.anchor.startsWith(`${title} ./`)))
|
|
365
|
-
.map((r) => ` - ${r.anchor} — no \`Risk:\` in a quality finding's chunk`),
|
|
366
216
|
]
|
|
367
217
|
if (problems.length === 0) return
|
|
368
218
|
refuse(
|
|
369
|
-
`gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code
|
|
219
|
+
`gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code:\n${problems.join("\n")}`,
|
|
370
220
|
)
|
|
371
221
|
}
|
|
372
222
|
|
|
373
223
|
/**
|
|
374
224
|
* A reviewer walks the human through everything since `base`, every chunk
|
|
375
|
-
* pointing at its code
|
|
376
|
-
* then adds each note's text and answer boxes, and the summary chunks.
|
|
225
|
+
* pointing at its code; gtd then appends the summary chunks.
|
|
377
226
|
*/
|
|
378
227
|
const walkthrough = async (
|
|
379
228
|
base: string,
|
|
380
|
-
|
|
229
|
+
summary: t.ReviewSummary,
|
|
381
230
|
lap: t.ReviewRange,
|
|
382
231
|
carry: string | undefined,
|
|
232
|
+
nitFix: string | undefined,
|
|
383
233
|
afterRiskFix: boolean,
|
|
384
234
|
): Promise<void> => {
|
|
385
|
-
|
|
386
|
-
await reviewing(base, carry, lap, t.walkthroughBlock(notes, report.nitFix))
|
|
235
|
+
await reviewing(base, carry, lap, t.nitFixBlock(nitFix))
|
|
387
236
|
const written = read(REVIEW)
|
|
388
237
|
// A review the reviewer did not write is the round's own refusal or rest.
|
|
389
238
|
if (written === undefined) return
|
|
390
|
-
const review = t.unranked(written)
|
|
391
|
-
// Against the file as written, so a refusal's line numbers match it.
|
|
392
239
|
requirePointers(
|
|
393
240
|
written,
|
|
394
|
-
notes,
|
|
395
241
|
changesSince(base).some((c) => isCode(c.path)),
|
|
396
242
|
)
|
|
397
|
-
// Its risks are fixed and the review written again, so the re-reviewer edits this draft, never gtd's
|
|
243
|
+
// Its risks are fixed and the review written again, so the re-reviewer edits this draft, never gtd's additions.
|
|
398
244
|
if (!afterRiskFix && reviewRisks(written).length > 0) return
|
|
399
|
-
const full =
|
|
400
|
-
await ranked(review, t.withWalkthrough(review, report), new Set(notes.map((n) => n.title))),
|
|
401
|
-
new Set(reviewChunks(review).map((c) => c.title)),
|
|
402
|
-
)
|
|
245
|
+
const full = t.withSummary(written, summary)
|
|
403
246
|
if (full !== written) {
|
|
404
247
|
await run("review.carrying", writeScript(REVIEW, full), {
|
|
405
|
-
label: "
|
|
248
|
+
label: "Adding the quality lap's summary",
|
|
406
249
|
file: REVIEW,
|
|
407
250
|
base,
|
|
408
251
|
})
|
|
@@ -410,12 +253,10 @@ const walkthrough = async (
|
|
|
410
253
|
}
|
|
411
254
|
|
|
412
255
|
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* caller to build, questions are answered in place, nits fixed in one batch,
|
|
418
|
-
* praise dropped.
|
|
256
|
+
* A reviewer's walkthrough of everything since `base` reaches a human, who
|
|
257
|
+
* may comment: each note is judged — edits end the review as `feedback` for
|
|
258
|
+
* the caller to build, questions are answered in place, nits fixed in one
|
|
259
|
+
* batch, praise dropped.
|
|
419
260
|
*/
|
|
420
261
|
export const humanReview = (
|
|
421
262
|
base: string,
|
|
@@ -432,8 +273,6 @@ export const humanReview = (
|
|
|
432
273
|
const severities = new Map<string, Severity>()
|
|
433
274
|
// Set by a nit fix, so the fresh review that follows names its commit.
|
|
434
275
|
let nitFix: string | undefined
|
|
435
|
-
// Every note the human answered in this review, left out of a fresh one.
|
|
436
|
-
const answered = new Set<string>()
|
|
437
276
|
// Set by a risk fix, so the review written next reaches the human whatever it marks.
|
|
438
277
|
let riskFixed = false
|
|
439
278
|
return kit.review(base, {
|
|
@@ -444,20 +283,17 @@ export const humanReview = (
|
|
|
444
283
|
const fixedAt = nitFix
|
|
445
284
|
const afterRiskFix = riskFixed
|
|
446
285
|
riskFixed = false
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
guard,
|
|
459
|
-
)()
|
|
460
|
-
: composing(b, report, carry, fixedAt, answered)
|
|
286
|
+
const summary: t.ReviewSummary = {
|
|
287
|
+
report: options.report ?? NO_FINDINGS,
|
|
288
|
+
standards: {
|
|
289
|
+
added: standardsAddedSince(b),
|
|
290
|
+
undecided: parseStandards(read(STANDARDS) ?? "").undecided,
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
return guarded(
|
|
294
|
+
() => walkthrough(b, summary, options.lap ?? "first", carry, fixedAt, afterRiskFix),
|
|
295
|
+
guard,
|
|
296
|
+
)()
|
|
461
297
|
},
|
|
462
298
|
fixRisks: guarded((notes) => {
|
|
463
299
|
riskFixed = true
|
|
@@ -472,7 +308,6 @@ export const humanReview = (
|
|
|
472
308
|
reviewMissing,
|
|
473
309
|
collecting: feedback.collecting,
|
|
474
310
|
triage: triage(severities),
|
|
475
|
-
decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
|
|
476
311
|
settles: settlesStandard,
|
|
477
312
|
learn: learnStandards,
|
|
478
313
|
pending: standardsPending,
|
|
@@ -526,11 +361,9 @@ export interface BuildTailOptions extends TailOptions {
|
|
|
526
361
|
readonly feedback?: FeedbackTarget | undefined
|
|
527
362
|
/** Write access for the nit and risk fix turns; absent: the bundled `access` export decides. */
|
|
528
363
|
readonly fixAccess?: ScopeAccess | undefined
|
|
529
|
-
/** Which round
|
|
364
|
+
/** Which round the reviewer writes; absent: `first`. */
|
|
530
365
|
readonly lap?: t.ReviewRange | undefined
|
|
531
|
-
/**
|
|
532
|
-
readonly walkthrough?: boolean | undefined
|
|
533
|
-
/** What the triaged quality lap left for the review; absent: nothing. */
|
|
366
|
+
/** What the quality lap and its finding gate did, for the review's summary; absent: nothing. */
|
|
534
367
|
readonly report?: LensReport | undefined
|
|
535
368
|
}
|
|
536
369
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
vars,
|
|
9
9
|
writeScript,
|
|
10
10
|
} from "../../flows/index.js"
|
|
11
|
+
import { reviewRisks } from "../../steering/index.js"
|
|
11
12
|
import * as kit from "../../workflow-kit/index.js"
|
|
12
13
|
import { guarded, healthy, type EscalationCount, type Guard } from "./health.js"
|
|
13
14
|
import {
|
|
@@ -108,8 +109,8 @@ export interface TailOptions {
|
|
|
108
109
|
|
|
109
110
|
/**
|
|
110
111
|
* The build tail: fix (when entered red), keep green, the quality lap — a
|
|
111
|
-
* judge sorts its findings
|
|
112
|
-
*
|
|
112
|
+
* judge sorts its findings and the mechanical ones are fixed — then the
|
|
113
|
+
* finding gate over the rest, then `finish` with what became of them all.
|
|
113
114
|
*/
|
|
114
115
|
export function tail<T>(
|
|
115
116
|
finish: (escalations: EscalationCount, report: LensReport) => Promise<T>,
|
|
@@ -214,22 +215,29 @@ export const handOffReview = (
|
|
|
214
215
|
escalations: EscalationCount,
|
|
215
216
|
guard?: Guard,
|
|
216
217
|
report: LensReport = NO_FINDINGS,
|
|
217
|
-
): Promise<void> =>
|
|
218
|
-
|
|
218
|
+
): Promise<void> => {
|
|
219
|
+
// Set by a risk fix, so the re-review written next is summarised whatever it marks.
|
|
220
|
+
let riskFixed = false
|
|
221
|
+
return kit.reviewToHandOff(base, {
|
|
219
222
|
file: REVIEW,
|
|
220
223
|
reviewing: guarded(async (b, carry) => {
|
|
224
|
+
const afterRiskFix = riskFixed
|
|
225
|
+
riskFixed = false
|
|
221
226
|
await reviewing(b, carry)
|
|
222
|
-
// gtd adds the lap's chunks itself: .gtd/QUALITY.md is gone, so a reviewer leaving one out would lose it.
|
|
223
227
|
const review = read(REVIEW)
|
|
224
|
-
|
|
225
|
-
|
|
228
|
+
// A risk-marked draft is fixed and written again first: the re-reviewer edits it, never gtd's summary.
|
|
229
|
+
if (review === undefined || (!afterRiskFix && reviewRisks(review).length > 0)) return
|
|
230
|
+
const full = t.withSummary(review, { report })
|
|
226
231
|
if (full !== review) {
|
|
227
232
|
await run("review.carrying", writeScript(REVIEW, full), {
|
|
228
|
-
label: "Adding the quality lap's
|
|
233
|
+
label: "Adding the quality lap's summary",
|
|
229
234
|
})
|
|
230
235
|
}
|
|
231
236
|
}, guard),
|
|
232
|
-
fixRisks: guarded(
|
|
237
|
+
fixRisks: guarded((risks) => {
|
|
238
|
+
riskFixed = true
|
|
239
|
+
return fixRisks(risks)
|
|
240
|
+
}, guard),
|
|
233
241
|
keepGreen: () => healthy(fixCheck, { escalations, guard }),
|
|
234
242
|
awaitReview: (b) => awaitReview(b, t.handOffAwaitReviewMessage(b)),
|
|
235
243
|
reviewMissing,
|
|
@@ -246,6 +254,7 @@ export const handOffReview = (
|
|
|
246
254
|
deleted:
|
|
247
255
|
"gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix.",
|
|
248
256
|
})
|
|
257
|
+
}
|
|
249
258
|
|
|
250
259
|
/** The tail the fix workflow runs: ends at the hand-off review since `base`, which gets what the lap left. */
|
|
251
260
|
export const handOffTail = (fixFirst: boolean, base: string, guard?: Guard): Promise<void> =>
|