@pmelab/gtd 48.0.0 → 50.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 +31 -20
- package/dist/chunks/{reserved-DF1HD6jL.mjs → reserved-CdOzKFVX.mjs} +150 -24
- package/dist/chunks/{review-Bv40kZu9.mjs → review-BChXEl2Y.mjs} +132 -24
- package/dist/chunks/{steering-D7hWeMgE.mjs → steering-DtUVUz9l.mjs} +49 -13
- package/dist/chunks/{tail-D89fU_wf.mjs → tail-DRM2eC1z.mjs} +1 -1
- package/dist/gtd.bundle.mjs +22 -5
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +1 -1
- package/dist/web/main.js +27 -3
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +16 -9
- 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 +338 -62
- package/package.json +1 -1
- package/src/steering/index.ts +2 -0
- package/src/steering/review.test.ts +87 -0
- package/src/steering/review.ts +75 -8
- package/src/workflows/feature/coverage.feature +3 -3
- package/src/workflows/feature/fast-path.feature +2 -2
- package/src/workflows/feature/feature.ts +10 -3
- package/src/workflows/feature/flag-mode.feature +2 -2
- package/src/workflows/feature/lens-review.feature +128 -13
- package/src/workflows/feature/one-step-land.feature +2 -2
- package/src/workflows/feature/refactor-absorbs.feature +2 -2
- package/src/workflows/feature/refactor-first.feature +2 -2
- package/src/workflows/feature/refactor-rebuild.feature +2 -2
- package/src/workflows/feature/refactor-review.feature +6 -6
- package/src/workflows/feature/review-answers.feature +51 -5
- package/src/workflows/feature/review-gate.feature +23 -6
- package/src/workflows/feature/reviewer-walkthrough.feature +240 -0
- package/src/workflows/feature/seam-rejection.feature +2 -2
- package/src/workflows/feature/seam.feature +2 -2
- package/src/workflows/feature/severity-budget.feature +111 -0
- package/src/workflows/feature/severity-sorted-review.feature +197 -0
- package/src/workflows/feature/slice-findings.feature +68 -5
- package/src/workflows/feature/slicing.feature +6 -6
- package/src/workflows/feature/standards.feature +29 -1
- package/src/workflows/shared/review.ts +226 -25
- package/src/workflows/shared/steps.ts +3 -2
- package/src/workflows/shared/text.test.ts +216 -0
- package/src/workflows/shared/text.ts +201 -19
|
@@ -1276,6 +1276,33 @@ describe("review.view", () => {
|
|
|
1276
1276
|
expect(view.nodes[1]!.detail).toBe("")
|
|
1277
1277
|
})
|
|
1278
1278
|
|
|
1279
|
+
it("shows a chunk's leading severity line as its status, not its prose", () => {
|
|
1280
|
+
const view = review.view(
|
|
1281
|
+
[
|
|
1282
|
+
"# Review: abc1234",
|
|
1283
|
+
"",
|
|
1284
|
+
"## Ranked",
|
|
1285
|
+
"",
|
|
1286
|
+
"_critical: security, one-way door_",
|
|
1287
|
+
"",
|
|
1288
|
+
"The new export.",
|
|
1289
|
+
"",
|
|
1290
|
+
"- [ ] ./a.ts#1-1",
|
|
1291
|
+
"",
|
|
1292
|
+
"## Unranked",
|
|
1293
|
+
"",
|
|
1294
|
+
"_emphasis, not a tier_",
|
|
1295
|
+
"",
|
|
1296
|
+
"- [ ] ./a.ts#2-2",
|
|
1297
|
+
"",
|
|
1298
|
+
].join("\n"),
|
|
1299
|
+
)
|
|
1300
|
+
expect(view.nodes.map((c) => [c.status, c.detail])).toEqual([
|
|
1301
|
+
["critical: security, one-way door", "The new export."],
|
|
1302
|
+
[undefined, "_emphasis, not a tier_"],
|
|
1303
|
+
])
|
|
1304
|
+
})
|
|
1305
|
+
|
|
1279
1306
|
it("is built from one parse of the document, not one per element", () => {
|
|
1280
1307
|
const uniqueContent = [
|
|
1281
1308
|
"# Review: def4567",
|
|
@@ -1706,6 +1733,29 @@ describe("reviewNotes", async () => {
|
|
|
1706
1733
|
expect(reviewNotes(base, base)).toEqual([])
|
|
1707
1734
|
})
|
|
1708
1735
|
|
|
1736
|
+
it("an edited tier line is a note", () => {
|
|
1737
|
+
const was = doc("_could_\n\nProse.")
|
|
1738
|
+
const after = doc("_critical: this leaks the token_\n\nProse.")
|
|
1739
|
+
expect(reviewNotes(was, after)).toMatchObject([
|
|
1740
|
+
{
|
|
1741
|
+
kind: "chunk",
|
|
1742
|
+
before: "Prose.\n\n_could_",
|
|
1743
|
+
text: "Prose.\n\n_critical: this leaks the token_",
|
|
1744
|
+
},
|
|
1745
|
+
])
|
|
1746
|
+
})
|
|
1747
|
+
|
|
1748
|
+
it("prose added under a tier is the human's alone, and a leading ! still forces", () => {
|
|
1749
|
+
const was = doc("_could_\n\nProse.")
|
|
1750
|
+
expect(reviewNotes(was, doc("_could_\n\n! Not here.\n\nProse."))).toMatchObject([
|
|
1751
|
+
{ kind: "chunk", text: "! Not here. Prose.", forced: true },
|
|
1752
|
+
])
|
|
1753
|
+
})
|
|
1754
|
+
|
|
1755
|
+
it("a deleted tier line asks nothing", () => {
|
|
1756
|
+
expect(reviewNotes(doc("_could_\n\nProse."), base)).toEqual([])
|
|
1757
|
+
})
|
|
1758
|
+
|
|
1709
1759
|
it("a pointer note added", () => {
|
|
1710
1760
|
const after = doc("Prose.").replace("#1-3", "#1-3 rename this")
|
|
1711
1761
|
expect(reviewNotes(base, after)).toEqual([
|
|
@@ -1840,6 +1890,43 @@ describe("reviewRisks", async () => {
|
|
|
1840
1890
|
})
|
|
1841
1891
|
})
|
|
1842
1892
|
|
|
1893
|
+
describe("reviewChunks", async () => {
|
|
1894
|
+
const { reviewChunks } = await import("./review.js")
|
|
1895
|
+
|
|
1896
|
+
it("counts each chunk's pointers, a chunk of prose and answer boxes holding none", () => {
|
|
1897
|
+
const content = [
|
|
1898
|
+
"# Review: abc1234",
|
|
1899
|
+
"",
|
|
1900
|
+
"<!-- base: abc1234def5678901234567890123456789abcd -->",
|
|
1901
|
+
"",
|
|
1902
|
+
"## Walkthrough",
|
|
1903
|
+
"",
|
|
1904
|
+
"- [ ] ./src/a.ts#1-3 — first",
|
|
1905
|
+
"- [ ] ./src/b.ts#4-6 — second",
|
|
1906
|
+
"",
|
|
1907
|
+
"## Fixed automatically",
|
|
1908
|
+
"",
|
|
1909
|
+
"Tick exactly one:",
|
|
1910
|
+
"",
|
|
1911
|
+
"- [ ] fix as suggested",
|
|
1912
|
+
"- [ ] won't fix",
|
|
1913
|
+
"- [ ] _your answer_",
|
|
1914
|
+
].join("\n")
|
|
1915
|
+
expect(reviewChunks(content)).toEqual([
|
|
1916
|
+
{
|
|
1917
|
+
title: "Walkthrough",
|
|
1918
|
+
line: 4,
|
|
1919
|
+
pointers: 2,
|
|
1920
|
+
files: [
|
|
1921
|
+
{ path: "./src/a.ts", line: 1, rangeEnd: 3 },
|
|
1922
|
+
{ path: "./src/b.ts", line: 4, rangeEnd: 6 },
|
|
1923
|
+
],
|
|
1924
|
+
},
|
|
1925
|
+
{ title: "Fixed automatically", line: 9, pointers: 0, files: [] },
|
|
1926
|
+
])
|
|
1927
|
+
})
|
|
1928
|
+
})
|
|
1929
|
+
|
|
1843
1930
|
describe("answer boxes", async () => {
|
|
1844
1931
|
const { reviewDecisions, reviewNotes, unansweredNotes } = await import("./review.js")
|
|
1845
1932
|
const note = (title: string, boxes: readonly [string, string, string]): string =>
|
package/src/steering/review.ts
CHANGED
|
@@ -77,6 +77,8 @@ interface Changeset {
|
|
|
77
77
|
readonly headingLine: number
|
|
78
78
|
/** The chunk's answer boxes, when gtd gave it some. */
|
|
79
79
|
readonly answers?: readonly AnswerOption[]
|
|
80
|
+
/** The tier and reason gtd ranked the chunk with, from its leading severity line. */
|
|
81
|
+
readonly severity?: { readonly label: string; readonly line: number }
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
interface AnswerOption {
|
|
@@ -89,6 +91,10 @@ export const ANSWER_LEAD = "Tick exactly one:"
|
|
|
89
91
|
export const ANSWER_FIX = "fix as suggested"
|
|
90
92
|
export const ANSWER_WONT_FIX = "won't fix"
|
|
91
93
|
|
|
94
|
+
/** A chunk's leading `_critical: security_` paragraph: the tier gtd ranked it with, and why. */
|
|
95
|
+
const SEVERITY_LINE_RE = /^_((?:critical|should|could)(?::[^_\n]*)?)_$/
|
|
96
|
+
export const severityLine = (label: string): string => `_${label}_`
|
|
97
|
+
|
|
92
98
|
interface ReviewDoc {
|
|
93
99
|
readonly shortHash?: string
|
|
94
100
|
readonly fullHash?: string
|
|
@@ -521,6 +527,26 @@ const answerListOf = (
|
|
|
521
527
|
return { lead, list, options }
|
|
522
528
|
}
|
|
523
529
|
|
|
530
|
+
/** A leading severity line is the chunk's tier, never its prose. */
|
|
531
|
+
const splitSeverity = (
|
|
532
|
+
content: string,
|
|
533
|
+
run: readonly RootContent[],
|
|
534
|
+
): {
|
|
535
|
+
readonly severity?: { readonly label: string; readonly line: number } | undefined
|
|
536
|
+
readonly leadingRun: readonly RootContent[]
|
|
537
|
+
} => {
|
|
538
|
+
const first = run[0]
|
|
539
|
+
const label =
|
|
540
|
+
first?.type === "paragraph"
|
|
541
|
+
? SEVERITY_LINE_RE.exec(sourceText(content, first).trim())?.[1]
|
|
542
|
+
: undefined
|
|
543
|
+
if (label === undefined) return { leadingRun: run }
|
|
544
|
+
return {
|
|
545
|
+
severity: { label, line: toLspPosition(first!.position!.start).line },
|
|
546
|
+
leadingRun: run.slice(1),
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
524
550
|
/** Splits one chunk's body nodes into its file pointers (hunk pointers are task items, collected recursively at ANY nesting depth via `taskItems` — a nested hunk is the same kind of hunk as a top-level one) and description prose. The description is the chunk's own leading run and never a node that contains a hunk pointer: it stops at the first node whose `taskItems` include a real pointer (at any depth — a blockquote wrapping a pointer list stops it just as a top-level list would). Every node kind in what's left before that point contributes — a `footnoteDefinition` is the one exception, excluded by `blockNodesOfRun` itself (it's a note, never document content). */
|
|
525
551
|
const parseChunkBody = (
|
|
526
552
|
content: string,
|
|
@@ -534,6 +560,7 @@ const parseChunkBody = (
|
|
|
534
560
|
readonly files: readonly ReviewFile[]
|
|
535
561
|
readonly errors: readonly SteeringFinding[]
|
|
536
562
|
readonly answers?: readonly AnswerOption[]
|
|
563
|
+
readonly severity?: { readonly label: string; readonly line: number } | undefined
|
|
537
564
|
/** Whether a task box outside the answer list exists. */
|
|
538
565
|
readonly boxed: boolean
|
|
539
566
|
} => {
|
|
@@ -543,9 +570,10 @@ const parseChunkBody = (
|
|
|
543
570
|
const firstPointerIndex = body.findIndex(
|
|
544
571
|
(n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)),
|
|
545
572
|
)
|
|
546
|
-
const
|
|
573
|
+
const run = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter(
|
|
547
574
|
(n) => !isAnswer(n),
|
|
548
575
|
)
|
|
576
|
+
const { severity, leadingRun } = splitSeverity(content, run)
|
|
549
577
|
const descriptionNodes = blockNodesOfRun(content, leadingRun)
|
|
550
578
|
const descriptionInline = collapseImages(blockRunInline(content, leadingRun))
|
|
551
579
|
const description = leadingRun
|
|
@@ -569,6 +597,7 @@ const parseChunkBody = (
|
|
|
569
597
|
files,
|
|
570
598
|
errors,
|
|
571
599
|
...(answer === undefined ? {} : { answers: answer.options }),
|
|
600
|
+
...(severity === undefined ? {} : { severity }),
|
|
572
601
|
boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0),
|
|
573
602
|
}
|
|
574
603
|
}
|
|
@@ -618,6 +647,7 @@ const parseChangesets = (
|
|
|
618
647
|
descriptionInline,
|
|
619
648
|
files,
|
|
620
649
|
answers,
|
|
650
|
+
severity,
|
|
621
651
|
boxed,
|
|
622
652
|
errors: bodyErrors,
|
|
623
653
|
} = parseChunkBody(content, lines, title, body)
|
|
@@ -638,6 +668,7 @@ const parseChangesets = (
|
|
|
638
668
|
files,
|
|
639
669
|
headingLine,
|
|
640
670
|
...(answers === undefined ? {} : { answers }),
|
|
671
|
+
...(severity === undefined ? {} : { severity }),
|
|
641
672
|
})
|
|
642
673
|
}
|
|
643
674
|
if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" })
|
|
@@ -997,6 +1028,7 @@ const reviewView = (content: string): SteeringView => {
|
|
|
997
1028
|
detail: chunk.description,
|
|
998
1029
|
detailInline: chunk.descriptionInline,
|
|
999
1030
|
body: chunk.descriptionNodes,
|
|
1031
|
+
...(chunk.severity === undefined ? {} : { status: chunk.severity.label }),
|
|
1000
1032
|
anchor: { kind: "chunk", index: chunkIndex },
|
|
1001
1033
|
...noteAt(chunk.headingLine),
|
|
1002
1034
|
children: chunk.files.map((file, index) => ({
|
|
@@ -1177,11 +1209,22 @@ const pointerLabel = (f: ReviewFile): string =>
|
|
|
1177
1209
|
|
|
1178
1210
|
type Found = { line: number; note: Omit<ReviewNote, "id" | "forced"> }
|
|
1179
1211
|
|
|
1180
|
-
//
|
|
1181
|
-
//
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1212
|
+
// Rewording a tier disputes it, so a changed tier joins the note — after the
|
|
1213
|
+
// prose, so a leading `!` still forces; deleting one asks nothing.
|
|
1214
|
+
const chunkProse = (chunk: Changeset | undefined, retiered: boolean): string =>
|
|
1215
|
+
[
|
|
1216
|
+
chunk?.description ?? "",
|
|
1217
|
+
retiered && chunk?.severity !== undefined ? severityLine(chunk.severity.label) : "",
|
|
1218
|
+
]
|
|
1219
|
+
.filter((p) => p !== "")
|
|
1220
|
+
.join("\n\n")
|
|
1221
|
+
|
|
1222
|
+
const chunkNote = (chunk: Changeset, prior: Changeset | undefined): Found[] => {
|
|
1223
|
+
const retiered = chunk.severity !== undefined && chunk.severity.label !== prior?.severity?.label
|
|
1224
|
+
// A chunk added with its own answer boxes (a driver's imported note) is
|
|
1225
|
+
// answered by its tick, never judged as prose as well.
|
|
1226
|
+
return chunkProse(chunk, retiered) === chunkProse(prior, retiered) ||
|
|
1227
|
+
(prior === undefined && chunk.answers !== undefined)
|
|
1185
1228
|
? []
|
|
1186
1229
|
: [
|
|
1187
1230
|
{
|
|
@@ -1189,11 +1232,12 @@ const chunkNote = (chunk: Changeset, prior: Changeset | undefined): Found[] =>
|
|
|
1189
1232
|
note: {
|
|
1190
1233
|
kind: "chunk",
|
|
1191
1234
|
anchor: chunk.title,
|
|
1192
|
-
before: prior
|
|
1193
|
-
text: chunk
|
|
1235
|
+
before: chunkProse(prior, retiered),
|
|
1236
|
+
text: chunkProse(chunk, retiered),
|
|
1194
1237
|
},
|
|
1195
1238
|
},
|
|
1196
1239
|
]
|
|
1240
|
+
}
|
|
1197
1241
|
|
|
1198
1242
|
const pointerNotes = (chunk: Changeset, prior: Changeset | undefined): Found[] =>
|
|
1199
1243
|
chunk.files.flatMap((file): Found[] => {
|
|
@@ -1338,3 +1382,26 @@ export const reviewDecisions = (before: string, after: string): readonly ReviewD
|
|
|
1338
1382
|
return old?.choice === decision.choice && old.text === decision.text ? [] : [decision]
|
|
1339
1383
|
})
|
|
1340
1384
|
}
|
|
1385
|
+
|
|
1386
|
+
/** Each `##` chunk in document order: its title, 0-based heading line and how many hunk pointers it holds. */
|
|
1387
|
+
export const reviewChunks = (
|
|
1388
|
+
content: string,
|
|
1389
|
+
): readonly {
|
|
1390
|
+
title: string
|
|
1391
|
+
line: number
|
|
1392
|
+
pointers: number
|
|
1393
|
+
/** 0-based line of the chunk's severity line, when it has one. */
|
|
1394
|
+
severityLine?: number
|
|
1395
|
+
files: readonly { path: string; line?: number; rangeEnd?: number }[]
|
|
1396
|
+
}[] =>
|
|
1397
|
+
parseReviewDoc(content).changesets.map((c) => ({
|
|
1398
|
+
title: c.title,
|
|
1399
|
+
line: c.headingLine,
|
|
1400
|
+
pointers: c.files.length,
|
|
1401
|
+
...(c.severity === undefined ? {} : { severityLine: c.severity.line }),
|
|
1402
|
+
files: c.files.map((f) => ({
|
|
1403
|
+
path: f.path,
|
|
1404
|
+
...(f.line === undefined ? {} : { line: f.line }),
|
|
1405
|
+
...(f.rangeEnd === undefined ? {} : { rangeEnd: f.rangeEnd }),
|
|
1406
|
+
})),
|
|
1407
|
+
}))
|
|
@@ -61,13 +61,13 @@ Feature: a feature is done only when every acceptance criterion has a passing te
|
|
|
61
61
|
[{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
|
|
62
62
|
"""
|
|
63
63
|
When the process is driven
|
|
64
|
-
Then the process rests at "build.review.
|
|
64
|
+
Then the process rests at "build.review.reviewing"
|
|
65
65
|
And the commit subjects end with:
|
|
66
66
|
"""
|
|
67
67
|
gtd(check): implement.applying → implement.health.check
|
|
68
68
|
gtd(check): implement.health.check → implement.coverage
|
|
69
69
|
gtd(judge): implement.coverage → implement.closing
|
|
70
|
-
gtd(check): implement.closing → build.review.
|
|
70
|
+
gtd(check): implement.closing → build.review.reviewing
|
|
71
71
|
"""
|
|
72
72
|
And the commit "gtd(judge): implement.coverage → implement.closing" has a body containing "Gtd-Mark: covers=AC-1"
|
|
73
73
|
And the commit "gtd(judge): implement.coverage → implement.closing" has a body containing "Gtd-Mark: covers=AC-2"
|
|
@@ -144,7 +144,7 @@ Feature: a feature is done only when every acceptance criterion has a passing te
|
|
|
144
144
|
"""
|
|
145
145
|
And step "implement.uncovered" changes nothing
|
|
146
146
|
When the process is driven
|
|
147
|
-
Then the process rests at "build.review.
|
|
147
|
+
Then the process rests at "build.review.reviewing"
|
|
148
148
|
And the commit "gtd(human): implement.uncovered → implement.closing" has a body containing "Gtd-Mark: covers=AC-1"
|
|
149
149
|
And the commit "gtd(human): implement.uncovered → implement.closing" has a body containing "Gtd-Mark: uncovered=AC-2"
|
|
150
150
|
|
|
@@ -64,7 +64,7 @@ Feature: a trivial change takes the same few beats at either risk tier
|
|
|
64
64
|
"""
|
|
65
65
|
And step "implement.applying" runs its script
|
|
66
66
|
When the process is driven
|
|
67
|
-
Then the process rests at "build.review.
|
|
67
|
+
Then the process rests at "build.review.reviewing"
|
|
68
68
|
And "src/greeting.ts" contains "Hello, world"
|
|
69
69
|
And the commit subjects end with:
|
|
70
70
|
"""
|
|
@@ -80,7 +80,7 @@ Feature: a trivial change takes the same few beats at either risk tier
|
|
|
80
80
|
gtd(check): implement.health.check → implement.coverage
|
|
81
81
|
gtd(judge): implement.coverage → implement.closing
|
|
82
82
|
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
83
|
-
gtd(agent): build.quality.correctness.reviewing → build.review.
|
|
83
|
+
gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
|
|
84
84
|
"""
|
|
85
85
|
And the output of gtd "summary" <summary> "needs a human reviewer"
|
|
86
86
|
|
|
@@ -38,7 +38,8 @@ import { recordRisk, riskTierOn, type RiskTier } from "./tier.js"
|
|
|
38
38
|
// judged with `flagMode` on: it lands as the implementation, or (refactoring
|
|
39
39
|
// first when the judge asks) is built fresh from → the health loop → the
|
|
40
40
|
// triaged quality lap, whose leftovers are carried on → after the last slice,
|
|
41
|
-
//
|
|
41
|
+
// a reviewer's walkthrough of the whole change, every slice's findings
|
|
42
|
+
// among its chunks, for the human, which
|
|
42
43
|
// signs off (the episode ends back at `idle`) or sends a design → implement
|
|
43
44
|
// lap.
|
|
44
45
|
|
|
@@ -152,11 +153,15 @@ const buildSlice = async (sliceBase: string, build: SliceBuild): Promise<void> =
|
|
|
152
153
|
})
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* The quality lap of slice `last.slice`, then a reviewer's walkthrough since
|
|
158
|
+
* `base` for the human, carrying what earlier slices' `laps` left too.
|
|
159
|
+
*/
|
|
156
160
|
const reviewedTail = (
|
|
157
161
|
base: string,
|
|
158
162
|
lensBase: string,
|
|
159
163
|
last: { readonly slice: string; readonly laps: SliceBuild["laps"] } = { slice: "", laps: [] },
|
|
164
|
+
lap: "first" | "feedback" = "first",
|
|
160
165
|
): Promise<ReviewOutcome> =>
|
|
161
166
|
tail(
|
|
162
167
|
(escalations, report) =>
|
|
@@ -165,6 +170,8 @@ const reviewedTail = (
|
|
|
165
170
|
last.laps.length === 0
|
|
166
171
|
? report
|
|
167
172
|
: joinReports([...last.laps, { slice: last.slice, report }]),
|
|
173
|
+
walkthrough: true,
|
|
174
|
+
lap,
|
|
168
175
|
}),
|
|
169
176
|
{ green: true, lensBase },
|
|
170
177
|
)
|
|
@@ -189,7 +196,7 @@ const featureFeedback = async (
|
|
|
189
196
|
})
|
|
190
197
|
const tier = riskTierOn() ? recordRisk() : undefined
|
|
191
198
|
closed = await implementing({ tier, amend: true })
|
|
192
|
-
outcome = await reviewedTail(outcome.base, lensBase)
|
|
199
|
+
outcome = await reviewedTail(outcome.base, lensBase, undefined, "feedback")
|
|
193
200
|
}
|
|
194
201
|
}
|
|
195
202
|
|
|
@@ -67,7 +67,7 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
|
|
|
67
67
|
"""
|
|
68
68
|
And step "build.quality.correctness.reviewing" changes nothing
|
|
69
69
|
When the process is driven
|
|
70
|
-
Then the process rests at "build.review.
|
|
70
|
+
Then the process rests at "build.review.reviewing"
|
|
71
71
|
And "src/calc.ts" contains "subtract"
|
|
72
72
|
And "src/calc.test.ts" contains "[AC-1] subtracts"
|
|
73
73
|
And the commit subjects end with:
|
|
@@ -85,5 +85,5 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
|
|
|
85
85
|
gtd(check): implement.health.check → implement.coverage
|
|
86
86
|
gtd(judge): implement.coverage → implement.closing
|
|
87
87
|
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
88
|
-
gtd(agent): build.quality.correctness.reviewing → build.review.
|
|
88
|
+
gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
|
|
89
89
|
"""
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
@inmem
|
|
2
|
-
Feature: the quality
|
|
2
|
+
Feature: the quality lap's findings are triaged before the review
|
|
3
3
|
|
|
4
4
|
A judge classifies each `## ` finding the lenses wrote to `.gtd/QUALITY.md`:
|
|
5
5
|
a mechanical one is fixed by the fixer, noise is dropped with its `p` kept in
|
|
6
6
|
a trailer, and a design one becomes a note in `.gtd/REVIEW.md`, with answer
|
|
7
|
-
boxes
|
|
8
|
-
|
|
9
|
-
noise.
|
|
7
|
+
boxes, in the chunk the reviewer gave it. gtd lists, without tick boxes,
|
|
8
|
+
what was fixed automatically and what was dropped as noise.
|
|
10
9
|
|
|
11
10
|
Background:
|
|
12
11
|
Given the shipped workflows, as gtd install scaffolds them
|
|
@@ -75,7 +74,11 @@ Feature: the quality lenses are the only agentic review
|
|
|
75
74
|
"""
|
|
76
75
|
And step "build.quality.sorting" runs its script
|
|
77
76
|
And step "build.quality.closing" runs its script
|
|
78
|
-
And step "build.review.
|
|
77
|
+
And step "build.review.carrying" runs its script
|
|
78
|
+
And judge step "build.review.ranking" answers:
|
|
79
|
+
"""
|
|
80
|
+
[]
|
|
81
|
+
"""
|
|
79
82
|
|
|
80
83
|
Scenario: a mechanical finding is fixed and listed, noise dropped and listed, a design one noted
|
|
81
84
|
Given judge step "build.quality.triage" answers:
|
|
@@ -89,6 +92,20 @@ Feature: the quality lenses are the only agentic review
|
|
|
89
92
|
"""
|
|
90
93
|
And step "build.fix.quality.fixing" deletes ".gtd/QUALITY.md"
|
|
91
94
|
And step "build.health.check" changes nothing
|
|
95
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
96
|
+
"""
|
|
97
|
+
# Review: abc1234
|
|
98
|
+
|
|
99
|
+
<!-- base: 0000000 -->
|
|
100
|
+
|
|
101
|
+
## Subtract
|
|
102
|
+
|
|
103
|
+
- [ ] ./src/calc.ts#2-2 — subtract(a, b)
|
|
104
|
+
|
|
105
|
+
## AC-1 ignores floating point
|
|
106
|
+
|
|
107
|
+
- [ ] ./src/calc.ts#2-2 — where it applies
|
|
108
|
+
"""
|
|
92
109
|
When the process is driven
|
|
93
110
|
Then the process rests at "build.review.await-review"
|
|
94
111
|
And ".gtd/QUALITY.md" does not exist
|
|
@@ -97,8 +114,14 @@ Feature: the quality lenses are the only agentic review
|
|
|
97
114
|
"""
|
|
98
115
|
## AC-1 ignores floating point
|
|
99
116
|
|
|
117
|
+
_critical: judge unsure_
|
|
118
|
+
|
|
100
119
|
Should subtract round? The criterion does not say.
|
|
101
120
|
|
|
121
|
+
In the code:
|
|
122
|
+
|
|
123
|
+
- [ ] ./src/calc.ts#2-2 — where it applies
|
|
124
|
+
|
|
102
125
|
Tick exactly one:
|
|
103
126
|
|
|
104
127
|
- [ ] fix as suggested
|
|
@@ -129,8 +152,10 @@ Feature: the quality lenses are the only agentic review
|
|
|
129
152
|
gtd(judge): build.quality.triage → build.quality.sorting
|
|
130
153
|
gtd(check): build.quality.sorting → build.fix.quality.fixing
|
|
131
154
|
gtd(agent): build.fix.quality.fixing → build.health.check
|
|
132
|
-
gtd(check): build.health.check → build.review.
|
|
133
|
-
gtd(
|
|
155
|
+
gtd(check): build.health.check → build.review.reviewing
|
|
156
|
+
gtd(agent): build.review.reviewing → build.review.ranking
|
|
157
|
+
gtd(judge): build.review.ranking → build.review.carrying
|
|
158
|
+
gtd(check): build.review.carrying → build.review.await-review
|
|
134
159
|
"""
|
|
135
160
|
|
|
136
161
|
Scenario: an unsure verdict makes a finding a note, never a fix or a drop
|
|
@@ -138,6 +163,28 @@ Feature: the quality lenses are the only agentic review
|
|
|
138
163
|
"""
|
|
139
164
|
[{"id":"Q1","answer":"mechanical","p":0.5},{"id":"Q2","answer":"noise","p":0.6}]
|
|
140
165
|
"""
|
|
166
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
167
|
+
"""
|
|
168
|
+
# Review: abc1234
|
|
169
|
+
|
|
170
|
+
<!-- base: 0000000 -->
|
|
171
|
+
|
|
172
|
+
## Subtract
|
|
173
|
+
|
|
174
|
+
- [ ] ./src/calc.ts#2-2 — subtract(a, b)
|
|
175
|
+
|
|
176
|
+
## subtract has no negative test
|
|
177
|
+
|
|
178
|
+
- [ ] ./src/calc.ts#2-2 — where it applies
|
|
179
|
+
|
|
180
|
+
## add could be a method
|
|
181
|
+
|
|
182
|
+
- [ ] ./src/calc.ts#2-2 — where it applies
|
|
183
|
+
|
|
184
|
+
## AC-1 ignores floating point
|
|
185
|
+
|
|
186
|
+
- [ ] ./src/calc.ts#2-2 — where it applies
|
|
187
|
+
"""
|
|
141
188
|
When the process is driven
|
|
142
189
|
Then the process rests at "build.review.await-review"
|
|
143
190
|
And ".gtd/REVIEW.md" contains "## subtract has no negative test"
|
|
@@ -148,8 +195,10 @@ Feature: the quality lenses are the only agentic review
|
|
|
148
195
|
And the commit subjects end with:
|
|
149
196
|
"""
|
|
150
197
|
gtd(judge): build.quality.triage → build.quality.sorting
|
|
151
|
-
gtd(check): build.quality.sorting → build.review.
|
|
152
|
-
gtd(
|
|
198
|
+
gtd(check): build.quality.sorting → build.review.reviewing
|
|
199
|
+
gtd(agent): build.review.reviewing → build.review.ranking
|
|
200
|
+
gtd(judge): build.review.ranking → build.review.carrying
|
|
201
|
+
gtd(check): build.review.carrying → build.review.await-review
|
|
153
202
|
"""
|
|
154
203
|
|
|
155
204
|
Scenario: a mechanical finding the fix cap leaves becomes a note
|
|
@@ -166,6 +215,20 @@ Feature: the quality lenses are the only agentic review
|
|
|
166
215
|
A class would read better.
|
|
167
216
|
"""
|
|
168
217
|
And step "build.health.check" changes nothing
|
|
218
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
219
|
+
"""
|
|
220
|
+
# Review: abc1234
|
|
221
|
+
|
|
222
|
+
<!-- base: 0000000 -->
|
|
223
|
+
|
|
224
|
+
## Subtract
|
|
225
|
+
|
|
226
|
+
- [ ] ./src/calc.ts#2-2 — subtract(a, b)
|
|
227
|
+
|
|
228
|
+
## add could be a method
|
|
229
|
+
|
|
230
|
+
- [ ] ./src/calc.ts#2-2 — where it applies
|
|
231
|
+
"""
|
|
169
232
|
When the process is driven
|
|
170
233
|
Then the process rests at "build.review.await-review"
|
|
171
234
|
And ".gtd/QUALITY.md" does not exist
|
|
@@ -173,6 +236,8 @@ Feature: the quality lenses are the only agentic review
|
|
|
173
236
|
"""
|
|
174
237
|
## add could be a method
|
|
175
238
|
|
|
239
|
+
_critical: judge unsure_
|
|
240
|
+
|
|
176
241
|
_The fixer left this unresolved._
|
|
177
242
|
|
|
178
243
|
A class would read better.
|
|
@@ -186,8 +251,10 @@ Feature: the quality lenses are the only agentic review
|
|
|
186
251
|
gtd(agent): build.fix.quality.fixing
|
|
187
252
|
gtd(agent): build.fix.quality.fixing → build.health.check
|
|
188
253
|
gtd(check): build.health.check → build.quality.closing
|
|
189
|
-
gtd(check): build.quality.closing → build.review.
|
|
190
|
-
gtd(
|
|
254
|
+
gtd(check): build.quality.closing → build.review.reviewing
|
|
255
|
+
gtd(agent): build.review.reviewing → build.review.ranking
|
|
256
|
+
gtd(judge): build.review.ranking → build.review.carrying
|
|
257
|
+
gtd(check): build.review.carrying → build.review.await-review
|
|
191
258
|
"""
|
|
192
259
|
|
|
193
260
|
Scenario: a fixer that renames a chunk it leaves counts nothing fixed
|
|
@@ -204,6 +271,32 @@ Feature: the quality lenses are the only agentic review
|
|
|
204
271
|
Still open.
|
|
205
272
|
"""
|
|
206
273
|
And step "build.health.check" changes nothing
|
|
274
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
275
|
+
"""
|
|
276
|
+
# Review: abc1234
|
|
277
|
+
|
|
278
|
+
<!-- base: 0000000 -->
|
|
279
|
+
|
|
280
|
+
## Subtract
|
|
281
|
+
|
|
282
|
+
- [ ] ./src/calc.ts#2-2 — subtract(a, b)
|
|
283
|
+
|
|
284
|
+
## subtract has no negative test
|
|
285
|
+
|
|
286
|
+
- [ ] ./src/calc.ts#2-2 — where it applies
|
|
287
|
+
|
|
288
|
+
## add could be a method
|
|
289
|
+
|
|
290
|
+
- [ ] ./src/calc.ts#2-2 — where it applies
|
|
291
|
+
|
|
292
|
+
## AC-1 ignores floating point
|
|
293
|
+
|
|
294
|
+
- [ ] ./src/calc.ts#2-2 — where it applies
|
|
295
|
+
|
|
296
|
+
## null check in parse()
|
|
297
|
+
|
|
298
|
+
- [ ] ./src/calc.ts#2-2 — where it applies
|
|
299
|
+
"""
|
|
207
300
|
When the process is driven
|
|
208
301
|
Then the process rests at "build.review.await-review"
|
|
209
302
|
And ".gtd/REVIEW.md" contains "## null check in parse()"
|
|
@@ -223,17 +316,37 @@ Feature: the quality lenses are the only agentic review
|
|
|
223
316
|
All fixed.
|
|
224
317
|
"""
|
|
225
318
|
And step "build.health.check" changes nothing
|
|
319
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
320
|
+
"""
|
|
321
|
+
# Review: abc1234
|
|
322
|
+
|
|
323
|
+
<!-- base: 0000000 -->
|
|
324
|
+
|
|
325
|
+
## Subtract
|
|
326
|
+
|
|
327
|
+
- [ ] ./src/calc.ts#2-2 — subtract(a, b)
|
|
328
|
+
"""
|
|
226
329
|
When the process is driven
|
|
227
330
|
Then the process rests at "build.review.await-review"
|
|
228
331
|
And ".gtd/REVIEW.md" contains "- subtract has no negative test ("
|
|
229
332
|
And ".gtd/REVIEW.md" does not contain "check whether it was fixed"
|
|
230
333
|
|
|
231
|
-
Scenario: signing off a
|
|
334
|
+
Scenario: signing off a review with no notes ends the process
|
|
232
335
|
Given judge step "build.quality.triage" answers:
|
|
233
336
|
"""
|
|
234
337
|
[{"id":"Q1","answer":"noise","p":0.9},{"id":"Q2","answer":"noise","p":0.9},{"id":"Q3","answer":"noise","p":0.9}]
|
|
235
338
|
"""
|
|
236
339
|
And step "build.review.closing" runs its script
|
|
340
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
341
|
+
"""
|
|
342
|
+
# Review: abc1234
|
|
343
|
+
|
|
344
|
+
<!-- base: 0000000 -->
|
|
345
|
+
|
|
346
|
+
## Subtract
|
|
347
|
+
|
|
348
|
+
- [ ] ./src/calc.ts#2-2 — subtract(a, b)
|
|
349
|
+
"""
|
|
237
350
|
When the process is driven
|
|
238
351
|
Then the process rests at "build.review.await-review"
|
|
239
352
|
When gtd lands
|
|
@@ -242,7 +355,9 @@ Feature: the quality lenses are the only agentic review
|
|
|
242
355
|
And ".gtd/REVIEW.md" does not exist
|
|
243
356
|
And the commit subjects end with:
|
|
244
357
|
"""
|
|
245
|
-
gtd(
|
|
358
|
+
gtd(agent): build.review.reviewing → build.review.ranking
|
|
359
|
+
gtd(judge): build.review.ranking → build.review.carrying
|
|
360
|
+
gtd(check): build.review.carrying → build.review.await-review
|
|
246
361
|
gtd(human): build.review.await-review → build.review.closing
|
|
247
362
|
gtd(check): build.review.closing → idle
|
|
248
363
|
"""
|
|
@@ -64,7 +64,7 @@ Feature: a naive pass the judge rules land is itself the implementation
|
|
|
64
64
|
|
|
65
65
|
Scenario: a land judgment applies the naive pass and runs from idle to the review gate with no rebuild
|
|
66
66
|
When the process is driven
|
|
67
|
-
Then the process rests at "build.review.
|
|
67
|
+
Then the process rests at "build.review.reviewing"
|
|
68
68
|
And "src/greeting.ts" contains "Hello, world"
|
|
69
69
|
And "src/acceptance.test.ts" contains "[AC-1] holds"
|
|
70
70
|
And the commit subjects end with:
|
|
@@ -81,7 +81,7 @@ Feature: a naive pass the judge rules land is itself the implementation
|
|
|
81
81
|
gtd(check): implement.health.check → implement.coverage
|
|
82
82
|
gtd(judge): implement.coverage → implement.closing
|
|
83
83
|
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
84
|
-
gtd(agent): build.quality.correctness.reviewing → build.review.
|
|
84
|
+
gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
|
|
85
85
|
"""
|
|
86
86
|
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
|
|
87
87
|
And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
|
|
@@ -113,7 +113,7 @@ Feature: a refactor that carried the whole feature still gets a quality lap over
|
|
|
113
113
|
|
|
114
114
|
Given step "build.quality.correctness.reviewing" changes nothing
|
|
115
115
|
When the process is driven
|
|
116
|
-
Then the process rests at "build.review.
|
|
116
|
+
Then the process rests at "build.review.reviewing"
|
|
117
117
|
And "src/calc.ts" contains "subtract"
|
|
118
118
|
And the commit subjects end with:
|
|
119
119
|
"""
|
|
@@ -123,7 +123,7 @@ Feature: a refactor that carried the whole feature still gets a quality lap over
|
|
|
123
123
|
gtd(check): implement.health.check → implement.coverage
|
|
124
124
|
gtd(judge): implement.coverage → implement.closing
|
|
125
125
|
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
126
|
-
gtd(agent): build.quality.correctness.reviewing → build.review.
|
|
126
|
+
gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
|
|
127
127
|
"""
|
|
128
128
|
And the output lines of gtd "slices" start with:
|
|
129
129
|
"""
|