@pmelab/gtd 55.0.0 → 56.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 (61) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +60 -61
  3. package/dist/chunks/{reserved-DHafcW7t.mjs → reserved-B8EC-WE8.mjs} +77 -203
  4. package/dist/chunks/{review-BJGKwlYv.mjs → review-HdrX0FJ7.mjs} +30 -125
  5. package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
  6. package/dist/chunks/{tail-B9Hw2AFB.mjs → tail-BSLah2mA.mjs} +144 -56
  7. package/dist/gtd.bundle.mjs +259 -22
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +6 -6
  10. package/dist/web/main.css +1 -1
  11. package/dist/web/main.js +551 -66
  12. package/dist/workflow-kit.mjs +1 -1
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +14 -16
  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 +522 -453
  19. package/package.json +1 -1
  20. package/skills/authoring/SKILL.md +3 -3
  21. package/src/cli/Cli.ts +5 -5
  22. package/src/edge/Edge.test.ts +1 -1
  23. package/src/steering/FORMATS.md +5 -1
  24. package/src/steering/SteeringFormats.test.ts +2 -2
  25. package/src/steering/contract.test.ts +1 -0
  26. package/src/steering/findings.test.ts +47 -0
  27. package/src/steering/findings.ts +350 -0
  28. package/src/steering/fixtures/findings/gate.md +34 -0
  29. package/src/steering/fixtures/findings/gate.read.json +87 -0
  30. package/src/steering/index.test.ts +2 -2
  31. package/src/steering/index.ts +4 -3
  32. package/src/steering/review.test.ts +1 -75
  33. package/src/steering/review.ts +1 -74
  34. package/src/workflow/compile.test.ts +1 -0
  35. package/src/workflows/feature/criteria-coverage.feature +1 -6
  36. package/src/workflows/feature/feature.test.ts +1 -1
  37. package/src/workflows/feature/feature.ts +12 -20
  38. package/src/workflows/feature/lens-review.feature +323 -171
  39. package/src/workflows/feature/refactor-absorbs.feature +1 -7
  40. package/src/workflows/feature/refactor-first.feature +2 -10
  41. package/src/workflows/feature/refactor-rebuild.feature +4 -24
  42. package/src/workflows/feature/refactor-review.feature +33 -117
  43. package/src/workflows/feature/refactor.test.ts +0 -2
  44. package/src/workflows/feature/refactor.ts +0 -1
  45. package/src/workflows/feature/review-gate.feature +6 -22
  46. package/src/workflows/feature/reviewer-walkthrough.feature +16 -103
  47. package/src/workflows/feature/standards.feature +75 -34
  48. package/src/workflows/fix/fix-lens-origin.feature +20 -7
  49. package/src/workflows/fix/fix-quality.feature +102 -23
  50. package/src/workflows/review/review-door.feature +62 -59
  51. package/src/workflows/shared/learn.test.ts +0 -14
  52. package/src/workflows/shared/learn.ts +1 -6
  53. package/src/workflows/shared/lenses.ts +149 -43
  54. package/src/workflows/shared/review.test.ts +27 -26
  55. package/src/workflows/shared/review.ts +29 -196
  56. package/src/workflows/shared/tail.ts +18 -9
  57. package/src/workflows/shared/text.test.ts +103 -288
  58. package/src/workflows/shared/text.ts +131 -329
  59. package/src/workflows/feature/review-answers.feature +0 -324
  60. package/src/workflows/feature/severity-budget.feature +0 -116
  61. package/src/workflows/feature/severity-sorted-review.feature +0 -198
@@ -33,14 +33,16 @@ import {
33
33
  seamSteerMessage,
34
34
  summaryPrompt,
35
35
  withSkills,
36
- composedReview,
37
- titledNotes,
38
- walkthroughBlock,
39
- withWalkthrough,
40
- rankedReview,
41
- unranked,
36
+ findingGateFile,
37
+ nitFixBlock,
38
+ withSummary,
42
39
  } from "./text.js"
43
- import { reviewDecisions, steeringFormatFor, unansweredNotes } from "../../steering/index.js"
40
+ import {
41
+ findingEntries,
42
+ reviewDecisions,
43
+ steeringFormatFor,
44
+ unansweredNotes,
45
+ } from "../../steering/index.js"
44
46
  import { captureStep, renderText } from "./text.fixture.js"
45
47
 
46
48
  describe("withSkills", () => {
@@ -1272,138 +1274,8 @@ describe("buildReviewCollectingPrompt answer boxes", () => {
1272
1274
  })
1273
1275
  })
1274
1276
 
1275
- describe("composedReview", () => {
1276
- const BASE = "0123456789abcdef0123456789abcdef01234567"
1277
-
1278
- describe("standards", () => {
1279
- const conflict = { text: "Calculators are functions.", olds: ["Calculators are classes."] }
1280
- const withConflict = composedReview(BASE, {
1281
- notes: [],
1282
- fixed: [],
1283
- noise: [],
1284
- standards: { added: [], undecided: [conflict] },
1285
- })
1286
-
1287
- it("asks about each undecided standard in a note that blocks sign-off", () => {
1288
- expect(withConflict).toMatch(/^## Findings\n\n### Standard conflict [0-9a-f]{8}$/m)
1289
- expect(withConflict).toContain("- [ ] keep the new one (drop the old)")
1290
- expect(unansweredNotes(withConflict)).toHaveLength(1)
1291
- })
1292
-
1293
- it("reads its boxes by position, like a note's", () => {
1294
- const ticked = withConflict.replace("- [ ] keep the old one", "- [x] keep the old one")
1295
- expect(unansweredNotes(ticked)).toEqual([])
1296
- expect(reviewDecisions(withConflict, ticked)).toEqual([
1297
- expect.objectContaining({ choice: "wontfix" }),
1298
- ])
1299
- })
1300
-
1301
- it("lists the standards added", () => {
1302
- const doc = composedReview(BASE, {
1303
- notes: [],
1304
- fixed: [],
1305
- noise: [],
1306
- standards: { added: ["Prefer early returns."], undecided: [] },
1307
- })
1308
- expect(doc).toContain("## Standards added")
1309
- expect(doc).toContain("- Prefer early returns.")
1310
- })
1311
- })
1312
-
1313
- it("is a valid review with unique chunk titles, so every note matches its own chunk", () => {
1314
- const doc = composedReview(BASE, {
1315
- notes: [
1316
- { title: "same", body: "one" },
1317
- { title: "same", body: "two", unresolved: true },
1318
- ],
1319
- fixed: [{ title: "f", commit: "fedcba9876543210" }],
1320
- noise: [{ title: "n", p: 0.8 }],
1321
- })
1322
- expect(steeringFormatFor("review")!.validate(doc)).toEqual([])
1323
- expect(doc).toContain("# Review: 0123456\n\n<!-- base: " + BASE + " -->")
1324
- expect(doc).toContain("## Findings\n\n### same\n\none")
1325
- expect(doc).toContain("### same (2)\n\n_The fixer left this unresolved._\n\ntwo")
1326
- expect(doc).toContain("## Fixed automatically\n\n- f (fedcba9)")
1327
- expect(doc).toContain("## Dropped as noise\n\n- n (p=0.8)")
1328
- })
1329
-
1330
- it("holds one chunk when the lap left nothing", () => {
1331
- const doc = composedReview(BASE, { notes: [], fixed: [], noise: [] })
1332
- expect(steeringFormatFor("review")!.validate(doc)).toEqual([])
1333
- expect(doc).toContain("## Nothing to note")
1334
- })
1335
-
1336
- it("never reuses a title, a summary's included", () => {
1337
- const doc = composedReview(BASE, {
1338
- notes: [
1339
- { title: "a", body: "1" },
1340
- { title: "a (2)", body: "2" },
1341
- { title: "a", body: "3" },
1342
- { title: "Fixed automatically", body: "4" },
1343
- ],
1344
- fixed: [{ title: "f", commit: "fedcba9876543210" }],
1345
- noise: [],
1346
- })
1347
- const titles = doc.split("\n").filter((l) => /^#{2,3} /.test(l))
1348
- expect(titles).toEqual([
1349
- "## Findings",
1350
- "### a",
1351
- "### a (2)",
1352
- "### a (3)",
1353
- "### Fixed automatically (2)",
1354
- "## Fixed automatically",
1355
- ])
1356
- })
1357
-
1358
- it("turns a lens's task boxes into plain bullets, so no pointer or risk is read", () => {
1359
- const doc = composedReview(BASE, {
1360
- notes: [
1361
- { title: "a", body: "- [ ] ./src/a.ts#1-1 Risk: x\n- [x] done\n1. [ ] one\n> - [ ] q" },
1362
- ],
1363
- fixed: [],
1364
- noise: [],
1365
- })
1366
- expect(steeringFormatFor("review")!.validate(doc)).toEqual([])
1367
- expect(doc).toContain("- ./src/a.ts#1-1 Risk: x\n- done\n1. one\n> - q")
1368
- })
1369
-
1370
- it("names the nit fix on the review after it", () => {
1371
- const doc = composedReview(BASE, { notes: [], fixed: [], noise: [], nitFix: "abcdef0123" })
1372
- expect(doc).toContain(
1373
- "## Fixed from your notes\n\nYour nits and fix-as-suggested notes were worked on in abcdef0;",
1374
- )
1375
- })
1376
-
1377
- it("ends every note, and only a note, in its three answer boxes", () => {
1378
- const doc = composedReview(BASE, {
1379
- notes: [{ title: "a", body: "- ends in a list" }],
1380
- fixed: [{ title: "f", commit: "fedcba9876543210" }],
1381
- noise: [],
1382
- })
1383
- expect(doc).toContain(
1384
- "### a\n\n- ends in a list\n\nTick exactly one:\n\n- [ ] fix as suggested\n- [ ] won't fix\n- [ ] _your answer_\n\n## Fixed automatically",
1385
- )
1386
- expect(doc.match(/fix as suggested/g)).toHaveLength(1)
1387
- expect(unansweredNotes(doc)).toEqual([{ title: "a", line: 7, reason: "tick exactly one box" }])
1388
- })
1389
-
1390
- it("leaves out the notes already answered", () => {
1391
- const doc = composedReview(BASE, {
1392
- notes: [
1393
- { title: "a", body: "1" },
1394
- { title: "a", body: "2" },
1395
- ],
1396
- fixed: [],
1397
- noise: [],
1398
- answered: new Set(["a"]),
1399
- })
1400
- expect(doc).not.toContain("# a\n")
1401
- expect(doc).toContain("## a (2)\n\n2")
1402
- })
1403
- })
1404
-
1405
- describe("walkthrough", () => {
1406
- const empty = { notes: [], fixed: [], noise: [] }
1277
+ describe("withSummary", () => {
1278
+ const empty = { fixed: [], gateFixed: [], dropped: [], noise: [], unresolved: [] }
1407
1279
  const review = [
1408
1280
  "# Review: abc1234",
1409
1281
  "",
@@ -1413,201 +1285,144 @@ describe("walkthrough", () => {
1413
1285
  "",
1414
1286
  "- [ ] ./src/calc.ts#2-2 — subtract(a, b)",
1415
1287
  "",
1416
- "## Fixed automatically",
1288
+ "## Quality lap",
1417
1289
  "",
1418
1290
  "- [ ] ./src/calc.ts#2-2 — the reviewer's own chunk",
1419
1291
  "",
1420
1292
  ].join("\n")
1421
1293
 
1422
- it("titles notes uniquely, never a summary's, and leaves out answered ones", () => {
1423
- const notes = [
1424
- { title: "Fixed automatically", body: "a" },
1425
- { title: "x", body: "b" },
1426
- { title: "x", body: "c" },
1427
- ]
1428
- expect(titledNotes({ ...empty, notes }).map((n) => n.title)).toEqual([
1429
- "Fixed automatically (2)",
1430
- "x",
1431
- "x (2)",
1432
- ])
1433
- expect(titledNotes({ ...empty, notes, answered: new Set(["x"]) }).map((n) => n.title)).toEqual([
1434
- "Fixed automatically (2)",
1435
- "x (2)",
1436
- ])
1437
- })
1438
-
1439
- it("lists each note with its exact heading and quoted body", () => {
1440
- const block = walkthroughBlock([
1441
- { title: "No test", body: "Add one.\n\n- [ ] a box", lens: "tests" },
1442
- ])
1443
- expect(block).toContain("- `## No test`:\n\n > Add one.\n >\n > - a box")
1444
- expect(block).toContain("its heading exactly as given")
1445
- })
1446
-
1447
- it("names the nit fix's commit, and says nothing with nothing to tell", () => {
1448
- expect(walkthroughBlock([], "0123456789")).toContain(
1449
- "Commit `0123456` worked on the human's nits",
1450
- )
1451
- expect(walkthroughBlock([])).toBe("")
1452
- })
1453
-
1454
- it("puts a note's text under its heading and its boxes after the reviewer's pointers", () => {
1455
- const out = withWalkthrough(review, {
1456
- ...empty,
1457
- notes: [{ title: "Subtract", body: "It may overflow.", unresolved: true }],
1294
+ it("lists what the lap did in one chunk, numbered past a title the reviewer took", () => {
1295
+ const out = withSummary(review, {
1296
+ report: {
1297
+ fixed: [{ title: "f", commit: "fedcba9876543210", lens: "tests" }],
1298
+ gateFixed: [{ title: "g", commit: "0123456789abcdef" }],
1299
+ dropped: [{ title: "d" }],
1300
+ noise: [{ title: "n", p: 0.8 }],
1301
+ unresolved: [{ title: "u" }, { title: "v", unclear: true }],
1302
+ },
1458
1303
  })
1304
+ expect(steeringFormatFor("review")!.validate(out)).toEqual([])
1459
1305
  expect(out).toContain(
1460
1306
  [
1461
- "## Subtract",
1307
+ "## Quality lap (2)",
1308
+ "",
1309
+ "What the quality lenses found, and what became of it. Write a note under",
1310
+ "a line to reopen it.",
1462
1311
  "",
1463
- "_The fixer left this unresolved._",
1312
+ "Fixed automatically:",
1464
1313
  "",
1465
- "It may overflow.",
1314
+ "- f (fedcba9, lens tests)",
1466
1315
  "",
1467
- "In the code:",
1316
+ "Fixed at the finding gate:",
1468
1317
  "",
1469
- "- [ ] ./src/calc.ts#2-2 — subtract(a, b)",
1318
+ "- g (0123456)",
1470
1319
  "",
1471
- "Tick exactly one:",
1320
+ "Dropped at the finding gate:",
1472
1321
  "",
1473
- "- [ ] fix as suggested",
1474
- "- [ ] won't fix",
1475
- "- [ ] _your answer_",
1322
+ "- d",
1323
+ "",
1324
+ "Dropped as noise:",
1325
+ "",
1326
+ "- n (p=0.8)",
1327
+ "",
1328
+ "Unresolved after the finding gate:",
1329
+ "",
1330
+ "- u",
1331
+ "- v (removed while renaming another)",
1476
1332
  "",
1477
- "## Fixed automatically",
1478
1333
  ].join("\n"),
1479
1334
  )
1480
1335
  })
1481
1336
 
1482
- it("appends the fixes and noise, numbered past a title the reviewer took", () => {
1483
- const out = withWalkthrough(review, {
1484
- ...empty,
1485
- fixed: [{ title: "a", commit: "0123456789", lens: "tests" }],
1486
- noise: [{ title: "b", p: 0.9 }],
1337
+ it("asks about each undecided standard in a note that blocks sign-off, read by position", () => {
1338
+ const conflict = { text: "Calculators are functions.", olds: ["Calculators are classes."] }
1339
+ const out = withSummary(review, {
1340
+ report: empty,
1341
+ standards: { added: [], undecided: [conflict] },
1487
1342
  })
1488
- expect(
1489
- out.endsWith(
1490
- "## Fixed automatically (2)\n\n- a (0123456)\n\n## Dropped as noise\n\n- b (p=0.9)\n",
1491
- ),
1492
- ).toBe(true)
1493
- })
1494
-
1495
- it("leaves a review with nothing to add byte for byte, trailing newlines and all", () => {
1496
- expect(withWalkthrough(review, empty)).toBe(review)
1497
- expect(withWalkthrough(review.trimEnd(), empty)).toBe(review.trimEnd())
1498
- expect(withWalkthrough(review, { ...empty, notes: [{ title: "absent", body: "x" }] })).toBe(
1499
- review,
1500
- )
1343
+ expect(out).toMatch(/^## Standard conflict [0-9a-f]{8}$/m)
1344
+ expect(unansweredNotes(out)).toHaveLength(1)
1345
+ const ticked = out.replace("- [ ] keep the old one", "- [x] keep the old one")
1346
+ expect(unansweredNotes(ticked)).toEqual([])
1347
+ expect(reviewDecisions(out, ticked)).toEqual([expect.objectContaining({ choice: "wontfix" })])
1501
1348
  })
1502
1349
 
1503
- it("keeps a note ending in a list apart from the pointers, so its answer carries the list", () => {
1504
- const out = withWalkthrough(review, {
1505
- ...empty,
1506
- notes: [{ title: "Subtract", body: "Do this:\n\n- step one\n- step two" }],
1507
- }).replace("- [ ] fix as suggested", "- [x] fix as suggested")
1508
- const [decision] = reviewDecisions("", out)
1509
- expect(decision?.body).toContain("step two")
1510
- expect(decision?.body).not.toContain("./src/calc.ts")
1511
- })
1512
-
1513
- it("shows a title holding backticks as one unbroken code span", () => {
1514
- expect(walkthroughBlock([{ title: "`sub` drifts", body: "x" }])).toContain(
1515
- "- `` ## `sub` drifts ``:",
1516
- )
1350
+ it("lists the standards added", () => {
1351
+ const out = withSummary(review, {
1352
+ report: empty,
1353
+ standards: { added: ["Prefer early returns."], undecided: [] },
1354
+ })
1355
+ expect(out).toContain("## Standards added")
1356
+ expect(out).toContain("- Prefer early returns.")
1517
1357
  })
1518
1358
 
1519
- it("keeps Risk: out of a finding's chunk", () => {
1520
- expect(walkthroughBlock([{ title: "x", body: "y" }])).toContain(
1521
- "never open a note in a finding's chunk with `Risk:`",
1522
- )
1359
+ it("leaves a review with nothing to add byte for byte, trailing newlines and all", () => {
1360
+ expect(withSummary(`${review}\n\n`, { report: empty })).toBe(`${review}\n\n`)
1523
1361
  })
1524
1362
  })
1525
1363
 
1526
- describe("rankedReview", () => {
1527
- const review = [
1528
- "# Review: abc1234",
1529
- "",
1530
- "<!-- base: abc1234 -->",
1531
- "",
1532
- "## Rename",
1533
- "",
1534
- "- [ ] ./a.ts#1-1",
1535
- "",
1536
- "## Token",
1537
- "",
1538
- "_could_",
1539
- "",
1540
- "Reads the token.",
1541
- "",
1542
- "- [ ] ./a.ts#2-2",
1543
- "",
1544
- "## Fixed automatically",
1545
- "",
1546
- "- x (abc1234)",
1547
- "",
1548
- ].join("\n")
1364
+ describe("findingGateFile", () => {
1365
+ const gate = steeringFormatFor("findings")!
1549
1366
 
1550
- it("puts the ranked chunks first, each led by its tier, and leaves the rest in place", () => {
1551
- expect(
1552
- rankedReview(unranked(review), [
1553
- { title: "Token", label: "critical: security" },
1554
- { title: "Rename", label: "could" },
1555
- ]),
1556
- ).toBe(
1367
+ it("lists the open notes, then the noise, each with an unticked box, and reads back clean", () => {
1368
+ const file = findingGateFile(
1369
+ [
1370
+ { title: "a", body: "Design.", lens: "tests" },
1371
+ { title: "b", body: "Left over.", unresolved: true },
1372
+ ],
1373
+ [{ title: "n", body: "Meh.", p: 0.8 }],
1374
+ )
1375
+ expect(file).toBe(
1557
1376
  [
1558
- "# Review: abc1234",
1377
+ "# Quality",
1378
+ "",
1379
+ "## a",
1559
1380
  "",
1560
- "<!-- base: abc1234 -->",
1381
+ "_From lens tests._",
1561
1382
  "",
1562
- "## Token",
1383
+ "Design.",
1563
1384
  "",
1564
- "_critical: security_",
1385
+ "- [ ] fix",
1565
1386
  "",
1566
- "Reads the token.",
1387
+ "## b",
1567
1388
  "",
1568
- "- [ ] ./a.ts#2-2",
1389
+ "_The fixer left it unresolved._",
1569
1390
  "",
1570
- "## Rename",
1391
+ "Left over.",
1571
1392
  "",
1572
- "_could_",
1393
+ "- [ ] fix",
1573
1394
  "",
1574
- "- [ ] ./a.ts#1-1",
1395
+ "# Dropped as noise",
1575
1396
  "",
1576
- "## Fixed automatically",
1397
+ "## n",
1577
1398
  "",
1578
- "- x (abc1234)",
1399
+ "_The judge called it noise (p=0.8)._",
1400
+ "",
1401
+ "Meh.",
1402
+ "",
1403
+ "- [ ] fix",
1579
1404
  "",
1580
1405
  ].join("\n"),
1581
1406
  )
1407
+ expect(gate.validate(file)).toEqual([])
1408
+ expect(findingEntries(file).map((e) => [e.title, e.noise, e.fix])).toEqual([
1409
+ ["a", false, false],
1410
+ ["b", false, false],
1411
+ ["n", true, false],
1412
+ ])
1582
1413
  })
1583
1414
 
1584
- it("keeps a setext heading whole", () => {
1585
- expect(
1586
- rankedReview("# Review: a\n\nToken\n-----\n\n- [ ] ./a.ts#1\n", [
1587
- { title: "Token", label: "could" },
1588
- ]),
1589
- ).toBe("# Review: a\n\nToken\n-----\n\n_could_\n\n- [ ] ./a.ts#1\n")
1590
- })
1591
-
1592
- it("keeps a chunk's leading indented code block indented", () => {
1593
- const code = "# Review: abc1234\n\n## Code\n\n const x = 1\n\n- [ ] ./a.ts#1-1\n"
1594
- expect(rankedReview(code, [{ title: "Code", label: "could" }])).toContain(
1595
- "_could_\n\n const x = 1",
1596
- )
1597
- })
1598
-
1599
- it("drops a stale tier with no blank line under the heading, never a longer paragraph", () => {
1600
- expect(unranked("## A\n_could_\n\nbody\n")).toBe("## A\nbody\n")
1601
- expect(unranked("## A\n\n_could_\n- [ ] ./a.ts#1\n")).toBe("## A\n\n- [ ] ./a.ts#1\n")
1602
- const code = "## A\n\n _could_\n"
1603
- expect(unranked(code)).toBe(code)
1604
- const prose = "## A\n\n_should: x_\nruns on\n"
1605
- expect(unranked(prose)).toBe(prose)
1415
+ it("turns a lens's task boxes into plain bullets, so no box is read but its own", () => {
1416
+ const file = findingGateFile([{ title: "a", body: "- [ ] fix\n- [x] done" }], [])
1417
+ expect(file).toContain("- fix\n- done")
1418
+ expect(findingEntries(file)[0]!.boxes).toBe(1)
1606
1419
  })
1420
+ })
1607
1421
 
1608
- it("drops a stale tier, so a re-review is ranked afresh", () => {
1609
- expect(unranked(review)).not.toContain("_could_")
1610
- expect(unranked(unranked(review))).toBe(unranked(review))
1422
+ describe("nitFixBlock", () => {
1423
+ it("names the nit fix's commit, and says nothing with nothing to tell", () => {
1424
+ expect(nitFixBlock("0123456789")).toContain("Commit `0123456` worked on the human's nits")
1425
+ expect(nitFixBlock()).toBe("")
1611
1426
  })
1612
1427
  })
1613
1428