@pmelab/gtd 55.0.0 → 57.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 +72 -80
- package/dist/chunks/{reserved-DHafcW7t.mjs → reserved-X2RkNHin.mjs} +110 -318
- package/dist/chunks/{review-BJGKwlYv.mjs → review-v7sh5GF4.mjs} +30 -125
- package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
- package/dist/chunks/{tail-B9Hw2AFB.mjs → tail-S04KEewi.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 +63 -175
- 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 +603 -727
- 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 -7
- package/src/workflows/feature/criteria.test.ts +1 -41
- package/src/workflows/feature/criteria.ts +2 -14
- package/src/workflows/feature/fast-path.feature +0 -1
- package/src/workflows/feature/feature.test.ts +1 -1
- package/src/workflows/feature/feature.ts +16 -25
- package/src/workflows/feature/flag-mode.feature +1 -1
- package/src/workflows/feature/lens-models.feature +0 -1
- package/src/workflows/feature/lens-review.feature +323 -172
- package/src/workflows/feature/naive-test.feature +258 -0
- package/src/workflows/feature/one-step-land.feature +0 -1
- package/src/workflows/feature/refactor-absorbs.feature +1 -8
- package/src/workflows/feature/refactor-first.feature +2 -11
- package/src/workflows/feature/refactor-rebuild.feature +4 -25
- package/src/workflows/feature/refactor-review.feature +33 -118
- package/src/workflows/feature/refactor.test.ts +0 -2
- package/src/workflows/feature/refactor.ts +0 -1
- package/src/workflows/feature/review-gate.feature +7 -24
- package/src/workflows/feature/reviewer-walkthrough.feature +16 -104
- package/src/workflows/feature/seam-rejection.feature +0 -1
- package/src/workflows/feature/seam.feature +0 -1
- package/src/workflows/feature/seam.test.ts +1 -2
- package/src/workflows/feature/seam.ts +56 -10
- package/src/workflows/feature/standards.feature +75 -35
- 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/options.ts +9 -1
- package/src/workflows/shared/prose.ts +0 -6
- package/src/workflows/shared/review.test.ts +27 -26
- package/src/workflows/shared/review.ts +29 -196
- package/src/workflows/shared/skills.test.ts +0 -4
- package/src/workflows/shared/skills.ts +0 -5
- package/src/workflows/shared/steps.ts +1 -1
- package/src/workflows/shared/tail.ts +18 -9
- package/src/workflows/shared/text.test.ts +125 -329
- package/src/workflows/shared/text.ts +173 -471
- package/src/workflows/shared/vars.ts +2 -3
- package/src/workflows/feature/hands-on-skip.feature +0 -316
- package/src/workflows/feature/prototype-rounds.feature +0 -156
- package/src/workflows/feature/prototype.feature +0 -428
- package/src/workflows/feature/prototype.test.ts +0 -43
- package/src/workflows/feature/prototype.ts +0 -198
- 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
|
@@ -2118,7 +2118,7 @@ describe("answer boxes", async () => {
|
|
|
2118
2118
|
})
|
|
2119
2119
|
|
|
2120
2120
|
describe("review — sectioned layout", async () => {
|
|
2121
|
-
const { reviewChunks, reviewDecisions, reviewSections,
|
|
2121
|
+
const { reviewChunks, reviewDecisions, reviewSections, unansweredNotes } =
|
|
2122
2122
|
await import("./review.js")
|
|
2123
2123
|
const boxes = (ticked?: 0 | 1 | 2): string[] => [
|
|
2124
2124
|
"Tick exactly one:",
|
|
@@ -2230,78 +2230,4 @@ describe("review — sectioned layout", async () => {
|
|
|
2230
2230
|
.map((a) => a.title)
|
|
2231
2231
|
expect(titles.some((t) => t.includes("A finding"))).toBe(false)
|
|
2232
2232
|
})
|
|
2233
|
-
|
|
2234
|
-
describe("sectionedReview", () => {
|
|
2235
|
-
const flat = [
|
|
2236
|
-
...head,
|
|
2237
|
-
"## Weakened scenarios",
|
|
2238
|
-
"",
|
|
2239
|
-
"- a.feature — s — Then x",
|
|
2240
|
-
"",
|
|
2241
|
-
"## A finding",
|
|
2242
|
-
"",
|
|
2243
|
-
"_should: contract_",
|
|
2244
|
-
"",
|
|
2245
|
-
"Leaks a token.",
|
|
2246
|
-
"",
|
|
2247
|
-
...boxes(),
|
|
2248
|
-
"",
|
|
2249
|
-
"Wiring",
|
|
2250
|
-
"------",
|
|
2251
|
-
"",
|
|
2252
|
-
"### Detail",
|
|
2253
|
-
"",
|
|
2254
|
-
"- [ ] ./src/b.ts#3-4 — wires it",
|
|
2255
|
-
"",
|
|
2256
|
-
"## Fixed automatically",
|
|
2257
|
-
"",
|
|
2258
|
-
"- f (fedcba9)",
|
|
2259
|
-
"",
|
|
2260
|
-
].join("\n")
|
|
2261
|
-
|
|
2262
|
-
it("moves boxed chunks under Findings and the named ones under Walkthrough, a level deeper", () => {
|
|
2263
|
-
expect(sectionedReview(flat, new Set(["Wiring"]))).toBe(
|
|
2264
|
-
[
|
|
2265
|
-
...head,
|
|
2266
|
-
"## Weakened scenarios",
|
|
2267
|
-
"",
|
|
2268
|
-
"- a.feature — s — Then x",
|
|
2269
|
-
"",
|
|
2270
|
-
"## Findings",
|
|
2271
|
-
"",
|
|
2272
|
-
"### A finding",
|
|
2273
|
-
"",
|
|
2274
|
-
"_should: contract_",
|
|
2275
|
-
"",
|
|
2276
|
-
"Leaks a token.",
|
|
2277
|
-
"",
|
|
2278
|
-
...boxes(),
|
|
2279
|
-
"",
|
|
2280
|
-
"## Walkthrough",
|
|
2281
|
-
"",
|
|
2282
|
-
"### Wiring",
|
|
2283
|
-
"",
|
|
2284
|
-
"#### Detail",
|
|
2285
|
-
"",
|
|
2286
|
-
"- [ ] ./src/b.ts#3-4 — wires it",
|
|
2287
|
-
"",
|
|
2288
|
-
"## Fixed automatically",
|
|
2289
|
-
"",
|
|
2290
|
-
"- f (fedcba9)",
|
|
2291
|
-
"",
|
|
2292
|
-
].join("\n"),
|
|
2293
|
-
)
|
|
2294
|
-
})
|
|
2295
|
-
|
|
2296
|
-
it("is valid and stable once sectioned", () => {
|
|
2297
|
-
const once = sectionedReview(flat, new Set(["Wiring"]))
|
|
2298
|
-
expect(review.validate(once)).toEqual([])
|
|
2299
|
-
expect(sectionedReview(once, new Set(["Wiring"]))).toBe(once)
|
|
2300
|
-
})
|
|
2301
|
-
|
|
2302
|
-
it("leaves a review with nothing to group byte for byte", () => {
|
|
2303
|
-
const plain = [...head, "## Nothing to note", "", "Review the diff yourself.", ""].join("\n")
|
|
2304
|
-
expect(sectionedReview(plain, new Set())).toBe(plain)
|
|
2305
|
-
})
|
|
2306
|
-
})
|
|
2307
2233
|
})
|
package/src/steering/review.ts
CHANGED
|
@@ -96,7 +96,7 @@ export const ANSWER_WONT_FIX = "won't fix"
|
|
|
96
96
|
|
|
97
97
|
/** A chunk's leading `_critical: security_` paragraph: the tier gtd ranked it with, and why. */
|
|
98
98
|
const SEVERITY_LINE_RE = /^_((?:critical|should|could)(?::[^_\n]*)?)_$/
|
|
99
|
-
|
|
99
|
+
const severityLine = (label: string): string => `_${label}_`
|
|
100
100
|
|
|
101
101
|
interface ReviewDoc {
|
|
102
102
|
readonly shortHash?: string
|
|
@@ -1591,76 +1591,3 @@ export const reviewSections = (
|
|
|
1591
1591
|
section: s.section,
|
|
1592
1592
|
line: s.headingLine,
|
|
1593
1593
|
}))
|
|
1594
|
-
|
|
1595
|
-
const isBlankLine = (line: string | undefined): boolean => line !== undefined && line.trim() === ""
|
|
1596
|
-
|
|
1597
|
-
/** How many lines a chunk's heading takes: two for a setext heading and its underline. */
|
|
1598
|
-
const headingSize = ({ heading, headingLine }: ChunkBlock): number =>
|
|
1599
|
-
toLspPosition(heading.position!.end).line - headingLine + 1
|
|
1600
|
-
|
|
1601
|
-
/** \`block\` with its heading an ATX \`### \` on one line, and every heading in its body one level deeper. */
|
|
1602
|
-
const demoted = (chunk: ChunkBlock, block: readonly string[]): string[] => {
|
|
1603
|
-
const out = [...block]
|
|
1604
|
-
for (const node of chunk.body) {
|
|
1605
|
-
if (node.type !== "heading" || node.depth < 3 || node.depth > 5) continue
|
|
1606
|
-
const at = toLspPosition(node.position!.start).line - chunk.headingLine
|
|
1607
|
-
out[at] = out[at]!.replace(/^( {0,3})#/, "$1##")
|
|
1608
|
-
}
|
|
1609
|
-
const size = headingSize(chunk)
|
|
1610
|
-
const title = size > 1 ? `### ${out[0]!.trim()}` : out[0]!.replace(/^ {0,3}##/, "###")
|
|
1611
|
-
return [title, ...out.slice(size)]
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
/** A chunk's \`size\` heading lines, then its body with the blank lines around it trimmed. */
|
|
1615
|
-
const chunkText = (block: readonly string[], size: number): string => {
|
|
1616
|
-
const rest = block.slice(size)
|
|
1617
|
-
while (rest.length > 0 && isBlankLine(rest[0])) rest.shift()
|
|
1618
|
-
while (rest.length > 0 && isBlankLine(rest.at(-1))) rest.pop()
|
|
1619
|
-
const heading = block.slice(0, size).join("\n")
|
|
1620
|
-
return rest.length === 0 ? heading : `${heading}\n\n${rest.join("\n")}`
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
/**
|
|
1624
|
-
* A flat review regrouped: every chunk with answer boxes under `## Findings`
|
|
1625
|
-
* and every chunk titled in `walkthrough` under `## Walkthrough`, each in its
|
|
1626
|
-
* current order and one heading level deeper. Any other chunk stays a `## ` of
|
|
1627
|
-
* its own, before the sections when it came before them, after otherwise. An
|
|
1628
|
-
* already sectioned review comes back unchanged.
|
|
1629
|
-
*/
|
|
1630
|
-
export const sectionedReview = (content: string, walkthrough: ReadonlySet<string>): string => {
|
|
1631
|
-
const tree = parseMarkdown(content)
|
|
1632
|
-
const { chunks, sections } = splitChunks(tree, content)
|
|
1633
|
-
if (sections.length > 0 || chunks.length === 0) return content
|
|
1634
|
-
const { changesets } = parseReviewDoc(content)
|
|
1635
|
-
const lines = content.split("\n")
|
|
1636
|
-
const sectionOfChunk = (i: number): ReviewSection | undefined =>
|
|
1637
|
-
changesets[i]!.answers !== undefined
|
|
1638
|
-
? "findings"
|
|
1639
|
-
: walkthrough.has(changesets[i]!.title)
|
|
1640
|
-
? "walkthrough"
|
|
1641
|
-
: undefined
|
|
1642
|
-
const text = (i: number, demote: boolean): string => {
|
|
1643
|
-
const chunk = chunks[i]!
|
|
1644
|
-
const block = lines.slice(chunk.headingLine, chunks[i + 1]?.headingLine ?? lines.length)
|
|
1645
|
-
return chunkText(demote ? demoted(chunk, block) : block, demote ? 1 : headingSize(chunk))
|
|
1646
|
-
}
|
|
1647
|
-
const grouped = chunks.map((_, i) => sectionOfChunk(i))
|
|
1648
|
-
const first = grouped.findIndex((g) => g !== undefined)
|
|
1649
|
-
if (first === -1) return content
|
|
1650
|
-
const others = (keep: (i: number) => boolean): string[] =>
|
|
1651
|
-
chunks.flatMap((_, i) => (grouped[i] === undefined && keep(i) ? [text(i, false)] : []))
|
|
1652
|
-
const section = (s: ReviewSection): string[] => {
|
|
1653
|
-
const members = chunks.flatMap((_, i) => (grouped[i] === s ? [text(i, true)] : []))
|
|
1654
|
-
return members.length === 0 ? [] : [`## ${REVIEW_SECTIONS[s]}`, ...members]
|
|
1655
|
-
}
|
|
1656
|
-
const head = lines.slice(0, chunks[0]!.headingLine).join("\n").trim()
|
|
1657
|
-
return `${[
|
|
1658
|
-
head,
|
|
1659
|
-
...others((i) => i < first),
|
|
1660
|
-
...section("findings"),
|
|
1661
|
-
...section("walkthrough"),
|
|
1662
|
-
...others((i) => i > first),
|
|
1663
|
-
]
|
|
1664
|
-
.filter((p) => p !== "")
|
|
1665
|
-
.join("\n\n")}\n`
|
|
1666
|
-
}
|
|
@@ -7,7 +7,6 @@ Feature: the criteria-coverage lens checks the change's tests against its criter
|
|
|
7
7
|
|
|
8
8
|
Background:
|
|
9
9
|
Given the shipped workflows, as gtd install scaffolds them
|
|
10
|
-
And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
|
|
11
10
|
And an environment variable "GTD_QUALITYREVIEWS" set to "criteria-coverage"
|
|
12
11
|
And an environment variable "GTD_FLAGMODE" set to "false"
|
|
13
12
|
And a file "src/calc.ts" with:
|
|
@@ -88,10 +87,6 @@ Feature: the criteria-coverage lens checks the change's tests against its criter
|
|
|
88
87
|
|
|
89
88
|
- [ ] ./src/calc.ts#2-2 — subtract(a, b)
|
|
90
89
|
"""
|
|
91
|
-
And judge step "build.review.ranking" answers:
|
|
92
|
-
"""
|
|
93
|
-
[]
|
|
94
|
-
"""
|
|
95
90
|
And step "build.review.carrying" runs its script
|
|
96
91
|
When the process is driven
|
|
97
92
|
Then the process rests at "build.review.await-review"
|
|
@@ -103,7 +98,6 @@ Feature: the criteria-coverage lens checks the change's tests against its criter
|
|
|
103
98
|
gtd(judge): build.quality.triage → build.fix.quality.fixing
|
|
104
99
|
gtd(agent): build.fix.quality.fixing → build.health.check
|
|
105
100
|
gtd(check): build.health.check → build.review.reviewing
|
|
106
|
-
gtd(agent): build.review.reviewing → build.review.
|
|
107
|
-
gtd(judge): build.review.ranking → build.review.carrying
|
|
101
|
+
gtd(agent): build.review.reviewing → build.review.carrying
|
|
108
102
|
gtd(check): build.review.carrying → build.review.await-review
|
|
109
103
|
"""
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import type { Change } from "../../flows/index.js"
|
|
3
|
-
import {
|
|
4
|
-
criteriaOf,
|
|
5
|
-
criterionIds,
|
|
6
|
-
handsOnIds,
|
|
7
|
-
hasImpact,
|
|
8
|
-
statedCriteria,
|
|
9
|
-
unstableIds,
|
|
10
|
-
} from "./criteria.js"
|
|
3
|
+
import { criteriaOf, criterionIds, hasImpact, statedCriteria, unstableIds } from "./criteria.js"
|
|
11
4
|
|
|
12
5
|
const added = (path: string, after: string): Change => ({
|
|
13
6
|
path,
|
|
@@ -55,17 +48,6 @@ describe("criteriaOf", () => {
|
|
|
55
48
|
).toEqual([{ id: "AC-1", text: "exits 2 when config is bad - missing file" }])
|
|
56
49
|
})
|
|
57
50
|
|
|
58
|
-
it("reads the (no hands-on) tag between an ID and its colon", () => {
|
|
59
|
-
expect(
|
|
60
|
-
criteriaOf(
|
|
61
|
-
"## Acceptance Criteria\n\n- AC-1: runs\n- AC-2 (no hands-on): the prompt says so\n",
|
|
62
|
-
),
|
|
63
|
-
).toEqual([
|
|
64
|
-
{ id: "AC-1", text: "runs" },
|
|
65
|
-
{ id: "AC-2", text: "the prompt says so", noHandsOn: true },
|
|
66
|
-
])
|
|
67
|
-
})
|
|
68
|
-
|
|
69
51
|
it("is undefined without the section", () => {
|
|
70
52
|
expect(criteriaOf("# R\n\n- AC-1: x\n")).toBeUndefined()
|
|
71
53
|
})
|
|
@@ -84,19 +66,6 @@ describe("statedCriteria", () => {
|
|
|
84
66
|
})
|
|
85
67
|
})
|
|
86
68
|
|
|
87
|
-
it("keeps a stated (no hands-on) tag, renumbered or not", () => {
|
|
88
|
-
const tagged = "## Acceptance criteria\n\n- AC-1: a\n- AC-2 (no hands-on): b\n"
|
|
89
|
-
expect(statedCriteria([added("TODO.md", tagged)], "abc")?.criteria).toEqual([
|
|
90
|
-
{ id: "AC-1", text: "a" },
|
|
91
|
-
{ id: "AC-2", text: "b", noHandsOn: true },
|
|
92
|
-
])
|
|
93
|
-
const partial = "## Acceptance criteria\n\n- a\n- AC-7 (no hands-on): b\n"
|
|
94
|
-
expect(statedCriteria([added("TODO.md", partial)], "abc")?.criteria).toEqual([
|
|
95
|
-
{ id: "AC-1", text: "a" },
|
|
96
|
-
{ id: "AC-2", text: "b", noHandsOn: true },
|
|
97
|
-
])
|
|
98
|
-
})
|
|
99
|
-
|
|
100
69
|
it("renumbers when the stated IDs are partial or clash", () => {
|
|
101
70
|
const sketch = [added("TODO.md", "## Acceptance criteria\n\n- AC-4: a\n- b\n")]
|
|
102
71
|
expect(statedCriteria(sketch, "abc")?.criteria).toEqual([
|
|
@@ -144,15 +113,6 @@ describe("criterionIds", () => {
|
|
|
144
113
|
})
|
|
145
114
|
})
|
|
146
115
|
|
|
147
|
-
describe("handsOnIds", () => {
|
|
148
|
-
const requirements =
|
|
149
|
-
"## Acceptance Criteria\n\n- AC-1: a\n- AC-2 (no hands-on): b\n- AC-3: c\n- AC-4 (no hands-on): d\n"
|
|
150
|
-
|
|
151
|
-
it("leaves out every criterion tagged (no hands-on)", () => {
|
|
152
|
-
expect(handsOnIds(requirements)).toEqual(["AC-1", "AC-3"])
|
|
153
|
-
})
|
|
154
|
-
})
|
|
155
|
-
|
|
156
116
|
describe("unstableIds", () => {
|
|
157
117
|
const ids = { closed: ["AC-1", "AC-3"], used: ["AC-1", "AC-2", "AC-3"] }
|
|
158
118
|
|
|
@@ -17,8 +17,6 @@ import * as t from "../shared/text.js"
|
|
|
17
17
|
export interface Criterion {
|
|
18
18
|
readonly id: string
|
|
19
19
|
readonly text: string
|
|
20
|
-
/** Tagged `(no hands-on)`: its tests prove it, so the hands-on test skips it. */
|
|
21
|
-
readonly noHandsOn?: true
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
/** Acceptance criteria the human's sketch stated outright, ready to normalise. */
|
|
@@ -33,13 +31,9 @@ export interface StatedCriteria {
|
|
|
33
31
|
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
34
32
|
const CLEAR_MIN_P = 0.8
|
|
35
33
|
|
|
36
|
-
const ID = /^(AC-\d+)
|
|
34
|
+
const ID = /^(AC-\d+):\s*(.*)$/
|
|
37
35
|
|
|
38
|
-
const criterion = (id: RegExpExecArray, as = id[1]!): Criterion => ({
|
|
39
|
-
id: as,
|
|
40
|
-
text: id[3]!,
|
|
41
|
-
...(id[2] === undefined ? {} : { noHandsOn: true }),
|
|
42
|
-
})
|
|
36
|
+
const criterion = (id: RegExpExecArray, as = id[1]!): Criterion => ({ id: as, text: id[2]! })
|
|
43
37
|
|
|
44
38
|
/** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
|
|
45
39
|
const listUnder = (text: string, title: RegExp, level?: number): string[] | undefined => {
|
|
@@ -83,12 +77,6 @@ export const criteriaOf = (text: string): readonly Criterion[] | undefined =>
|
|
|
83
77
|
export const criterionIds = (text: string): string[] =>
|
|
84
78
|
(criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "")
|
|
85
79
|
|
|
86
|
-
/** The ID'd criteria of `text` a person can try by hand: none tagged `(no hands-on)`. */
|
|
87
|
-
export const handsOnIds = (text: string): string[] => {
|
|
88
|
-
const tagged = (criteriaOf(text) ?? []).filter((c) => c.noHandsOn).map((c) => c.id)
|
|
89
|
-
return criterionIds(text).filter((id) => !tagged.includes(id))
|
|
90
|
-
}
|
|
91
|
-
|
|
92
80
|
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
93
81
|
const numbered = (items: readonly string[]): Criterion[] => {
|
|
94
82
|
const ids = items.map((item) => ID.exec(item))
|
|
@@ -8,7 +8,6 @@ Feature: a trivial change takes the same few beats at either risk tier
|
|
|
8
8
|
|
|
9
9
|
Background:
|
|
10
10
|
Given the shipped workflows, as gtd install scaffolds them
|
|
11
|
-
And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
|
|
12
11
|
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
|
|
13
12
|
And an environment variable "GTD_FLAG_RISK_TIER" set to "1"
|
|
14
13
|
And a file "src/greeting.ts" with:
|
|
@@ -91,7 +91,7 @@ describe("afterTail feedback lap", () => {
|
|
|
91
91
|
await repo.drive({
|
|
92
92
|
"review-gate.check": () => undefined,
|
|
93
93
|
"build.health.check": () => undefined,
|
|
94
|
-
"build.review.
|
|
94
|
+
"build.review.reviewing": (_, r) => r.write(".gtd/REVIEW.md", review()),
|
|
95
95
|
})
|
|
96
96
|
repo.write(".gtd/REVIEW.md", review("also export multiply"))
|
|
97
97
|
await repo.land()
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
start,
|
|
10
10
|
} from "../../flows/index.js"
|
|
11
11
|
import { gate } from "../shared/health.js"
|
|
12
|
-
import { flagModeOn, shipped } from "../shared/options.js"
|
|
12
|
+
import { flagModeOn, naiveTestOn, shipped } from "../shared/options.js"
|
|
13
13
|
import { FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
|
|
14
|
-
import { refusePendingHandOff
|
|
14
|
+
import { refusePendingHandOff } from "../shared/tail.js"
|
|
15
15
|
import * as t from "../shared/text.js"
|
|
16
16
|
import {
|
|
17
17
|
criterionIds,
|
|
@@ -22,20 +22,18 @@ import {
|
|
|
22
22
|
type StatedCriteria,
|
|
23
23
|
} from "./criteria.js"
|
|
24
24
|
import { design } from "./planning.js"
|
|
25
|
-
import { buildTail,
|
|
26
|
-
import { prototypeRounds } from "./prototype.js"
|
|
25
|
+
import { buildTail, isCode, reUnwind, type ReviewOutcome } from "../shared/review.js"
|
|
27
26
|
import { implement, implementing, seamBuild } from "./seam.js"
|
|
28
27
|
import { recordRisk, riskTierOn } from "./tier.js"
|
|
29
28
|
|
|
30
29
|
// The ordinary start. Any change to the tree starts a process: `idle` →
|
|
31
30
|
// `unwind` reverts the sketch (its intent survives in history) → a
|
|
32
|
-
// green-baseline gate → design → a naive pass,
|
|
31
|
+
// green-baseline gate → design → a naive pass, tried by a person with
|
|
32
|
+
// `naiveTest` on and judged with `flagMode` on: it
|
|
33
33
|
// lands as the implementation, or (refactoring first when the judge asks) is
|
|
34
|
-
// built fresh from → the health loop →
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// chunks, for the human, which signs off (the episode ends back at `idle`) or
|
|
38
|
-
// sends a design → implement lap.
|
|
34
|
+
// built fresh from → the health loop → the triaged quality lap and its finding gate → a
|
|
35
|
+
// reviewer's walkthrough of the whole change for the human, which signs off
|
|
36
|
+
// (the episode ends back at `idle`) or sends a design → implement lap.
|
|
39
37
|
|
|
40
38
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
41
39
|
export const unwind = (): Promise<void> => {
|
|
@@ -74,7 +72,7 @@ export const buildFeature = async (firstBase: string): Promise<void> => {
|
|
|
74
72
|
!changesSince(seam.base).some((c) => isCode(c.path) && !carried.includes(c.path))
|
|
75
73
|
? firstBase
|
|
76
74
|
: seam.base
|
|
77
|
-
const outcome = await
|
|
75
|
+
const outcome = await buildTail(base, { green: true, lensBase: base, requirements })
|
|
78
76
|
await featureFeedback(outcome, base, { closed, used })
|
|
79
77
|
}
|
|
80
78
|
|
|
@@ -85,18 +83,6 @@ const gather = (handedOut: Set<string>): readonly string[] => {
|
|
|
85
83
|
return [...handedOut]
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
/** The quality lap, then a reviewer's walkthrough since `base` for the human. */
|
|
89
|
-
const reviewedTail = (
|
|
90
|
-
base: string,
|
|
91
|
-
lensBase: string,
|
|
92
|
-
requirements: string | undefined,
|
|
93
|
-
lap: "first" | "feedback" = "first",
|
|
94
|
-
): Promise<ReviewOutcome> =>
|
|
95
|
-
tail(
|
|
96
|
-
(escalations, report) => humanReview(base, escalations, { report, walkthrough: true, lap }),
|
|
97
|
-
{ green: true, lensBase, requirements },
|
|
98
|
-
)
|
|
99
|
-
|
|
100
86
|
// seam.naive / seam.judge never run on a feedback lap: its design → implement is built unjudged
|
|
101
87
|
// and unrefactored.
|
|
102
88
|
const featureFeedback = async (
|
|
@@ -118,7 +104,12 @@ const featureFeedback = async (
|
|
|
118
104
|
const tier = riskTierOn() ? recordRisk() : undefined
|
|
119
105
|
const requirements = read(REQUIREMENTS)
|
|
120
106
|
closed = await implementing({ tier, amend: true })
|
|
121
|
-
outcome = await
|
|
107
|
+
outcome = await buildTail(outcome.base, {
|
|
108
|
+
green: true,
|
|
109
|
+
lensBase,
|
|
110
|
+
requirements,
|
|
111
|
+
lap: "feedback",
|
|
112
|
+
})
|
|
122
113
|
}
|
|
123
114
|
}
|
|
124
115
|
|
|
@@ -149,7 +140,7 @@ export const ordinaryStart = async (): Promise<void> => {
|
|
|
149
140
|
refusePendingHandOff()
|
|
150
141
|
// Read right after idle, so a bad value refuses the landing that would pin it.
|
|
151
142
|
flagModeOn()
|
|
152
|
-
|
|
143
|
+
naiveTestOn()
|
|
153
144
|
const stated = statedCriteria(changes(), head())
|
|
154
145
|
await unwind()
|
|
155
146
|
if (changes(FEEDBACK).some((c) => c.status !== "deleted")) {
|
|
@@ -11,7 +11,7 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
|
|
|
11
11
|
"""
|
|
12
12
|
import { feature } from "@pmelab/gtd/workflows/feature"
|
|
13
13
|
export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
|
|
14
|
-
export default feature({ flagMode: false
|
|
14
|
+
export default feature({ flagMode: false })
|
|
15
15
|
"""
|
|
16
16
|
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
|
|
17
17
|
And a file "src/calc.ts" with:
|
|
@@ -12,7 +12,6 @@ Feature: each quality lens runs on its own model hint
|
|
|
12
12
|
export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
|
|
13
13
|
export default feature({
|
|
14
14
|
flagMode: false,
|
|
15
|
-
prototypeRounds: "0",
|
|
16
15
|
qualityReviews: "correctness, conventions, test-audit=opus",
|
|
17
16
|
})
|
|
18
17
|
"""
|