@pmelab/gtd 51.0.0 → 52.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 +41 -38
- package/dist/chunks/{reserved-CUkk9XgL.mjs → reserved-D0MKVFFa.mjs} +28 -34
- package/dist/chunks/{review-BaCXlIT2.mjs → review-ZzUQH1KZ.mjs} +11 -9
- package/dist/chunks/{steering-DtUVUz9l.mjs → steering-CLhu8C7q.mjs} +262 -52
- package/dist/chunks/{tail-CX6Pdsql.mjs → tail-I1-vVq0P.mjs} +1 -1
- package/dist/gtd.bundle.mjs +224 -68
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +27 -26
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +510 -147
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +181 -222
- 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 +478 -313
- package/package.json +1 -1
- package/src/edge/Edge.ts +1 -30
- package/src/replay/Replay.test.ts +4 -8
- package/src/replay/Replay.ts +35 -3
- package/src/replay/index.ts +7 -1
- package/src/steering/FORMATS.md +4 -1
- package/src/steering/SteeringFormat.ts +7 -1
- package/src/steering/contract.test.ts +2 -1
- package/src/steering/fixtures/review/sectioned.md +31 -0
- package/src/steering/fixtures/review/sectioned.read.json +119 -0
- package/src/steering/index.ts +4 -0
- package/src/steering/qa.ts +1 -1
- package/src/steering/review.test.ts +270 -4
- package/src/steering/review.ts +328 -69
- package/src/workflows/feature/criteria.test.ts +39 -1
- package/src/workflows/feature/criteria.ts +21 -7
- package/src/workflows/feature/fast-path.feature +1 -6
- package/src/workflows/feature/feature.ts +1 -2
- package/src/workflows/feature/flag-mode.feature +1 -6
- package/src/workflows/feature/green.ts +22 -0
- package/src/workflows/feature/hands-on-skip.feature +344 -0
- package/src/workflows/feature/lens-models.feature +0 -4
- package/src/workflows/feature/lens-review.feature +6 -8
- package/src/workflows/feature/one-step-land.feature +1 -6
- package/src/workflows/feature/prototype-rounds.feature +1 -1
- package/src/workflows/feature/prototype.feature +10 -9
- package/src/workflows/feature/prototype.ts +48 -17
- package/src/workflows/feature/refactor-absorbs.feature +3 -8
- package/src/workflows/feature/refactor-first.feature +3 -8
- package/src/workflows/feature/refactor-rebuild.feature +7 -12
- package/src/workflows/feature/refactor-review.feature +52 -72
- package/src/workflows/feature/review-answers.feature +90 -5
- package/src/workflows/feature/review-gate.feature +11 -53
- package/src/workflows/feature/reviewer-walkthrough.feature +29 -12
- package/src/workflows/feature/seam-rejection.feature +1 -6
- package/src/workflows/feature/seam.feature +1 -6
- package/src/workflows/feature/seam.test.ts +1 -1
- package/src/workflows/feature/seam.ts +2 -3
- package/src/workflows/feature/severity-budget.feature +7 -7
- package/src/workflows/feature/severity-sorted-review.feature +50 -54
- package/src/workflows/feature/slice-findings.feature +5 -15
- package/src/workflows/feature/slicing.feature +4 -24
- package/src/workflows/feature/standards.feature +0 -4
- package/src/workflows/shared/review.ts +26 -6
- package/src/workflows/shared/text.test.ts +45 -11
- package/src/workflows/shared/text.ts +45 -38
- package/src/workflows/feature/coverage.feature +0 -165
- package/src/workflows/feature/coverage.ts +0 -120
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import { FOOTNOTE_ACTION_TITLE, steeringFormatFor, viewOf } from "./index.js"
|
|
3
|
-
import { parseReviewDoc } from "./review.js"
|
|
3
|
+
import { parseReviewDoc, unansweredNotes } from "./review.js"
|
|
4
4
|
import { getParseCount } from "./index.js"
|
|
5
5
|
|
|
6
6
|
describe("parseReviewDoc", () => {
|
|
@@ -1532,13 +1532,90 @@ describe("review.apply", () => {
|
|
|
1532
1532
|
).toEqual({ ok: false, reason: "anchor-not-found" })
|
|
1533
1533
|
})
|
|
1534
1534
|
|
|
1535
|
-
it("a question
|
|
1535
|
+
it("a question anchor — not this format's own kind — refuses anchor-not-found", () => {
|
|
1536
1536
|
expect(review.apply(NESTED_CONTENT, { kind: "question", index: 0 }, { checked: true })).toEqual(
|
|
1537
1537
|
{ ok: false, reason: "anchor-not-found" },
|
|
1538
1538
|
)
|
|
1539
1539
|
})
|
|
1540
1540
|
})
|
|
1541
1541
|
|
|
1542
|
+
describe("review decisions — a finding's answer boxes", () => {
|
|
1543
|
+
const FINDINGS = [
|
|
1544
|
+
"# Review: abc1234",
|
|
1545
|
+
"",
|
|
1546
|
+
"<!-- base: abc1234def5678901234567890123456789abcd -->",
|
|
1547
|
+
"",
|
|
1548
|
+
"## Findings",
|
|
1549
|
+
"",
|
|
1550
|
+
"### Token check",
|
|
1551
|
+
"",
|
|
1552
|
+
"- [ ] ./src/auth.ts#1-2",
|
|
1553
|
+
"",
|
|
1554
|
+
"Tick exactly one:",
|
|
1555
|
+
"",
|
|
1556
|
+
"- [ ] fix as suggested",
|
|
1557
|
+
"- [x] won't fix",
|
|
1558
|
+
"- [ ] _your answer_",
|
|
1559
|
+
"",
|
|
1560
|
+
"## Walkthrough",
|
|
1561
|
+
"",
|
|
1562
|
+
"### Rename",
|
|
1563
|
+
"",
|
|
1564
|
+
"- [ ] ./src/calc.ts#1-1",
|
|
1565
|
+
"",
|
|
1566
|
+
].join("\n")
|
|
1567
|
+
const option = (index: number) => ({ kind: "option" as const, questionIndex: 0, index })
|
|
1568
|
+
const write = (content: string, index: number, opts: { checked?: boolean; text?: string }) => {
|
|
1569
|
+
const result = review.apply(content, option(index), opts)
|
|
1570
|
+
if (!result.ok) throw new Error(result.reason)
|
|
1571
|
+
return applyEdits(content, result.edits)
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
it("views a finding's boxes as its decision, apart from its hunks", () => {
|
|
1575
|
+
const [finding, walkthrough] = review.view(FINDINGS).nodes
|
|
1576
|
+
expect(finding!.answered).toBe(true)
|
|
1577
|
+
expect(finding!.decision!.map((o) => [o.title, o.checked])).toEqual([
|
|
1578
|
+
["fix as suggested", false],
|
|
1579
|
+
["won't fix", true],
|
|
1580
|
+
["", false],
|
|
1581
|
+
])
|
|
1582
|
+
expect(finding!.children!.map((c) => c.anchor.kind)).toEqual(["hunk"])
|
|
1583
|
+
expect(walkthrough!.decision).toBeUndefined()
|
|
1584
|
+
expect(walkthrough!.answered).toBeUndefined()
|
|
1585
|
+
})
|
|
1586
|
+
|
|
1587
|
+
it("ticking a box unticks the others", () => {
|
|
1588
|
+
const after = write(FINDINGS, 0, { checked: true })
|
|
1589
|
+
expect(after).toContain("- [x] fix as suggested\n- [ ] won't fix\n- [ ] _your answer_")
|
|
1590
|
+
expect(unansweredNotes(after)).toEqual([])
|
|
1591
|
+
})
|
|
1592
|
+
|
|
1593
|
+
it("an answer ticks the free-text slot and writes its text in one edit set", () => {
|
|
1594
|
+
const after = write(FINDINGS, 2, { checked: true, text: "Only for\n admin tokens " })
|
|
1595
|
+
expect(after).toContain("- [ ] won't fix\n- [x] Only for admin tokens\n")
|
|
1596
|
+
expect(review.view(after).nodes[0]!.decision![2]!.title).toBe("Only for admin tokens")
|
|
1597
|
+
expect(unansweredNotes(after)).toEqual([])
|
|
1598
|
+
expect(review.validate(after)).toEqual([])
|
|
1599
|
+
})
|
|
1600
|
+
|
|
1601
|
+
it("clearing the answer unticks the slot and restores its placeholder", () => {
|
|
1602
|
+
const answered = write(FINDINGS, 2, { checked: true, text: "Later" })
|
|
1603
|
+
const cleared = write(answered, 2, { checked: false, text: "" })
|
|
1604
|
+
expect(cleared).toContain("- [ ] won't fix\n- [ ] _your answer_\n")
|
|
1605
|
+
expect(review.view(cleared).nodes[0]!.answered).toBe(false)
|
|
1606
|
+
})
|
|
1607
|
+
|
|
1608
|
+
it("a walkthrough chunk or a missing box refuses anchor-not-found", () => {
|
|
1609
|
+
expect(
|
|
1610
|
+
review.apply(FINDINGS, { kind: "option", questionIndex: 1, index: 0 }, { checked: true }),
|
|
1611
|
+
).toEqual({ ok: false, reason: "anchor-not-found" })
|
|
1612
|
+
expect(review.apply(FINDINGS, option(3), { checked: true })).toEqual({
|
|
1613
|
+
ok: false,
|
|
1614
|
+
reason: "anchor-not-found",
|
|
1615
|
+
})
|
|
1616
|
+
})
|
|
1617
|
+
})
|
|
1618
|
+
|
|
1542
1619
|
describe("review.view — chunk-level footnote projection", () => {
|
|
1543
1620
|
it("projects a footnote marker on the chunk's own heading line as that chunk node's own `note`, distinct from a hunk's own note", () => {
|
|
1544
1621
|
// review.sample carries exactly this shape: `## Sample chunk[^naduiqc4]`
|
|
@@ -1899,7 +1976,7 @@ describe("reviewChunks", async () => {
|
|
|
1899
1976
|
"",
|
|
1900
1977
|
"<!-- base: abc1234def5678901234567890123456789abcd -->",
|
|
1901
1978
|
"",
|
|
1902
|
-
"##
|
|
1979
|
+
"## Changes",
|
|
1903
1980
|
"",
|
|
1904
1981
|
"- [ ] ./src/a.ts#1-3 — first",
|
|
1905
1982
|
"- [ ] ./src/b.ts#4-6 — second",
|
|
@@ -1914,7 +1991,7 @@ describe("reviewChunks", async () => {
|
|
|
1914
1991
|
].join("\n")
|
|
1915
1992
|
expect(reviewChunks(content)).toEqual([
|
|
1916
1993
|
{
|
|
1917
|
-
title: "
|
|
1994
|
+
title: "Changes",
|
|
1918
1995
|
line: 4,
|
|
1919
1996
|
pointers: 2,
|
|
1920
1997
|
files: [
|
|
@@ -2039,3 +2116,192 @@ describe("answer boxes", async () => {
|
|
|
2039
2116
|
])
|
|
2040
2117
|
})
|
|
2041
2118
|
})
|
|
2119
|
+
|
|
2120
|
+
describe("review — sectioned layout", async () => {
|
|
2121
|
+
const { reviewChunks, reviewDecisions, reviewSections, sectionedReview, unansweredNotes } =
|
|
2122
|
+
await import("./review.js")
|
|
2123
|
+
const boxes = (ticked?: 0 | 1 | 2): string[] => [
|
|
2124
|
+
"Tick exactly one:",
|
|
2125
|
+
"",
|
|
2126
|
+
`- [${ticked === 0 ? "x" : " "}] fix as suggested`,
|
|
2127
|
+
`- [${ticked === 1 ? "x" : " "}] won't fix`,
|
|
2128
|
+
`- [${ticked === 2 ? "x" : " "}] _your answer_`,
|
|
2129
|
+
]
|
|
2130
|
+
const head = [
|
|
2131
|
+
"# Review: abc1234",
|
|
2132
|
+
"",
|
|
2133
|
+
"<!-- base: abc1234def5678901234567890123456789abcd -->",
|
|
2134
|
+
"",
|
|
2135
|
+
]
|
|
2136
|
+
const sectioned = (ticked?: 0 | 1 | 2): string =>
|
|
2137
|
+
[
|
|
2138
|
+
...head,
|
|
2139
|
+
"## Findings",
|
|
2140
|
+
"",
|
|
2141
|
+
"### A finding",
|
|
2142
|
+
"",
|
|
2143
|
+
"_critical: security_",
|
|
2144
|
+
"",
|
|
2145
|
+
"Leaks a token.",
|
|
2146
|
+
"",
|
|
2147
|
+
"- [ ] ./src/a.ts#1-2 — here",
|
|
2148
|
+
"",
|
|
2149
|
+
...boxes(ticked),
|
|
2150
|
+
"",
|
|
2151
|
+
"## Walkthrough",
|
|
2152
|
+
"",
|
|
2153
|
+
"### Wiring",
|
|
2154
|
+
"",
|
|
2155
|
+
"- [x] ./src/b.ts#3-4 — wires it",
|
|
2156
|
+
"",
|
|
2157
|
+
"## Fixed automatically",
|
|
2158
|
+
"",
|
|
2159
|
+
"- f (fedcba9)",
|
|
2160
|
+
"",
|
|
2161
|
+
].join("\n")
|
|
2162
|
+
|
|
2163
|
+
it("reads `###` chunks under the sections and other `## ` chunks on their own", () => {
|
|
2164
|
+
expect(review.validate(sectioned())).toEqual([])
|
|
2165
|
+
expect(reviewChunks(sectioned())).toEqual([
|
|
2166
|
+
expect.objectContaining({ title: "A finding", line: 6, pointers: 1, section: "findings" }),
|
|
2167
|
+
expect.objectContaining({ title: "Wiring", line: 22, pointers: 1, section: "walkthrough" }),
|
|
2168
|
+
{ title: "Fixed automatically", line: 26, pointers: 0, files: [] },
|
|
2169
|
+
])
|
|
2170
|
+
expect(reviewSections(sectioned())).toEqual([
|
|
2171
|
+
{ section: "findings", line: 4 },
|
|
2172
|
+
{ section: "walkthrough", line: 20 },
|
|
2173
|
+
])
|
|
2174
|
+
})
|
|
2175
|
+
|
|
2176
|
+
it("reads a decision from a finding", () => {
|
|
2177
|
+
expect(unansweredNotes(sectioned())).toEqual([
|
|
2178
|
+
{ title: "A finding", line: 7, reason: "tick exactly one box" },
|
|
2179
|
+
])
|
|
2180
|
+
expect(reviewDecisions(sectioned(), sectioned(1))).toEqual([
|
|
2181
|
+
expect.objectContaining({ anchor: "A finding", choice: "wontfix" }),
|
|
2182
|
+
])
|
|
2183
|
+
})
|
|
2184
|
+
|
|
2185
|
+
it("reads no decision in the walkthrough: boxes there are a chunk gone wrong", () => {
|
|
2186
|
+
const content = sectioned(0).replace(
|
|
2187
|
+
"- [x] ./src/b.ts#3-4 — wires it\n",
|
|
2188
|
+
["- [x] ./src/b.ts#3-4 — wires it", "", ...boxes(1), ""].join("\n"),
|
|
2189
|
+
)
|
|
2190
|
+
expect(unansweredNotes(content)).toEqual([])
|
|
2191
|
+
expect(reviewDecisions(sectioned(), content)).toEqual([
|
|
2192
|
+
expect.objectContaining({ anchor: "A finding", choice: "fix" }),
|
|
2193
|
+
])
|
|
2194
|
+
})
|
|
2195
|
+
|
|
2196
|
+
it("still reads the flat layout, every `## ` a chunk and its boxes a decision", () => {
|
|
2197
|
+
const flat = [...head, "## A finding", "", "Leaks a token.", "", ...boxes(), ""].join("\n")
|
|
2198
|
+
expect(review.validate(flat)).toEqual([])
|
|
2199
|
+
expect(reviewChunks(flat)).toEqual([{ title: "A finding", line: 4, pointers: 0, files: [] }])
|
|
2200
|
+
expect(unansweredNotes(flat)).toHaveLength(1)
|
|
2201
|
+
})
|
|
2202
|
+
|
|
2203
|
+
it("outlines the sections as containers, each finding's decision a node of its own", () => {
|
|
2204
|
+
const outline = viewOf(review, sectioned()).outline
|
|
2205
|
+
expect(outline.map((n) => n.name)).toEqual(["Findings", "Walkthrough", "Fixed automatically"])
|
|
2206
|
+
expect(outline[0]!.children!.map((n) => n.name)).toEqual(["A finding (0/1)"])
|
|
2207
|
+
expect(outline[0]!.children![0]!.children!.map((n) => n.name)).toEqual(["Decision: open"])
|
|
2208
|
+
expect(outline[0]!.children![0]!.children![0]!.selectionRange.start.line).toBe(14)
|
|
2209
|
+
// A fully ticked walkthrough chunk is reviewed: its section is left empty.
|
|
2210
|
+
expect(outline[1]!.children).toEqual([])
|
|
2211
|
+
})
|
|
2212
|
+
|
|
2213
|
+
it("names the ticked decision, and keeps a decided finding while a hunk is unticked", () => {
|
|
2214
|
+
const outline = viewOf(review, sectioned(1)).outline
|
|
2215
|
+
expect(outline[0]!.children![0]!.children!.map((n) => n.name)).toEqual(["Decision: won't fix"])
|
|
2216
|
+
})
|
|
2217
|
+
|
|
2218
|
+
it("groups the view's chunks by section, anchors unchanged", () => {
|
|
2219
|
+
const { nodes } = review.view(sectioned())
|
|
2220
|
+
expect(nodes.map((n) => [n.title, n.group, n.anchor])).toEqual([
|
|
2221
|
+
["A finding", "Findings", { kind: "chunk", index: 0 }],
|
|
2222
|
+
["Wiring", "Walkthrough", { kind: "chunk", index: 1 }],
|
|
2223
|
+
["Fixed automatically", undefined, { kind: "chunk", index: 2 }],
|
|
2224
|
+
])
|
|
2225
|
+
})
|
|
2226
|
+
|
|
2227
|
+
it("offers a chunk's toggle only within its own span, never on the next section's heading", () => {
|
|
2228
|
+
const titles = viewOf(review, sectioned())
|
|
2229
|
+
.actionsAt({ start: { line: 20, character: 0 }, end: { line: 20, character: 0 } })
|
|
2230
|
+
.map((a) => a.title)
|
|
2231
|
+
expect(titles.some((t) => t.includes("A finding"))).toBe(false)
|
|
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
|
+
})
|