@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.
Files changed (83) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +72 -80
  3. package/dist/chunks/{reserved-DHafcW7t.mjs → reserved-X2RkNHin.mjs} +110 -318
  4. package/dist/chunks/{review-BJGKwlYv.mjs → review-v7sh5GF4.mjs} +30 -125
  5. package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
  6. package/dist/chunks/{tail-B9Hw2AFB.mjs → tail-S04KEewi.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 +63 -175
  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 +603 -727
  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 -7
  36. package/src/workflows/feature/criteria.test.ts +1 -41
  37. package/src/workflows/feature/criteria.ts +2 -14
  38. package/src/workflows/feature/fast-path.feature +0 -1
  39. package/src/workflows/feature/feature.test.ts +1 -1
  40. package/src/workflows/feature/feature.ts +16 -25
  41. package/src/workflows/feature/flag-mode.feature +1 -1
  42. package/src/workflows/feature/lens-models.feature +0 -1
  43. package/src/workflows/feature/lens-review.feature +323 -172
  44. package/src/workflows/feature/naive-test.feature +258 -0
  45. package/src/workflows/feature/one-step-land.feature +0 -1
  46. package/src/workflows/feature/refactor-absorbs.feature +1 -8
  47. package/src/workflows/feature/refactor-first.feature +2 -11
  48. package/src/workflows/feature/refactor-rebuild.feature +4 -25
  49. package/src/workflows/feature/refactor-review.feature +33 -118
  50. package/src/workflows/feature/refactor.test.ts +0 -2
  51. package/src/workflows/feature/refactor.ts +0 -1
  52. package/src/workflows/feature/review-gate.feature +7 -24
  53. package/src/workflows/feature/reviewer-walkthrough.feature +16 -104
  54. package/src/workflows/feature/seam-rejection.feature +0 -1
  55. package/src/workflows/feature/seam.feature +0 -1
  56. package/src/workflows/feature/seam.test.ts +1 -2
  57. package/src/workflows/feature/seam.ts +56 -10
  58. package/src/workflows/feature/standards.feature +75 -35
  59. package/src/workflows/fix/fix-lens-origin.feature +20 -7
  60. package/src/workflows/fix/fix-quality.feature +102 -23
  61. package/src/workflows/review/review-door.feature +62 -59
  62. package/src/workflows/shared/learn.test.ts +0 -14
  63. package/src/workflows/shared/learn.ts +1 -6
  64. package/src/workflows/shared/lenses.ts +149 -43
  65. package/src/workflows/shared/options.ts +9 -1
  66. package/src/workflows/shared/prose.ts +0 -6
  67. package/src/workflows/shared/review.test.ts +27 -26
  68. package/src/workflows/shared/review.ts +29 -196
  69. package/src/workflows/shared/skills.test.ts +0 -4
  70. package/src/workflows/shared/skills.ts +0 -5
  71. package/src/workflows/shared/steps.ts +1 -1
  72. package/src/workflows/shared/tail.ts +18 -9
  73. package/src/workflows/shared/text.test.ts +125 -329
  74. package/src/workflows/shared/text.ts +173 -471
  75. package/src/workflows/shared/vars.ts +2 -3
  76. package/src/workflows/feature/hands-on-skip.feature +0 -316
  77. package/src/workflows/feature/prototype-rounds.feature +0 -156
  78. package/src/workflows/feature/prototype.feature +0 -428
  79. package/src/workflows/feature/prototype.test.ts +0 -43
  80. package/src/workflows/feature/prototype.ts +0 -198
  81. package/src/workflows/feature/review-answers.feature +0 -324
  82. package/src/workflows/feature/severity-budget.feature +0 -116
  83. package/src/workflows/feature/severity-sorted-review.feature +0 -198
@@ -5,7 +5,6 @@ import { join } from "node:path"
5
5
  import { afterEach, describe, expect, it } from "vitest"
6
6
  import {
7
7
  designImpactPrompt,
8
- prototypeFixingPrompt,
9
8
  buildReviewTriageMessage,
10
9
  buildReviewReviewingPrompt,
11
10
  agentWithSkills,
@@ -21,8 +20,7 @@ import {
21
20
  buildReviewCollectingPrompt,
22
21
  designGateAnswerMessage,
23
22
  designTriagePrompt,
24
- normaliseCriteriaScript,
25
- prototypeTestingPrompt,
23
+ naiveTryMessage,
26
24
  implementBuildingPrompt,
27
25
  implementFixPrompt,
28
26
  naivePassPrompt,
@@ -33,14 +31,16 @@ import {
33
31
  seamSteerMessage,
34
32
  summaryPrompt,
35
33
  withSkills,
36
- composedReview,
37
- titledNotes,
38
- walkthroughBlock,
39
- withWalkthrough,
40
- rankedReview,
41
- unranked,
34
+ findingGateFile,
35
+ nitFixBlock,
36
+ withSummary,
42
37
  } from "./text.js"
43
- import { reviewDecisions, steeringFormatFor, unansweredNotes } from "../../steering/index.js"
38
+ import {
39
+ findingEntries,
40
+ reviewDecisions,
41
+ steeringFormatFor,
42
+ unansweredNotes,
43
+ } from "../../steering/index.js"
44
44
  import { captureStep, renderText } from "./text.fixture.js"
45
45
 
46
46
  describe("withSkills", () => {
@@ -174,37 +174,6 @@ describe("the risk tier prompt and gate line", () => {
174
174
  })
175
175
  })
176
176
 
177
- describe("the (no hands-on) tag", () => {
178
- it("is explained at triage and at the design gate", () => {
179
- expect(renderText(() => designTriagePrompt("base"))).toContain(
180
- "`- AC-<n> (no hands-on): <criterion>`",
181
- )
182
- expect(renderText(() => designGateAnswerMessage())).toContain(
183
- "add or remove `(no hands-on)` between an `AC-<n>` and its colon",
184
- )
185
- })
186
-
187
- it("names the tagged criteria the testing turn skips, and only when there are any", () => {
188
- expect(renderText(() => prototypeTestingPrompt(["AC-2", "AC-3"], false))).toContain(
189
- "- Skip AC-2, AC-3: no person can observe them",
190
- )
191
- expect(renderText(() => prototypeTestingPrompt([], false))).not.toContain("Skip")
192
- })
193
-
194
- it("is kept when the stated criteria are recorded", () => {
195
- const script = normaliseCriteriaScript({
196
- path: "TODO.md",
197
- commit: "abc",
198
- criteria: [
199
- { id: "AC-1", text: "a" },
200
- { id: "AC-2", text: "b", noHandsOn: true },
201
- ],
202
- })
203
- expect(script).toContain("'- AC-1: a'")
204
- expect(script).toContain("'- AC-2 (no hands-on): b'")
205
- })
206
- })
207
-
208
177
  describe("the shared open-question instruction", () => {
209
178
  it("leaves the option count to the agent, floored at two real options", () => {
210
179
  const prompt = renderText(() => designTriagePrompt("base"))
@@ -867,17 +836,11 @@ describe("Impact in prompts", () => {
867
836
  it.each([
868
837
  ["naive", () => naivePassPrompt()],
869
838
  ["implement", () => implementBuildingPrompt(undefined)],
870
- ["prototype fixing", () => prototypeFixingPrompt()],
871
839
  ])("[AC-5] [AC-7] the %s prompt gives Impact as hints, not allowed files", (_n, prompt) => {
872
840
  const p = renderText(prompt).replace(/\s+/g, " ")
873
841
  expect(p).toContain("`## Impact`")
874
842
  expect(p).toContain("not a list of allowed files")
875
843
  })
876
-
877
- it("the prototype testing prompt, which changes no code, carries no Impact hints", () => {
878
- const p = renderText(() => prototypeTestingPrompt([], false))
879
- expect(p).not.toContain("not a list of allowed files")
880
- })
881
844
  })
882
845
 
883
846
  describe("the seam phase's texts", () => {
@@ -898,6 +861,27 @@ describe("the seam phase's texts", () => {
898
861
  expect(prompt).toContain("solved the task the wrong way")
899
862
  })
900
863
 
864
+ it("hands a pass a person sent back its tip, their notes and their edits", () => {
865
+ const prompt = renderText(() =>
866
+ naivePassPrompt({ tested: "tip123", notes: "the button does nothing", edits: "edit456" }),
867
+ )
868
+ expect(prompt).toContain("A person tried your previous attempt (`tip123`) by hand")
869
+ expect(prompt).toContain(
870
+ "git diff --binary --no-color --no-ext-diff --no-textconv tip123^ tip123",
871
+ )
872
+ expect(prompt).toContain("> the button does nothing")
873
+ expect(prompt).toContain("git show edit456 -- . ':(exclude).gtd'")
874
+ const bare = renderText(() => naivePassPrompt({ tested: "tip123" }))
875
+ expect(bare).not.toContain("Their notes")
876
+ expect(bare).not.toContain("git show")
877
+ })
878
+
879
+ it("offers send-back and sign-off at the naive test, naming what a sign-off reaches", () => {
880
+ expect(renderText(() => naiveTryMessage(true))).toContain("the seam judge rules on its shape")
881
+ expect(renderText(() => naiveTryMessage(false))).toContain("lands as the implementation")
882
+ expect(renderText(() => naiveTryMessage(true))).toContain("`.gtd/NAIVE-TEST.md`")
883
+ })
884
+
901
885
  it("asks for a breadth-first pass tested through existing code, green before it ends", () => {
902
886
  const prompt = renderText(() => naivePassPrompt())
903
887
  expect(prompt).toContain("breadth-first")
@@ -908,7 +892,6 @@ describe("the seam phase's texts", () => {
908
892
  it.each([
909
893
  ["naive", () => naivePassPrompt()],
910
894
  ["building", () => implementBuildingPrompt("abc123")],
911
- ["prototype fixing", () => prototypeFixingPrompt()],
912
895
  ])(
913
896
  "the %s prompt asks for a test per behaviour, never a criterion ID in its name",
914
897
  (_n, prompt) => {
@@ -1272,138 +1255,8 @@ describe("buildReviewCollectingPrompt answer boxes", () => {
1272
1255
  })
1273
1256
  })
1274
1257
 
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: [] }
1258
+ describe("withSummary", () => {
1259
+ const empty = { fixed: [], gateFixed: [], dropped: [], noise: [], unresolved: [] }
1407
1260
  const review = [
1408
1261
  "# Review: abc1234",
1409
1262
  "",
@@ -1413,201 +1266,144 @@ describe("walkthrough", () => {
1413
1266
  "",
1414
1267
  "- [ ] ./src/calc.ts#2-2 — subtract(a, b)",
1415
1268
  "",
1416
- "## Fixed automatically",
1269
+ "## Quality lap",
1417
1270
  "",
1418
1271
  "- [ ] ./src/calc.ts#2-2 — the reviewer's own chunk",
1419
1272
  "",
1420
1273
  ].join("\n")
1421
1274
 
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 }],
1275
+ it("lists what the lap did in one chunk, numbered past a title the reviewer took", () => {
1276
+ const out = withSummary(review, {
1277
+ report: {
1278
+ fixed: [{ title: "f", commit: "fedcba9876543210", lens: "tests" }],
1279
+ gateFixed: [{ title: "g", commit: "0123456789abcdef" }],
1280
+ dropped: [{ title: "d" }],
1281
+ noise: [{ title: "n", p: 0.8 }],
1282
+ unresolved: [{ title: "u" }, { title: "v", unclear: true }],
1283
+ },
1458
1284
  })
1285
+ expect(steeringFormatFor("review")!.validate(out)).toEqual([])
1459
1286
  expect(out).toContain(
1460
1287
  [
1461
- "## Subtract",
1288
+ "## Quality lap (2)",
1289
+ "",
1290
+ "What the quality lenses found, and what became of it. Write a note under",
1291
+ "a line to reopen it.",
1292
+ "",
1293
+ "Fixed automatically:",
1462
1294
  "",
1463
- "_The fixer left this unresolved._",
1295
+ "- f (fedcba9, lens tests)",
1464
1296
  "",
1465
- "It may overflow.",
1297
+ "Fixed at the finding gate:",
1466
1298
  "",
1467
- "In the code:",
1299
+ "- g (0123456)",
1468
1300
  "",
1469
- "- [ ] ./src/calc.ts#2-2 — subtract(a, b)",
1301
+ "Dropped at the finding gate:",
1470
1302
  "",
1471
- "Tick exactly one:",
1303
+ "- d",
1472
1304
  "",
1473
- "- [ ] fix as suggested",
1474
- "- [ ] won't fix",
1475
- "- [ ] _your answer_",
1305
+ "Dropped as noise:",
1306
+ "",
1307
+ "- n (p=0.8)",
1308
+ "",
1309
+ "Unresolved after the finding gate:",
1310
+ "",
1311
+ "- u",
1312
+ "- v (removed while renaming another)",
1476
1313
  "",
1477
- "## Fixed automatically",
1478
1314
  ].join("\n"),
1479
1315
  )
1480
1316
  })
1481
1317
 
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 }],
1318
+ it("asks about each undecided standard in a note that blocks sign-off, read by position", () => {
1319
+ const conflict = { text: "Calculators are functions.", olds: ["Calculators are classes."] }
1320
+ const out = withSummary(review, {
1321
+ report: empty,
1322
+ standards: { added: [], undecided: [conflict] },
1487
1323
  })
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
- )
1324
+ expect(out).toMatch(/^## Standard conflict [0-9a-f]{8}$/m)
1325
+ expect(unansweredNotes(out)).toHaveLength(1)
1326
+ const ticked = out.replace("- [ ] keep the old one", "- [x] keep the old one")
1327
+ expect(unansweredNotes(ticked)).toEqual([])
1328
+ expect(reviewDecisions(out, ticked)).toEqual([expect.objectContaining({ choice: "wontfix" })])
1501
1329
  })
1502
1330
 
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
- )
1331
+ it("lists the standards added", () => {
1332
+ const out = withSummary(review, {
1333
+ report: empty,
1334
+ standards: { added: ["Prefer early returns."], undecided: [] },
1335
+ })
1336
+ expect(out).toContain("## Standards added")
1337
+ expect(out).toContain("- Prefer early returns.")
1517
1338
  })
1518
1339
 
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
- )
1340
+ it("leaves a review with nothing to add byte for byte, trailing newlines and all", () => {
1341
+ expect(withSummary(`${review}\n\n`, { report: empty })).toBe(`${review}\n\n`)
1523
1342
  })
1524
1343
  })
1525
1344
 
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")
1345
+ describe("findingGateFile", () => {
1346
+ const gate = steeringFormatFor("findings")!
1549
1347
 
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(
1348
+ it("lists the open notes, then the noise, each with an unticked box, and reads back clean", () => {
1349
+ const file = findingGateFile(
1350
+ [
1351
+ { title: "a", body: "Design.", lens: "tests" },
1352
+ { title: "b", body: "Left over.", unresolved: true },
1353
+ ],
1354
+ [{ title: "n", body: "Meh.", p: 0.8 }],
1355
+ )
1356
+ expect(file).toBe(
1557
1357
  [
1558
- "# Review: abc1234",
1358
+ "# Quality",
1359
+ "",
1360
+ "## a",
1361
+ "",
1362
+ "_From lens tests._",
1559
1363
  "",
1560
- "<!-- base: abc1234 -->",
1364
+ "Design.",
1561
1365
  "",
1562
- "## Token",
1366
+ "- [ ] fix",
1563
1367
  "",
1564
- "_critical: security_",
1368
+ "## b",
1565
1369
  "",
1566
- "Reads the token.",
1370
+ "_The fixer left it unresolved._",
1567
1371
  "",
1568
- "- [ ] ./a.ts#2-2",
1372
+ "Left over.",
1569
1373
  "",
1570
- "## Rename",
1374
+ "- [ ] fix",
1571
1375
  "",
1572
- "_could_",
1376
+ "# Dropped as noise",
1573
1377
  "",
1574
- "- [ ] ./a.ts#1-1",
1378
+ "## n",
1575
1379
  "",
1576
- "## Fixed automatically",
1380
+ "_The judge called it noise (p=0.8)._",
1577
1381
  "",
1578
- "- x (abc1234)",
1382
+ "Meh.",
1383
+ "",
1384
+ "- [ ] fix",
1579
1385
  "",
1580
1386
  ].join("\n"),
1581
1387
  )
1388
+ expect(gate.validate(file)).toEqual([])
1389
+ expect(findingEntries(file).map((e) => [e.title, e.noise, e.fix])).toEqual([
1390
+ ["a", false, false],
1391
+ ["b", false, false],
1392
+ ["n", true, false],
1393
+ ])
1582
1394
  })
1583
1395
 
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)
1396
+ it("turns a lens's task boxes into plain bullets, so no box is read but its own", () => {
1397
+ const file = findingGateFile([{ title: "a", body: "- [ ] fix\n- [x] done" }], [])
1398
+ expect(file).toContain("- fix\n- done")
1399
+ expect(findingEntries(file)[0]!.boxes).toBe(1)
1606
1400
  })
1401
+ })
1607
1402
 
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))
1403
+ describe("nitFixBlock", () => {
1404
+ it("names the nit fix's commit, and says nothing with nothing to tell", () => {
1405
+ expect(nitFixBlock("0123456789")).toContain("Commit `0123456` worked on the human's nits")
1406
+ expect(nitFixBlock()).toBe("")
1611
1407
  })
1612
1408
  })
1613
1409