@pmelab/gtd 43.0.0 → 45.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 +33 -13
- package/dist/chunks/{reserved-COEMFEpy.mjs → reserved-ty1mtlnS.mjs} +155 -3
- package/dist/chunks/review-BmoH-9im.mjs +408 -0
- package/dist/chunks/{tail-C_xn8U7U.mjs → tail-CM3sVoAb.mjs} +3 -3
- package/dist/workflow-kit.mjs +22 -10
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +6 -278
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +27 -7
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +379 -66
- package/package.json +1 -1
- package/src/steering/FORMATS.md +4 -4
- package/src/workflow-kit/review.test.ts +80 -2
- package/src/workflow-kit/review.ts +45 -11
- package/src/workflows/feature/coverage.ts +1 -1
- package/src/workflows/feature/feature.test.ts +8 -0
- package/src/workflows/feature/feature.ts +2 -2
- package/src/workflows/feature/index.ts +1 -1
- package/src/workflows/feature/refactor.ts +1 -1
- package/src/workflows/feature/review-answers.feature +12 -3
- package/src/workflows/feature/seam.ts +1 -1
- package/src/workflows/feature/standards.feature +286 -0
- package/src/workflows/feature/steps.test.ts +2 -64
- package/src/workflows/feature/steps.ts +5 -41
- package/src/workflows/fix/fix.feature +20 -21
- package/src/workflows/index.ts +1 -1
- package/src/workflows/review/review-door.feature +158 -0
- package/src/workflows/review/review.test.ts +1 -73
- package/src/workflows/review/review.ts +34 -6
- package/src/workflows/shared/learn.test.ts +154 -0
- package/src/workflows/shared/learn.ts +206 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/{feature → shared}/review.test.ts +4 -8
- package/src/workflows/{feature → shared}/review.ts +23 -10
- package/src/workflows/shared/skills.test.ts +2 -0
- package/src/workflows/shared/skills.ts +2 -0
- package/src/workflows/shared/standards.test.ts +90 -0
- package/src/workflows/shared/standards.ts +156 -0
- package/src/workflows/shared/steps.test.ts +64 -2
- package/src/workflows/shared/steps.ts +40 -1
- package/src/workflows/shared/tail.ts +2 -2
- package/src/workflows/shared/text.test.ts +58 -1
- package/src/workflows/shared/text.ts +39 -1
- package/src/workflows/shared/vars.ts +2 -0
package/package.json
CHANGED
package/src/steering/FORMATS.md
CHANGED
|
@@ -24,11 +24,11 @@ read them.
|
|
|
24
24
|
its first line starts `H:`.
|
|
25
25
|
- **Review notes**: what a human added to a `review` document since the reviewer
|
|
26
26
|
wrote it — changed chunk prose, pointer notes, and new one-shot footnotes,
|
|
27
|
-
never threads. **This is the hand-off from
|
|
28
|
-
|
|
27
|
+
never threads. **This is the hand-off from a `fix` review to the next `fix`**:
|
|
28
|
+
the review leaves the annotated file beside the reviewer's own copy
|
|
29
29
|
(`.gtd/REVIEW.orig.md`), and `fix` reads its work from the notes between the
|
|
30
|
-
two and from the risks, so a file
|
|
31
|
-
|
|
30
|
+
two and from the risks, so a hand-off file must read the same in any `fix`
|
|
31
|
+
that consumes it.
|
|
32
32
|
|
|
33
33
|
## What may change
|
|
34
34
|
|
|
@@ -199,8 +199,12 @@ const driveRound = async (
|
|
|
199
199
|
/** Nothing but thread edits lands after the in-gate collecting turn. */
|
|
200
200
|
settled?: boolean
|
|
201
201
|
noDropped?: boolean
|
|
202
|
+
learn?: ReviewPolicy["learn"]
|
|
203
|
+
settles?: ReviewPolicy["settles"]
|
|
204
|
+
pending?: () => boolean
|
|
202
205
|
} = {},
|
|
203
206
|
) => {
|
|
207
|
+
let reviews = 0
|
|
204
208
|
const triaged: string[][] = []
|
|
205
209
|
const captured: string[] = []
|
|
206
210
|
const editRanges: (string | undefined)[] = []
|
|
@@ -211,9 +215,15 @@ const driveRound = async (
|
|
|
211
215
|
file: "r.md",
|
|
212
216
|
feedback: "f.md",
|
|
213
217
|
isCode: (path: string) => path.endsWith(".ts"),
|
|
214
|
-
reviewing: async () =>
|
|
218
|
+
reviewing: async () => {
|
|
219
|
+
reviews++
|
|
220
|
+
state.files["r.md"] = before
|
|
221
|
+
},
|
|
215
222
|
fixRisks: async () => {},
|
|
216
223
|
keepGreen: async () => {},
|
|
224
|
+
learn: opts.learn,
|
|
225
|
+
settles: opts.settles,
|
|
226
|
+
pending: opts.pending,
|
|
217
227
|
awaitReview: async () => {
|
|
218
228
|
awaits++
|
|
219
229
|
if (awaits > 2 || (awaits > 1 && opts.inGate === undefined)) throw new Error("stop")
|
|
@@ -270,7 +280,7 @@ const driveRound = async (
|
|
|
270
280
|
},
|
|
271
281
|
} as unknown as ReviewPolicy
|
|
272
282
|
const result = await review("base", p).catch((e: Error) => e)
|
|
273
|
-
return { result, triaged, dropped, captured, rounds, editRanges }
|
|
283
|
+
return { result, triaged, dropped, captured, rounds, editRanges, reviews }
|
|
274
284
|
}
|
|
275
285
|
|
|
276
286
|
describe("a one-shot footnote is a note, a thread footnote is not", () => {
|
|
@@ -561,6 +571,74 @@ describe("a note's answer boxes", () => {
|
|
|
561
571
|
})
|
|
562
572
|
})
|
|
563
573
|
|
|
574
|
+
describe("learning from the answers", () => {
|
|
575
|
+
const boxes = (wont: string, own = "_your answer_"): string =>
|
|
576
|
+
doc(
|
|
577
|
+
POINTER,
|
|
578
|
+
"",
|
|
579
|
+
"## Rounding",
|
|
580
|
+
"",
|
|
581
|
+
"Should subtract round?",
|
|
582
|
+
"",
|
|
583
|
+
"Tick exactly one:",
|
|
584
|
+
"",
|
|
585
|
+
"- [ ] fix as suggested",
|
|
586
|
+
`- [${wont}] won't fix`,
|
|
587
|
+
`- [${own === "_your answer_" ? " " : "x"}] ${own}`,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
it("learn reads every answer once it is marked, before any is routed", async () => {
|
|
591
|
+
const seen: ReviewDecision[][] = []
|
|
592
|
+
let marked: string[] = []
|
|
593
|
+
const r = await driveRound(boxes(" "), boxes("x"), {
|
|
594
|
+
learn: async (ds) => {
|
|
595
|
+
seen.push([...ds])
|
|
596
|
+
marked = [...state.marks]
|
|
597
|
+
},
|
|
598
|
+
})
|
|
599
|
+
expect(seen).toEqual([[expect.objectContaining({ anchor: "Rounding", choice: "wontfix" })]])
|
|
600
|
+
expect(marked).toEqual(["wontfix=Rounding"])
|
|
601
|
+
expect(r.result).toEqual({ verdict: "signoff" })
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it("an answer the policy settles is learned from, never marked, routed or collected", async () => {
|
|
605
|
+
const seen: ReviewDecision[][] = []
|
|
606
|
+
const r = await driveRound(boxes(" "), boxes("x"), {
|
|
607
|
+
settles: () => true,
|
|
608
|
+
learn: async (ds) => void seen.push([...ds]),
|
|
609
|
+
})
|
|
610
|
+
expect(seen).toHaveLength(1)
|
|
611
|
+
expect(state.marks).toEqual([])
|
|
612
|
+
expect(r.triaged).toEqual([])
|
|
613
|
+
expect(r.rounds).toEqual([])
|
|
614
|
+
expect(r.result).toEqual({ verdict: "signoff" })
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
it("a hand-edit round learns from its answers too", async () => {
|
|
618
|
+
const seen: ReviewDecision[][] = []
|
|
619
|
+
await driveRound(boxes(" "), boxes("x"), {
|
|
620
|
+
handEdit: true,
|
|
621
|
+
learn: async (ds) => void seen.push([...ds]),
|
|
622
|
+
})
|
|
623
|
+
expect(seen).toHaveLength(1)
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
it("learn is not asked when no note was answered", async () => {
|
|
627
|
+
let asked = false
|
|
628
|
+
await driveRound(boxes(" "), `${boxes(" ")}\n\nstray prose`, {
|
|
629
|
+
silent: true,
|
|
630
|
+
learn: async () => void (asked = true),
|
|
631
|
+
})
|
|
632
|
+
expect(asked).toBe(false)
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
it("a sign-off while something is pending writes the review again", async () => {
|
|
636
|
+
const r = await driveRound(boxes(" "), boxes("x"), { pending: () => true })
|
|
637
|
+
expect(r.reviews).toBe(2)
|
|
638
|
+
expect((r.result as Error).message).toBe("stop")
|
|
639
|
+
})
|
|
640
|
+
})
|
|
641
|
+
|
|
564
642
|
describe("deleted answer boxes", () => {
|
|
565
643
|
it("are refused even when a reply turn landed them", async () => {
|
|
566
644
|
const withBoxes = doc(
|
|
@@ -70,6 +70,12 @@ export interface ReviewPolicy {
|
|
|
70
70
|
readonly fixNits: (notes: readonly ReviewNote[]) => Promise<void>
|
|
71
71
|
/** Told every note the human answered this round, before any of them is routed. */
|
|
72
72
|
readonly decided?: (decisions: readonly ReviewDecision[]) => void
|
|
73
|
+
/** An answer the policy settles in `learn` itself: never routed, recorded or told to `decided`. */
|
|
74
|
+
readonly settles?: (decision: ReviewDecision) => boolean
|
|
75
|
+
/** Reads every answer of the round once it is recorded, before any is routed. */
|
|
76
|
+
readonly learn?: (decisions: readonly ReviewDecision[]) => Promise<void>
|
|
77
|
+
/** Something still waits on the human's answer: a sign-off writes the review again instead. */
|
|
78
|
+
readonly pending?: () => boolean
|
|
73
79
|
/** Remove `file` at the end of a round at commit `round`. */
|
|
74
80
|
readonly close: (round: string) => Promise<void>
|
|
75
81
|
/** What a collecting turn reads. */
|
|
@@ -222,6 +228,19 @@ const record = (p: ReviewPolicy, decisions: readonly ReviewDecision[]): void =>
|
|
|
222
228
|
for (const d of decisions) if (d.choice === "wontfix") mark("wontfix", oneLine(d.anchor))
|
|
223
229
|
}
|
|
224
230
|
|
|
231
|
+
const routed = (p: ReviewPolicy, answers: readonly ReviewDecision[]): readonly ReviewDecision[] =>
|
|
232
|
+
answers.filter((d) => p.settles?.(d) !== true)
|
|
233
|
+
|
|
234
|
+
/** Record the routed answers on the gate's commit, then hand every answer to `learn`. */
|
|
235
|
+
const recordAndLearn = async (
|
|
236
|
+
p: ReviewPolicy,
|
|
237
|
+
decisions: readonly ReviewDecision[],
|
|
238
|
+
answers: readonly ReviewDecision[],
|
|
239
|
+
): Promise<void> => {
|
|
240
|
+
record(p, decisions)
|
|
241
|
+
if (answers.length > 0 && p.learn !== undefined) await p.learn(answers)
|
|
242
|
+
}
|
|
243
|
+
|
|
225
244
|
/**
|
|
226
245
|
* Judge every note, then run each verdict's route: answers, then nit fixes,
|
|
227
246
|
* then the edits' lap. Of the human's `decisions`, a fix joins the nit batch
|
|
@@ -233,8 +252,9 @@ export const routeNotes = async (
|
|
|
233
252
|
notes: readonly ReviewNote[],
|
|
234
253
|
r: Round,
|
|
235
254
|
decisions: readonly ReviewDecision[] = [],
|
|
255
|
+
answers: readonly ReviewDecision[] = decisions,
|
|
236
256
|
): Promise<Finish> => {
|
|
237
|
-
|
|
257
|
+
await recordAndLearn(p, decisions, answers)
|
|
238
258
|
const of = (c: ReviewDecision["choice"]) => decisions.filter((d) => d.choice === c)
|
|
239
259
|
const fixes = of("fix").map((d, i) => decisionNote(d, `fix-${i + 1}`, d.body))
|
|
240
260
|
const judged = [
|
|
@@ -313,14 +333,14 @@ const settledAfter = (
|
|
|
313
333
|
|
|
314
334
|
const finishNotes = async (
|
|
315
335
|
p: ReviewPolicy,
|
|
316
|
-
|
|
317
|
-
|
|
336
|
+
notes: readonly ReviewNote[],
|
|
337
|
+
answers: readonly ReviewDecision[],
|
|
318
338
|
reopened: boolean,
|
|
319
339
|
ctx: Parameters<typeof routeNotes>[2],
|
|
320
340
|
): Promise<Finish> => {
|
|
321
|
-
// An
|
|
322
|
-
|
|
323
|
-
|
|
341
|
+
// An answer the policy settles still routes, if only to learn: there is nothing to collect.
|
|
342
|
+
if (notes.length + answers.length > 0)
|
|
343
|
+
return routeNotes(p, notes, ctx, routed(p, answers), answers)
|
|
324
344
|
if (reopened) return ctx.unfolded()
|
|
325
345
|
await ctx.close()
|
|
326
346
|
return ctx.outcome(await collect(p, p.captures.notes(ctx.round), "notes"))
|
|
@@ -343,6 +363,12 @@ const decisionsSince = (
|
|
|
343
363
|
return reviewDecisions(since.get(p.file)?.before ?? after, after)
|
|
344
364
|
}
|
|
345
365
|
|
|
366
|
+
// An unchanged review file has no baseline entry, which would read as all-new.
|
|
367
|
+
const notesSince = (
|
|
368
|
+
p: ReviewPolicy,
|
|
369
|
+
since: ReturnType<typeof changesSince>,
|
|
370
|
+
): readonly ReviewNote[] => reviewNotes(since.get(p.file)?.before ?? "", read(p.file) ?? "")
|
|
371
|
+
|
|
346
372
|
const finish = async (
|
|
347
373
|
p: ReviewPolicy,
|
|
348
374
|
reviewed: string,
|
|
@@ -368,12 +394,14 @@ const finish = async (
|
|
|
368
394
|
// Thread-only edits settled the round, but a change note or hand-edit since
|
|
369
395
|
// `reviewed` must still reach a collecting turn when a dropped edit is refused.
|
|
370
396
|
const reopened = isReopened(p, { settled, folded })
|
|
371
|
-
const
|
|
372
|
-
if ((settled && !reopened &&
|
|
397
|
+
const answers = decisionsSince(p, since)
|
|
398
|
+
if ((settled && !reopened && answers.length === 0) || (edited.length === 0 && !noted))
|
|
373
399
|
return unfolded()
|
|
400
|
+
// Read before `learn`, which may add to the review file.
|
|
401
|
+
const notes = notesSince(p, since)
|
|
374
402
|
if (edited.length > 0) {
|
|
375
403
|
// A hand-edit plans a lap; its collecting turn reads the answers in the review.
|
|
376
|
-
|
|
404
|
+
await recordAndLearn(p, routed(p, answers), answers)
|
|
377
405
|
await close()
|
|
378
406
|
return outcome(
|
|
379
407
|
await collect(p, p.captures.edits(round, reopened ? reviewed : undefined), "edits", {
|
|
@@ -382,7 +410,7 @@ const finish = async (
|
|
|
382
410
|
}),
|
|
383
411
|
)
|
|
384
412
|
}
|
|
385
|
-
return finishNotes(p,
|
|
413
|
+
return finishNotes(p, notes, answers, reopened, {
|
|
386
414
|
round,
|
|
387
415
|
close,
|
|
388
416
|
outcome,
|
|
@@ -390,6 +418,12 @@ const finish = async (
|
|
|
390
418
|
})
|
|
391
419
|
}
|
|
392
420
|
|
|
421
|
+
/** A sign-off while something waits on the human writes the review again instead. */
|
|
422
|
+
const unlessPending = (p: ReviewPolicy, result: Finish): Finish =>
|
|
423
|
+
"verdict" in result && result.verdict === "signoff" && p.pending?.() === true
|
|
424
|
+
? { next: "rereview", carry: undefined }
|
|
425
|
+
: result
|
|
426
|
+
|
|
393
427
|
/** Write the review; if it marks risks, fix them, keep green, and write it again — once, so the re-review's own risks reach the human unfixed. */
|
|
394
428
|
const reviewOnce = async (
|
|
395
429
|
p: Pick<ReviewPolicy, "file" | "reviewing" | "fixRisks" | "keepGreen">,
|
|
@@ -420,7 +454,7 @@ export const review = async (base: string, p: ReviewPolicy): Promise<ReviewOutco
|
|
|
420
454
|
for (;;) {
|
|
421
455
|
collectedAt = await converse(p, base, reviewed, collectedAt)
|
|
422
456
|
if (collectedAt === "missing") break
|
|
423
|
-
const result = await finish(p, reviewed, collectedAt)
|
|
457
|
+
const result = unlessPending(p, await finish(p, reviewed, collectedAt))
|
|
424
458
|
if (!("next" in result)) return result
|
|
425
459
|
if (result.next === "rereview") {
|
|
426
460
|
carry = result.carry
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type CriteriaSlice,
|
|
20
20
|
} from "./criteria.js"
|
|
21
21
|
import { isTestFile } from "./refactor.js"
|
|
22
|
-
import { isCode } from "
|
|
22
|
+
import { isCode } from "../shared/review.js"
|
|
23
23
|
|
|
24
24
|
/** Below this the judge's `yes` is not trusted: a wrongly covered ID ends the feature without its test. */
|
|
25
25
|
const COVERED_MIN_P = 0.7
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"
|
|
|
2
2
|
import { runsItsScript, testRepo } from "../../testkit/index.js"
|
|
3
3
|
import { CONFIG_SCAFFOLD } from "../../install/index.js"
|
|
4
4
|
import { reviewDoc } from "../shared/config.fixture.js"
|
|
5
|
+
import type * as featureEntry from "./index.js"
|
|
5
6
|
|
|
6
7
|
describe("an ordinary feature start", () => {
|
|
7
8
|
it("unwinds a captured TODO, checks the baseline and starts design from it", async () => {
|
|
@@ -363,3 +364,10 @@ describe("flagMode", () => {
|
|
|
363
364
|
expect(repo.subjects()).toEqual(["chore: initial commit"])
|
|
364
365
|
})
|
|
365
366
|
})
|
|
367
|
+
|
|
368
|
+
// pins the public re-exports from the feature entry point
|
|
369
|
+
export type _PublicBuildTailOptions = [
|
|
370
|
+
featureEntry.BuildTailOptions,
|
|
371
|
+
featureEntry.FeedbackTarget,
|
|
372
|
+
featureEntry.ReviewRange,
|
|
373
|
+
]
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
type StatedCriteria,
|
|
27
27
|
} from "./criteria.js"
|
|
28
28
|
import { design } from "./planning.js"
|
|
29
|
-
import { buildTail, isCode, reUnwind,
|
|
29
|
+
import { buildTail, humanReview, isCode, reUnwind, type ReviewOutcome } from "../shared/review.js"
|
|
30
30
|
import { implement, implementing, seamBuild } from "./seam.js"
|
|
31
31
|
import { sliceCap, sliceCriteria, slicing } from "./slicing.js"
|
|
32
32
|
import { recordRisk, riskTierOn, type RiskTier } from "./tier.js"
|
|
@@ -153,7 +153,7 @@ const settleQuality = async (): Promise<void> => {
|
|
|
153
153
|
|
|
154
154
|
/** The triaged quality lap, then the human review since `base`. */
|
|
155
155
|
const reviewedTail = (base: string, lensBase: string): Promise<ReviewOutcome> =>
|
|
156
|
-
tail((escalations, report) =>
|
|
156
|
+
tail((escalations, report) => humanReview(base, escalations, { report }), {
|
|
157
157
|
green: true,
|
|
158
158
|
lensBase,
|
|
159
159
|
triage: true,
|
|
@@ -11,7 +11,7 @@ export {
|
|
|
11
11
|
type BuildTailOptions,
|
|
12
12
|
type FeedbackTarget,
|
|
13
13
|
type ReviewOutcome,
|
|
14
|
-
} from "
|
|
14
|
+
} from "../shared/review.js"
|
|
15
15
|
export type { ReviewRange } from "../shared/text.js"
|
|
16
16
|
export * from "./feature.js"
|
|
17
17
|
export * from "../shared/reserved.js"
|
|
@@ -20,7 +20,7 @@ import { healthy, type Guard } from "../shared/health.js"
|
|
|
20
20
|
import { coder, FEEDBACK, HANDOFF, REFACTOR } from "../shared/steps.js"
|
|
21
21
|
import * as t from "../shared/text.js"
|
|
22
22
|
import { defaults, testFileGlobs } from "../shared/vars.js"
|
|
23
|
-
import { buildTail, reUnwind, type FeedbackTarget } from "
|
|
23
|
+
import { buildTail, reUnwind, type FeedbackTarget } from "../shared/review.js"
|
|
24
24
|
import { refactorCollecting } from "./steps.js"
|
|
25
25
|
|
|
26
26
|
export const REFACTOR_PACKAGE = REFACTOR
|
|
@@ -114,12 +114,16 @@ Feature: every note in the review is answered with one tick
|
|
|
114
114
|
Then landing is refused with "answer-completeness"
|
|
115
115
|
|
|
116
116
|
Scenario: won't fix drops the note, records it, and signs off
|
|
117
|
+
Given judge step "build.review.standards.judge" answers:
|
|
118
|
+
"""
|
|
119
|
+
[{"id":"standard-1","answer":"no","p":0.9}]
|
|
120
|
+
"""
|
|
117
121
|
When the process is driven
|
|
118
122
|
And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
|
|
119
123
|
And gtd lands
|
|
120
124
|
And the process is driven
|
|
121
125
|
Then the process rests at "idle"
|
|
122
|
-
And the commit "gtd(human): build.review.await-review → build.review.
|
|
126
|
+
And the commit "gtd(human): build.review.await-review → build.review.standards.judge" has a body containing "Gtd-Mark: wontfix=AC-1 ignores floating point"
|
|
123
127
|
And ".gtd/TODO.md" does not exist
|
|
124
128
|
And no commit body contains "build.review.triage"
|
|
125
129
|
|
|
@@ -145,7 +149,11 @@ Feature: every note in the review is answered with one tick
|
|
|
145
149
|
Then the process rests at "idle"
|
|
146
150
|
|
|
147
151
|
Scenario: an own answer is judged like a note
|
|
148
|
-
Given judge step "build.review.
|
|
152
|
+
Given judge step "build.review.standards.judge" answers:
|
|
153
|
+
"""
|
|
154
|
+
[{"id":"standard-1","answer":"no","p":0.9}]
|
|
155
|
+
"""
|
|
156
|
+
And judge step "build.review.triage" answers:
|
|
149
157
|
"""
|
|
150
158
|
[{"id":"answer-1","answer":"nit","p":0.9}]
|
|
151
159
|
"""
|
|
@@ -163,7 +171,8 @@ Feature: every note in the review is answered with one tick
|
|
|
163
171
|
Then the process rests at "build.review.await-review"
|
|
164
172
|
And the commit subjects end with:
|
|
165
173
|
"""
|
|
166
|
-
gtd(human): build.review.await-review → build.review.
|
|
174
|
+
gtd(human): build.review.await-review → build.review.standards.judge
|
|
175
|
+
gtd(judge): build.review.standards.judge → build.review.triage
|
|
167
176
|
gtd(judge): build.review.triage → build.review.fix.nits.fixing
|
|
168
177
|
gtd(agent): build.review.fix.nits.fixing → build.health.check
|
|
169
178
|
gtd(check): build.health.check → build.review.closing
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
refactorStrays,
|
|
46
46
|
reviewedRefactor,
|
|
47
47
|
} from "./refactor.js"
|
|
48
|
-
import { isCode } from "
|
|
48
|
+
import { isCode } from "../shared/review.js"
|
|
49
49
|
import { flagModeOn } from "../shared/options.js"
|
|
50
50
|
import type { RiskTier } from "./tier.js"
|
|
51
51
|
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: the review learns standards from the human's answers
|
|
3
|
+
|
|
4
|
+
A won't-fix tick or an own answer at the review gate is a candidate
|
|
5
|
+
standard. A judge asks whether it is reusable; above `standardMinP` it is
|
|
6
|
+
kept in `.gtd/STANDARDS.md` with no further question, and every later agent
|
|
7
|
+
turn — lenses and coder alike — is told to follow it. One that contradicts
|
|
8
|
+
a standard already kept becomes a note in `.gtd/REVIEW.md` the review cannot
|
|
9
|
+
sign off without: keep the new one, keep the old one, or write the merged
|
|
10
|
+
wording.
|
|
11
|
+
|
|
12
|
+
Background:
|
|
13
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
14
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
|
|
15
|
+
And an environment variable "GTD_FLAGMODE" set to "false"
|
|
16
|
+
And a file "src/calc.ts" with:
|
|
17
|
+
"""
|
|
18
|
+
export const add = (a: number, b: number) => a + b
|
|
19
|
+
"""
|
|
20
|
+
And the working tree is committed as "feat: add calculator"
|
|
21
|
+
And a file "TODO.md" with:
|
|
22
|
+
"""
|
|
23
|
+
# Add subtract
|
|
24
|
+
|
|
25
|
+
subtract(a, b) returns the difference.
|
|
26
|
+
"""
|
|
27
|
+
And step "unwind" changes nothing
|
|
28
|
+
And step "start-gate.check" changes nothing
|
|
29
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
30
|
+
"""
|
|
31
|
+
# Requirements
|
|
32
|
+
|
|
33
|
+
## Concerns
|
|
34
|
+
|
|
35
|
+
### Subtract
|
|
36
|
+
|
|
37
|
+
- `subtract(a, b)` returns `a - b`.
|
|
38
|
+
|
|
39
|
+
## Acceptance Criteria
|
|
40
|
+
|
|
41
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
42
|
+
"""
|
|
43
|
+
And step "design.gate.answer" changes nothing
|
|
44
|
+
And step "seam.naive" writes "src/calc.test.ts" with:
|
|
45
|
+
"""
|
|
46
|
+
it("[AC-1] subtracts", () => {})
|
|
47
|
+
"""
|
|
48
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
49
|
+
"""
|
|
50
|
+
export const add = (a: number, b: number) => a + b
|
|
51
|
+
export const subtract = (a: number, b: number) => a - b
|
|
52
|
+
"""
|
|
53
|
+
And step "seam.check" changes nothing
|
|
54
|
+
And step "implement.applying" runs its script
|
|
55
|
+
And step "implement.health.check" changes nothing
|
|
56
|
+
And step "implement.closing" runs its script
|
|
57
|
+
And judge step "implement.coverage" answers:
|
|
58
|
+
"""
|
|
59
|
+
[{"id":"AC-1","answer":"yes","p":0.9}]
|
|
60
|
+
"""
|
|
61
|
+
And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
|
|
62
|
+
"""
|
|
63
|
+
# Quality
|
|
64
|
+
|
|
65
|
+
## subtract has no class
|
|
66
|
+
|
|
67
|
+
Calculators read better as classes.
|
|
68
|
+
"""
|
|
69
|
+
And step "build.quality.sorting" runs its script
|
|
70
|
+
And step "build.review.composing" runs its script
|
|
71
|
+
And judge step "build.quality.triage" answers:
|
|
72
|
+
"""
|
|
73
|
+
[{"id":"Q1","answer":"design","p":0.9}]
|
|
74
|
+
"""
|
|
75
|
+
And step "build.review.closing" runs its script
|
|
76
|
+
And step "build.review.standards.recording" runs its script
|
|
77
|
+
And step "build.review.standards.raising" runs its script
|
|
78
|
+
|
|
79
|
+
Scenario: a won't-fix the judge calls reusable is kept, and the review signs off
|
|
80
|
+
Given judge step "build.review.standards.judge" answers:
|
|
81
|
+
"""
|
|
82
|
+
[{"id":"standard-1","answer":"yes","p":0.9}]
|
|
83
|
+
"""
|
|
84
|
+
When the process is driven
|
|
85
|
+
And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
|
|
86
|
+
And gtd lands
|
|
87
|
+
And the process is driven
|
|
88
|
+
Then the process rests at "idle"
|
|
89
|
+
And ".gtd/STANDARDS.md" contains:
|
|
90
|
+
"""
|
|
91
|
+
- Won't fix: subtract has no class — Calculators read better as classes.
|
|
92
|
+
"""
|
|
93
|
+
And the commit subjects end with:
|
|
94
|
+
"""
|
|
95
|
+
gtd(human): build.review.await-review → build.review.standards.judge
|
|
96
|
+
gtd(judge): build.review.standards.judge → build.review.standards.recording
|
|
97
|
+
gtd(check): build.review.standards.recording → build.review.closing
|
|
98
|
+
gtd(check): build.review.closing → idle
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
Scenario: the next review lists the standards added
|
|
102
|
+
Given judge step "build.review.standards.judge" answers:
|
|
103
|
+
"""
|
|
104
|
+
[{"id":"standard-1","answer":"yes","p":0.9}]
|
|
105
|
+
"""
|
|
106
|
+
And judge step "build.review.triage" answers:
|
|
107
|
+
"""
|
|
108
|
+
[{"id":"answer-1","answer":"nit","p":0.9}]
|
|
109
|
+
"""
|
|
110
|
+
And step "build.review.fix.nits.fixing" writes "src/calc.ts" with:
|
|
111
|
+
"""
|
|
112
|
+
export function add(a: number, b: number) { return a + b }
|
|
113
|
+
export function subtract(a: number, b: number) { return a - b }
|
|
114
|
+
"""
|
|
115
|
+
And step "build.health.check" changes nothing
|
|
116
|
+
When the process is driven
|
|
117
|
+
And the text "- [ ] _your answer_" in ".gtd/REVIEW.md" is replaced with "- [x] Calculators are plain functions."
|
|
118
|
+
And gtd lands
|
|
119
|
+
And the process is driven
|
|
120
|
+
Then the process rests at "build.review.await-review"
|
|
121
|
+
And ".gtd/REVIEW.md" contains:
|
|
122
|
+
"""
|
|
123
|
+
## Standards added
|
|
124
|
+
|
|
125
|
+
Learned from your answers and kept in `.gtd/STANDARDS.md`:
|
|
126
|
+
|
|
127
|
+
- Calculators are plain functions. (re: subtract has no class)
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
Scenario: an answer below the threshold is not kept
|
|
131
|
+
Given judge step "build.review.standards.judge" answers:
|
|
132
|
+
"""
|
|
133
|
+
[{"id":"standard-1","answer":"yes","p":0.6}]
|
|
134
|
+
"""
|
|
135
|
+
When the process is driven
|
|
136
|
+
And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
|
|
137
|
+
And gtd lands
|
|
138
|
+
And the process is driven
|
|
139
|
+
Then the process rests at "idle"
|
|
140
|
+
And ".gtd/STANDARDS.md" does not exist
|
|
141
|
+
|
|
142
|
+
Scenario: a kept standard reaches the lenses and the coder of a later process
|
|
143
|
+
Given a file ".gtd/STANDARDS.md" with:
|
|
144
|
+
"""
|
|
145
|
+
# Standards
|
|
146
|
+
|
|
147
|
+
- Won't fix: subtract has no class — Calculators read better as classes.
|
|
148
|
+
"""
|
|
149
|
+
And the working tree is committed as "chore: keep a standard"
|
|
150
|
+
And a file "TODO.md" with:
|
|
151
|
+
"""
|
|
152
|
+
# Add subtract
|
|
153
|
+
|
|
154
|
+
Please add subtract(a, b).
|
|
155
|
+
"""
|
|
156
|
+
And step "seam.naive" is prompted with "- Won't fix: subtract has no class"
|
|
157
|
+
And step "build.quality.correctness.reviewing" is prompted with "- Won't fix: subtract has no class"
|
|
158
|
+
When the process is driven
|
|
159
|
+
Then the process rests at "build.review.await-review"
|
|
160
|
+
|
|
161
|
+
Scenario: a contradicting standard blocks sign-off until the human picks one
|
|
162
|
+
Given a file ".gtd/STANDARDS.md" with:
|
|
163
|
+
"""
|
|
164
|
+
# Standards
|
|
165
|
+
|
|
166
|
+
- Calculators are classes.
|
|
167
|
+
"""
|
|
168
|
+
And the working tree is committed as "chore: keep a standard"
|
|
169
|
+
And a file "TODO.md" with:
|
|
170
|
+
"""
|
|
171
|
+
# Add subtract
|
|
172
|
+
|
|
173
|
+
Please add subtract(a, b).
|
|
174
|
+
"""
|
|
175
|
+
And judge step "build.review.standards.judge" answers:
|
|
176
|
+
"""
|
|
177
|
+
[{"id":"standard-1","answer":"yes","p":0.9},{"id":"standard-1.contradicts-1","answer":"yes","p":0.8}]
|
|
178
|
+
"""
|
|
179
|
+
And judge step "build.review.triage" answers:
|
|
180
|
+
"""
|
|
181
|
+
[{"id":"answer-1","answer":"praise","p":0.9}]
|
|
182
|
+
"""
|
|
183
|
+
When the process is driven
|
|
184
|
+
And the text "- [ ] _your answer_" in ".gtd/REVIEW.md" is replaced with "- [x] Calculators are plain functions."
|
|
185
|
+
And gtd lands
|
|
186
|
+
And the process is driven
|
|
187
|
+
Then the process rests at "build.review.await-review"
|
|
188
|
+
And ".gtd/STANDARDS.md" contains:
|
|
189
|
+
"""
|
|
190
|
+
- Calculators are classes.
|
|
191
|
+
|
|
192
|
+
## Undecided
|
|
193
|
+
|
|
194
|
+
- Calculators are plain functions. (re: subtract has no class)
|
|
195
|
+
- contradicts: Calculators are classes.
|
|
196
|
+
"""
|
|
197
|
+
And ".gtd/REVIEW.md" contains "## Standard conflict "
|
|
198
|
+
And ".gtd/REVIEW.md" contains:
|
|
199
|
+
"""
|
|
200
|
+
New: Calculators are plain functions. (re: subtract has no class)
|
|
201
|
+
|
|
202
|
+
Old:
|
|
203
|
+
|
|
204
|
+
- Calculators are classes.
|
|
205
|
+
|
|
206
|
+
Tick exactly one:
|
|
207
|
+
|
|
208
|
+
- [ ] keep the new one (drop the old)
|
|
209
|
+
- [ ] keep the old one (discard the new)
|
|
210
|
+
- [ ] _your answer_
|
|
211
|
+
"""
|
|
212
|
+
And ".gtd/REVIEW.md" does not contain "## subtract has no class"
|
|
213
|
+
And landing is refused with "answer-completeness: 1 note(s) in .gtd/REVIEW.md not answered"
|
|
214
|
+
When the text "- [ ] keep the new one (drop the old)" in ".gtd/REVIEW.md" is replaced with "- [x] keep the new one (drop the old)"
|
|
215
|
+
And gtd lands
|
|
216
|
+
And the process is driven
|
|
217
|
+
Then the process rests at "idle"
|
|
218
|
+
And ".gtd/STANDARDS.md" contains "- Calculators are plain functions. (re: subtract has no class)"
|
|
219
|
+
And ".gtd/STANDARDS.md" does not contain "Calculators are classes."
|
|
220
|
+
And ".gtd/STANDARDS.md" does not contain "Undecided"
|
|
221
|
+
|
|
222
|
+
Scenario: keeping the old standard discards the new one
|
|
223
|
+
Given a file ".gtd/STANDARDS.md" with:
|
|
224
|
+
"""
|
|
225
|
+
# Standards
|
|
226
|
+
|
|
227
|
+
- Calculators are classes.
|
|
228
|
+
"""
|
|
229
|
+
And the working tree is committed as "chore: keep a standard"
|
|
230
|
+
And a file "TODO.md" with:
|
|
231
|
+
"""
|
|
232
|
+
# Add subtract
|
|
233
|
+
|
|
234
|
+
Please add subtract(a, b).
|
|
235
|
+
"""
|
|
236
|
+
And judge step "build.review.standards.judge" answers:
|
|
237
|
+
"""
|
|
238
|
+
[{"id":"standard-1","answer":"yes","p":0.9},{"id":"standard-1.contradicts-1","answer":"yes","p":0.8}]
|
|
239
|
+
"""
|
|
240
|
+
When the process is driven
|
|
241
|
+
And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
|
|
242
|
+
And gtd lands
|
|
243
|
+
And the process is driven
|
|
244
|
+
And the text "- [ ] keep the old one (discard the new)" in ".gtd/REVIEW.md" is replaced with "- [x] keep the old one (discard the new)"
|
|
245
|
+
And gtd lands
|
|
246
|
+
And the process is driven
|
|
247
|
+
Then the process rests at "idle"
|
|
248
|
+
And ".gtd/STANDARDS.md" contains:
|
|
249
|
+
"""
|
|
250
|
+
# Standards
|
|
251
|
+
|
|
252
|
+
Kept from your review answers for every agent turn. One standard per bullet.
|
|
253
|
+
|
|
254
|
+
- Calculators are classes.
|
|
255
|
+
"""
|
|
256
|
+
And ".gtd/STANDARDS.md" does not contain "Won't fix"
|
|
257
|
+
|
|
258
|
+
Scenario: an own answer to a conflict replaces both with its wording
|
|
259
|
+
Given a file ".gtd/STANDARDS.md" with:
|
|
260
|
+
"""
|
|
261
|
+
# Standards
|
|
262
|
+
|
|
263
|
+
- Calculators are classes.
|
|
264
|
+
"""
|
|
265
|
+
And the working tree is committed as "chore: keep a standard"
|
|
266
|
+
And a file "TODO.md" with:
|
|
267
|
+
"""
|
|
268
|
+
# Add subtract
|
|
269
|
+
|
|
270
|
+
Please add subtract(a, b).
|
|
271
|
+
"""
|
|
272
|
+
And judge step "build.review.standards.judge" answers:
|
|
273
|
+
"""
|
|
274
|
+
[{"id":"standard-1","answer":"yes","p":0.9},{"id":"standard-1.contradicts-1","answer":"yes","p":0.8}]
|
|
275
|
+
"""
|
|
276
|
+
When the process is driven
|
|
277
|
+
And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
|
|
278
|
+
And gtd lands
|
|
279
|
+
And the process is driven
|
|
280
|
+
And the text "- [ ] _your answer_" in ".gtd/REVIEW.md" is replaced with "- [x] Calculators with state are classes."
|
|
281
|
+
And gtd lands
|
|
282
|
+
And the process is driven
|
|
283
|
+
Then the process rests at "idle"
|
|
284
|
+
And ".gtd/STANDARDS.md" contains "- Calculators with state are classes."
|
|
285
|
+
And ".gtd/STANDARDS.md" does not contain "- Calculators are classes."
|
|
286
|
+
And ".gtd/STANDARDS.md" does not contain "Won't fix"
|