@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.
Files changed (66) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +41 -38
  3. package/dist/chunks/{reserved-CUkk9XgL.mjs → reserved-D0MKVFFa.mjs} +28 -34
  4. package/dist/chunks/{review-BaCXlIT2.mjs → review-ZzUQH1KZ.mjs} +11 -9
  5. package/dist/chunks/{steering-DtUVUz9l.mjs → steering-CLhu8C7q.mjs} +262 -52
  6. package/dist/chunks/{tail-CX6Pdsql.mjs → tail-I1-vVq0P.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +224 -68
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +27 -26
  10. package/dist/web/main.css +1 -1
  11. package/dist/web/main.js +510 -147
  12. package/dist/workflow-kit.mjs +1 -1
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +181 -222
  15. package/dist/workflows/fix.mjs +3 -3
  16. package/dist/workflows/review.mjs +3 -3
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +478 -313
  19. package/package.json +1 -1
  20. package/src/edge/Edge.ts +1 -30
  21. package/src/replay/Replay.test.ts +4 -8
  22. package/src/replay/Replay.ts +35 -3
  23. package/src/replay/index.ts +7 -1
  24. package/src/steering/FORMATS.md +4 -1
  25. package/src/steering/SteeringFormat.ts +7 -1
  26. package/src/steering/contract.test.ts +2 -1
  27. package/src/steering/fixtures/review/sectioned.md +31 -0
  28. package/src/steering/fixtures/review/sectioned.read.json +119 -0
  29. package/src/steering/index.ts +4 -0
  30. package/src/steering/qa.ts +1 -1
  31. package/src/steering/review.test.ts +270 -4
  32. package/src/steering/review.ts +328 -69
  33. package/src/workflows/feature/criteria.test.ts +39 -1
  34. package/src/workflows/feature/criteria.ts +21 -7
  35. package/src/workflows/feature/fast-path.feature +1 -6
  36. package/src/workflows/feature/feature.ts +1 -2
  37. package/src/workflows/feature/flag-mode.feature +1 -6
  38. package/src/workflows/feature/green.ts +22 -0
  39. package/src/workflows/feature/hands-on-skip.feature +344 -0
  40. package/src/workflows/feature/lens-models.feature +0 -4
  41. package/src/workflows/feature/lens-review.feature +6 -8
  42. package/src/workflows/feature/one-step-land.feature +1 -6
  43. package/src/workflows/feature/prototype-rounds.feature +1 -1
  44. package/src/workflows/feature/prototype.feature +10 -9
  45. package/src/workflows/feature/prototype.ts +48 -17
  46. package/src/workflows/feature/refactor-absorbs.feature +3 -8
  47. package/src/workflows/feature/refactor-first.feature +3 -8
  48. package/src/workflows/feature/refactor-rebuild.feature +7 -12
  49. package/src/workflows/feature/refactor-review.feature +52 -72
  50. package/src/workflows/feature/review-answers.feature +90 -5
  51. package/src/workflows/feature/review-gate.feature +11 -53
  52. package/src/workflows/feature/reviewer-walkthrough.feature +29 -12
  53. package/src/workflows/feature/seam-rejection.feature +1 -6
  54. package/src/workflows/feature/seam.feature +1 -6
  55. package/src/workflows/feature/seam.test.ts +1 -1
  56. package/src/workflows/feature/seam.ts +2 -3
  57. package/src/workflows/feature/severity-budget.feature +7 -7
  58. package/src/workflows/feature/severity-sorted-review.feature +50 -54
  59. package/src/workflows/feature/slice-findings.feature +5 -15
  60. package/src/workflows/feature/slicing.feature +4 -24
  61. package/src/workflows/feature/standards.feature +0 -4
  62. package/src/workflows/shared/review.ts +26 -6
  63. package/src/workflows/shared/text.test.ts +45 -11
  64. package/src/workflows/shared/text.ts +45 -38
  65. package/src/workflows/feature/coverage.feature +0 -165
  66. 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/option anchor — not this format's own kind — refuses anchor-not-found", () => {
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
- "## Walkthrough",
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: "Walkthrough",
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
+ })