@pmelab/gtd 49.0.0 → 50.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 (32) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +31 -26
  3. package/dist/chunks/{reserved-CYZ1bSBy.mjs → reserved-CdOzKFVX.mjs} +59 -2
  4. package/dist/chunks/{review-BewyJflS.mjs → review-BChXEl2Y.mjs} +84 -20
  5. package/dist/chunks/{steering-BXwjmocB.mjs → steering-DtUVUz9l.mjs} +44 -14
  6. package/dist/chunks/{tail-C1Oi2YEy.mjs → tail-DRM2eC1z.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +22 -5
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +1 -1
  10. package/dist/web/main.js +27 -3
  11. package/dist/workflow-kit.mjs +1 -1
  12. package/dist/workflows/cleanup.mjs +2 -2
  13. package/dist/workflows/feature.mjs +3 -3
  14. package/dist/workflows/fix.mjs +3 -3
  15. package/dist/workflows/review.mjs +3 -3
  16. package/dist/workflows/rollout.mjs +1 -1
  17. package/dist/workflows.mjs +181 -30
  18. package/package.json +1 -1
  19. package/src/steering/index.ts +1 -0
  20. package/src/steering/review.test.ts +60 -2
  21. package/src/steering/review.ts +66 -9
  22. package/src/workflows/feature/lens-review.feature +16 -4
  23. package/src/workflows/feature/review-answers.feature +8 -1
  24. package/src/workflows/feature/review-gate.feature +20 -3
  25. package/src/workflows/feature/reviewer-walkthrough.feature +14 -3
  26. package/src/workflows/feature/severity-budget.feature +111 -0
  27. package/src/workflows/feature/severity-sorted-review.feature +197 -0
  28. package/src/workflows/feature/slice-findings.feature +14 -2
  29. package/src/workflows/feature/standards.feature +4 -0
  30. package/src/workflows/shared/review.ts +124 -18
  31. package/src/workflows/shared/text.test.ts +90 -0
  32. package/src/workflows/shared/text.ts +77 -1
@@ -77,6 +77,8 @@ interface Changeset {
77
77
  readonly headingLine: number
78
78
  /** The chunk's answer boxes, when gtd gave it some. */
79
79
  readonly answers?: readonly AnswerOption[]
80
+ /** The tier and reason gtd ranked the chunk with, from its leading severity line. */
81
+ readonly severity?: { readonly label: string; readonly line: number }
80
82
  }
81
83
 
82
84
  interface AnswerOption {
@@ -89,6 +91,10 @@ export const ANSWER_LEAD = "Tick exactly one:"
89
91
  export const ANSWER_FIX = "fix as suggested"
90
92
  export const ANSWER_WONT_FIX = "won't fix"
91
93
 
94
+ /** A chunk's leading `_critical: security_` paragraph: the tier gtd ranked it with, and why. */
95
+ const SEVERITY_LINE_RE = /^_((?:critical|should|could)(?::[^_\n]*)?)_$/
96
+ export const severityLine = (label: string): string => `_${label}_`
97
+
92
98
  interface ReviewDoc {
93
99
  readonly shortHash?: string
94
100
  readonly fullHash?: string
@@ -521,6 +527,26 @@ const answerListOf = (
521
527
  return { lead, list, options }
522
528
  }
523
529
 
530
+ /** A leading severity line is the chunk's tier, never its prose. */
531
+ const splitSeverity = (
532
+ content: string,
533
+ run: readonly RootContent[],
534
+ ): {
535
+ readonly severity?: { readonly label: string; readonly line: number } | undefined
536
+ readonly leadingRun: readonly RootContent[]
537
+ } => {
538
+ const first = run[0]
539
+ const label =
540
+ first?.type === "paragraph"
541
+ ? SEVERITY_LINE_RE.exec(sourceText(content, first).trim())?.[1]
542
+ : undefined
543
+ if (label === undefined) return { leadingRun: run }
544
+ return {
545
+ severity: { label, line: toLspPosition(first!.position!.start).line },
546
+ leadingRun: run.slice(1),
547
+ }
548
+ }
549
+
524
550
  /** Splits one chunk's body nodes into its file pointers (hunk pointers are task items, collected recursively at ANY nesting depth via `taskItems` — a nested hunk is the same kind of hunk as a top-level one) and description prose. The description is the chunk's own leading run and never a node that contains a hunk pointer: it stops at the first node whose `taskItems` include a real pointer (at any depth — a blockquote wrapping a pointer list stops it just as a top-level list would). Every node kind in what's left before that point contributes — a `footnoteDefinition` is the one exception, excluded by `blockNodesOfRun` itself (it's a note, never document content). */
525
551
  const parseChunkBody = (
526
552
  content: string,
@@ -534,6 +560,7 @@ const parseChunkBody = (
534
560
  readonly files: readonly ReviewFile[]
535
561
  readonly errors: readonly SteeringFinding[]
536
562
  readonly answers?: readonly AnswerOption[]
563
+ readonly severity?: { readonly label: string; readonly line: number } | undefined
537
564
  /** Whether a task box outside the answer list exists. */
538
565
  readonly boxed: boolean
539
566
  } => {
@@ -543,9 +570,10 @@ const parseChunkBody = (
543
570
  const firstPointerIndex = body.findIndex(
544
571
  (n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)),
545
572
  )
546
- const leadingRun = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter(
573
+ const run = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter(
547
574
  (n) => !isAnswer(n),
548
575
  )
576
+ const { severity, leadingRun } = splitSeverity(content, run)
549
577
  const descriptionNodes = blockNodesOfRun(content, leadingRun)
550
578
  const descriptionInline = collapseImages(blockRunInline(content, leadingRun))
551
579
  const description = leadingRun
@@ -569,6 +597,7 @@ const parseChunkBody = (
569
597
  files,
570
598
  errors,
571
599
  ...(answer === undefined ? {} : { answers: answer.options }),
600
+ ...(severity === undefined ? {} : { severity }),
572
601
  boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0),
573
602
  }
574
603
  }
@@ -618,6 +647,7 @@ const parseChangesets = (
618
647
  descriptionInline,
619
648
  files,
620
649
  answers,
650
+ severity,
621
651
  boxed,
622
652
  errors: bodyErrors,
623
653
  } = parseChunkBody(content, lines, title, body)
@@ -638,6 +668,7 @@ const parseChangesets = (
638
668
  files,
639
669
  headingLine,
640
670
  ...(answers === undefined ? {} : { answers }),
671
+ ...(severity === undefined ? {} : { severity }),
641
672
  })
642
673
  }
643
674
  if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" })
@@ -997,6 +1028,7 @@ const reviewView = (content: string): SteeringView => {
997
1028
  detail: chunk.description,
998
1029
  detailInline: chunk.descriptionInline,
999
1030
  body: chunk.descriptionNodes,
1031
+ ...(chunk.severity === undefined ? {} : { status: chunk.severity.label }),
1000
1032
  anchor: { kind: "chunk", index: chunkIndex },
1001
1033
  ...noteAt(chunk.headingLine),
1002
1034
  children: chunk.files.map((file, index) => ({
@@ -1177,11 +1209,22 @@ const pointerLabel = (f: ReviewFile): string =>
1177
1209
 
1178
1210
  type Found = { line: number; note: Omit<ReviewNote, "id" | "forced"> }
1179
1211
 
1180
- // A chunk added with its own answer boxes (a driver's imported note) is
1181
- // answered by its tick, never judged as prose as well.
1182
- const chunkNote = (chunk: Changeset, prior: Changeset | undefined): Found[] =>
1183
- chunk.description === (prior?.description ?? "") ||
1184
- (prior === undefined && chunk.answers !== undefined)
1212
+ // Rewording a tier disputes it, so a changed tier joins the note — after the
1213
+ // prose, so a leading `!` still forces; deleting one asks nothing.
1214
+ const chunkProse = (chunk: Changeset | undefined, retiered: boolean): string =>
1215
+ [
1216
+ chunk?.description ?? "",
1217
+ retiered && chunk?.severity !== undefined ? severityLine(chunk.severity.label) : "",
1218
+ ]
1219
+ .filter((p) => p !== "")
1220
+ .join("\n\n")
1221
+
1222
+ const chunkNote = (chunk: Changeset, prior: Changeset | undefined): Found[] => {
1223
+ const retiered = chunk.severity !== undefined && chunk.severity.label !== prior?.severity?.label
1224
+ // A chunk added with its own answer boxes (a driver's imported note) is
1225
+ // answered by its tick, never judged as prose as well.
1226
+ return chunkProse(chunk, retiered) === chunkProse(prior, retiered) ||
1227
+ (prior === undefined && chunk.answers !== undefined)
1185
1228
  ? []
1186
1229
  : [
1187
1230
  {
@@ -1189,11 +1232,12 @@ const chunkNote = (chunk: Changeset, prior: Changeset | undefined): Found[] =>
1189
1232
  note: {
1190
1233
  kind: "chunk",
1191
1234
  anchor: chunk.title,
1192
- before: prior?.description ?? "",
1193
- text: chunk.description,
1235
+ before: chunkProse(prior, retiered),
1236
+ text: chunkProse(chunk, retiered),
1194
1237
  },
1195
1238
  },
1196
1239
  ]
1240
+ }
1197
1241
 
1198
1242
  const pointerNotes = (chunk: Changeset, prior: Changeset | undefined): Found[] =>
1199
1243
  chunk.files.flatMap((file): Found[] => {
@@ -1342,9 +1386,22 @@ export const reviewDecisions = (before: string, after: string): readonly ReviewD
1342
1386
  /** Each `##` chunk in document order: its title, 0-based heading line and how many hunk pointers it holds. */
1343
1387
  export const reviewChunks = (
1344
1388
  content: string,
1345
- ): readonly { title: string; line: number; pointers: number }[] =>
1389
+ ): readonly {
1390
+ title: string
1391
+ line: number
1392
+ pointers: number
1393
+ /** 0-based line of the chunk's severity line, when it has one. */
1394
+ severityLine?: number
1395
+ files: readonly { path: string; line?: number; rangeEnd?: number }[]
1396
+ }[] =>
1346
1397
  parseReviewDoc(content).changesets.map((c) => ({
1347
1398
  title: c.title,
1348
1399
  line: c.headingLine,
1349
1400
  pointers: c.files.length,
1401
+ ...(c.severity === undefined ? {} : { severityLine: c.severity.line }),
1402
+ files: c.files.map((f) => ({
1403
+ path: f.path,
1404
+ ...(f.line === undefined ? {} : { line: f.line }),
1405
+ ...(f.rangeEnd === undefined ? {} : { rangeEnd: f.rangeEnd }),
1406
+ })),
1350
1407
  }))
@@ -75,6 +75,10 @@ Feature: the quality lap's findings are triaged before the review
75
75
  And step "build.quality.sorting" runs its script
76
76
  And step "build.quality.closing" runs its script
77
77
  And step "build.review.carrying" runs its script
78
+ And judge step "build.review.ranking" answers:
79
+ """
80
+ []
81
+ """
78
82
 
79
83
  Scenario: a mechanical finding is fixed and listed, noise dropped and listed, a design one noted
80
84
  Given judge step "build.quality.triage" answers:
@@ -110,6 +114,8 @@ Feature: the quality lap's findings are triaged before the review
110
114
  """
111
115
  ## AC-1 ignores floating point
112
116
 
117
+ _critical: judge unsure_
118
+
113
119
  Should subtract round? The criterion does not say.
114
120
 
115
121
  In the code:
@@ -147,7 +153,8 @@ Feature: the quality lap's findings are triaged before the review
147
153
  gtd(check): build.quality.sorting → build.fix.quality.fixing
148
154
  gtd(agent): build.fix.quality.fixing → build.health.check
149
155
  gtd(check): build.health.check → build.review.reviewing
150
- gtd(agent): build.review.reviewing → build.review.carrying
156
+ gtd(agent): build.review.reviewing → build.review.ranking
157
+ gtd(judge): build.review.ranking → build.review.carrying
151
158
  gtd(check): build.review.carrying → build.review.await-review
152
159
  """
153
160
 
@@ -189,7 +196,8 @@ Feature: the quality lap's findings are triaged before the review
189
196
  """
190
197
  gtd(judge): build.quality.triage → build.quality.sorting
191
198
  gtd(check): build.quality.sorting → build.review.reviewing
192
- gtd(agent): build.review.reviewing → build.review.carrying
199
+ gtd(agent): build.review.reviewing → build.review.ranking
200
+ gtd(judge): build.review.ranking → build.review.carrying
193
201
  gtd(check): build.review.carrying → build.review.await-review
194
202
  """
195
203
 
@@ -228,6 +236,8 @@ Feature: the quality lap's findings are triaged before the review
228
236
  """
229
237
  ## add could be a method
230
238
 
239
+ _critical: judge unsure_
240
+
231
241
  _The fixer left this unresolved._
232
242
 
233
243
  A class would read better.
@@ -242,7 +252,8 @@ Feature: the quality lap's findings are triaged before the review
242
252
  gtd(agent): build.fix.quality.fixing → build.health.check
243
253
  gtd(check): build.health.check → build.quality.closing
244
254
  gtd(check): build.quality.closing → build.review.reviewing
245
- gtd(agent): build.review.reviewing → build.review.carrying
255
+ gtd(agent): build.review.reviewing → build.review.ranking
256
+ gtd(judge): build.review.ranking → build.review.carrying
246
257
  gtd(check): build.review.carrying → build.review.await-review
247
258
  """
248
259
 
@@ -344,7 +355,8 @@ Feature: the quality lap's findings are triaged before the review
344
355
  And ".gtd/REVIEW.md" does not exist
345
356
  And the commit subjects end with:
346
357
  """
347
- gtd(agent): build.review.reviewing → build.review.carrying
358
+ gtd(agent): build.review.reviewing → build.review.ranking
359
+ gtd(judge): build.review.ranking → build.review.carrying
348
360
  gtd(check): build.review.carrying → build.review.await-review
349
361
  gtd(human): build.review.await-review → build.review.closing
350
362
  gtd(check): build.review.closing → idle
@@ -109,6 +109,10 @@ Feature: every note in the review is answered with one tick
109
109
  - [ ] ./src/calc.ts#2-2 — now rounds the difference
110
110
  """
111
111
  And step "build.review.carrying" runs its script
112
+ And judge step "build.review.ranking" answers:
113
+ """
114
+ []
115
+ """
112
116
  And judge step "build.quality.triage" answers:
113
117
  """
114
118
  [{"id":"Q1","answer":"noise","p":0.9},{"id":"Q2","answer":"noise","p":0.9},{"id":"Q3","answer":"design","p":0.9}]
@@ -122,6 +126,8 @@ Feature: every note in the review is answered with one tick
122
126
  """
123
127
  ## AC-1 ignores floating point
124
128
 
129
+ _critical: judge unsure_
130
+
125
131
  Should subtract round? The criterion does not say.
126
132
 
127
133
  In the code:
@@ -215,7 +221,8 @@ Feature: every note in the review is answered with one tick
215
221
  gtd(agent): build.review.fix.nits.fixing → build.health.check
216
222
  gtd(check): build.health.check → build.review.closing
217
223
  gtd(check): build.review.closing → build.review.reviewing
218
- gtd(agent): build.review.reviewing → build.review.carrying
224
+ gtd(agent): build.review.reviewing → build.review.ranking
225
+ gtd(judge): build.review.ranking → build.review.carrying
219
226
  gtd(check): build.review.carrying → build.review.await-review
220
227
  """
221
228
  And ".gtd/REVIEW.md" does not contain "AC-1 ignores floating point"
@@ -69,6 +69,11 @@ Feature: a feature process ends at the human review
69
69
  new subtract function
70
70
  """
71
71
  And step "build.review.closing" runs its script
72
+ And step "build.review.carrying" runs its script
73
+ And judge step "build.review.ranking" answers:
74
+ """
75
+ []
76
+ """
72
77
 
73
78
  Scenario: a feature process ends at the review gate
74
79
  When the process is driven
@@ -79,7 +84,9 @@ Feature: a feature process ends at the human review
79
84
  """
80
85
  gtd(judge): implement.coverage → implement.closing
81
86
  gtd(check): implement.closing → build.review.reviewing
82
- gtd(agent): build.review.reviewing → build.review.await-review
87
+ gtd(agent): build.review.reviewing → build.review.ranking
88
+ gtd(judge): build.review.ranking → build.review.carrying
89
+ gtd(check): build.review.carrying → build.review.await-review
83
90
  """
84
91
 
85
92
  Scenario: a sign-off ends the process at idle
@@ -93,6 +100,8 @@ Feature: a feature process ends at the human review
93
100
 
94
101
  ## calc
95
102
 
103
+ _critical: judge unsure_
104
+
96
105
  - [x] ./src/calc.ts#2-2
97
106
  new subtract function
98
107
  """
@@ -102,7 +111,9 @@ Feature: a feature process ends at the human review
102
111
  And ".gtd/REVIEW.md" does not exist
103
112
  And the commit subjects end with:
104
113
  """
105
- gtd(agent): build.review.reviewing → build.review.await-review
114
+ gtd(agent): build.review.reviewing → build.review.ranking
115
+ gtd(judge): build.review.ranking → build.review.carrying
116
+ gtd(check): build.review.carrying → build.review.await-review
106
117
  gtd(human): build.review.await-review → build.review.closing
107
118
  gtd(check): build.review.closing → idle
108
119
  """
@@ -146,6 +157,8 @@ Feature: a feature process ends at the human review
146
157
 
147
158
  ## calc
148
159
 
160
+ _critical: judge unsure_
161
+
149
162
  - [ ] ./src/calc.ts#2-2
150
163
  new subtract function[^why]
151
164
 
@@ -164,6 +177,8 @@ Feature: a feature process ends at the human review
164
177
 
165
178
  ## calc
166
179
 
180
+ _critical: judge unsure_
181
+
167
182
  - [ ] ./src/calc.ts#2-2
168
183
  new subtract function
169
184
  """
@@ -215,7 +230,9 @@ Feature: a feature process ends at the human review
215
230
  gtd(check): implement.health.check → implement.coverage
216
231
  gtd(judge): implement.coverage → implement.closing
217
232
  gtd(check): implement.closing → build.review.reviewing
218
- gtd(agent): build.review.reviewing → build.review.await-review
233
+ gtd(agent): build.review.reviewing → build.review.ranking
234
+ gtd(judge): build.review.ranking → build.review.carrying
235
+ gtd(check): build.review.carrying → build.review.await-review
219
236
  """
220
237
 
221
238
  Scenario: a design numbering new behaviour at or below an earlier ID is refused
@@ -67,6 +67,10 @@ Feature: a reviewer walks the human through the feature's code
67
67
  And step "build.quality.sorting" runs its script
68
68
  And step "build.quality.closing" runs its script
69
69
  And step "build.review.carrying" runs its script
70
+ And judge step "build.review.ranking" answers:
71
+ """
72
+ []
73
+ """
70
74
 
71
75
  Scenario: the finding gets its text and answer boxes under the reviewer's pointers
72
76
  Given step "build.review.reviewing" is prompted with "- `## AC-1 ignores floating point`:"
@@ -93,12 +97,16 @@ Feature: a reviewer walks the human through the feature's code
93
97
  """
94
98
  ## Subtract
95
99
 
100
+ _critical: judge unsure_
101
+
96
102
  The new export.
97
103
 
98
104
  - [ ] ./src/calc.ts#2-2 — subtract(a, b)
99
105
 
100
106
  ## AC-1 ignores floating point
101
107
 
108
+ _critical: judge unsure_
109
+
102
110
  Should subtract round? The criterion does not say.
103
111
 
104
112
  In the code:
@@ -114,7 +122,8 @@ Feature: a reviewer walks the human through the feature's code
114
122
  And no commit body contains "build.review.composing"
115
123
  And the commit subjects end with:
116
124
  """
117
- gtd(agent): build.review.reviewing → build.review.carrying
125
+ gtd(agent): build.review.reviewing → build.review.ranking
126
+ gtd(judge): build.review.ranking → build.review.carrying
118
127
  gtd(check): build.review.carrying → build.review.await-review
119
128
  """
120
129
 
@@ -169,11 +178,13 @@ Feature: a reviewer walks the human through the feature's code
169
178
  Then the process rests at "build.review.await-review"
170
179
  And the commit subjects end with:
171
180
  """
172
- gtd(agent): build.review.reviewing → build.review.carrying
181
+ gtd(agent): build.review.reviewing → build.review.ranking
182
+ gtd(judge): build.review.ranking → build.review.carrying
173
183
  gtd(check): build.review.carrying → build.review.fix.risks.fixing
174
184
  gtd(agent): build.review.fix.risks.fixing → build.health.check
175
185
  gtd(check): build.health.check → build.review.reviewing
176
- gtd(agent): build.review.reviewing → build.review.carrying
186
+ gtd(agent): build.review.reviewing → build.review.ranking
187
+ gtd(judge): build.review.ranking → build.review.carrying
177
188
  gtd(check): build.review.carrying → build.review.await-review
178
189
  """
179
190
 
@@ -0,0 +1,111 @@
1
+ @inmem
2
+ Feature: the review's ranking is batched to the judge's budget
3
+
4
+ `build.review.ranking` puts as many chunks in one judgment as the budget's
5
+ even share still holds whole; the rest go to the next. A chunk too big to
6
+ fit even alone is cut, and a cut chunk is critical.
7
+
8
+ Background:
9
+ Given the shipped workflows, as gtd install scaffolds them
10
+ And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
11
+ And an environment variable "GTD_FLAGMODE" set to "false"
12
+ And an environment variable "GTD_JUDGEBUDGETBYTES" set to "150"
13
+ And a file "src/calc.ts" with:
14
+ """
15
+ export const add = (a: number, b: number) => a + b
16
+ """
17
+ And the working tree is committed as "feat: add calculator"
18
+ And a file "TODO.md" with:
19
+ """
20
+ # Add subtract
21
+
22
+ subtract(a, b) returns the difference.
23
+ """
24
+ And step "unwind" changes nothing
25
+ And step "start-gate.check" changes nothing
26
+ And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
27
+ """
28
+ # Requirements
29
+
30
+ ## Acceptance Criteria
31
+
32
+ - AC-1: subtract(a, b) returns a - b.
33
+ """
34
+ And step "design.gate.answer" changes nothing
35
+ And step "seam.naive" writes "src/calc.test.ts" with:
36
+ """
37
+ it("[AC-1] subtracts", () => {})
38
+ """
39
+ And step "seam.naive" writes "src/calc.ts" with:
40
+ """
41
+ export const add = (a: number, b: number) => a + b
42
+ export const subtract = (a: number, b: number) => a - b
43
+ """
44
+ And step "seam.check" changes nothing
45
+ And step "implement.applying" runs its script
46
+ And step "implement.health.check" changes nothing
47
+ And step "implement.closing" runs its script
48
+ And judge step "implement.coverage" answers:
49
+ """
50
+ [{"id":"AC-1","answer":"yes","p":0.9}]
51
+ """
52
+ And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
53
+ """
54
+ # Quality
55
+
56
+ ## AC-1 ignores floating point
57
+
58
+ Should subtract round? The criterion does not say.
59
+ """
60
+ And judge step "build.quality.triage" answers:
61
+ """
62
+ [{"id":"Q1","answer":"design","p":0.9}]
63
+ """
64
+ And step "build.quality.sorting" runs its script
65
+ And step "build.quality.closing" runs its script
66
+ And step "build.review.carrying" runs its script
67
+
68
+ Scenario: a chunk the budget cuts is listed as critical, the others judged in batches of their own
69
+ Given step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
70
+ """
71
+ # Review: abc1234
72
+
73
+ <!-- base: 0000000 -->
74
+
75
+ ## Subtract
76
+
77
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
78
+
79
+ ## AC-1 ignores floating point
80
+
81
+ - [ ] ./src/calc.ts#2-2 — returns the raw difference
82
+ """
83
+ And judge step "build.review.ranking", when its questions contain "C1.security", answers:
84
+ """
85
+ [{"id":"C1.security","answer":"no","p":0.9},{"id":"C1.reversibility","answer":"two-way","p":0.9},{"id":"C1.blastRadius","answer":"local","p":0.9},{"id":"C1.newConcept","answer":"no","p":0.9}]
86
+ """
87
+ And judge step "build.review.ranking", when its questions contain "C2.security", answers:
88
+ """
89
+ [{"id":"C2.security","answer":"no","p":0.9},{"id":"C2.reversibility","answer":"two-way","p":0.9},{"id":"C2.blastRadius","answer":"local","p":0.9},{"id":"C2.newConcept","answer":"no","p":0.9}]
90
+ """
91
+ When the process is driven
92
+ Then the process rests at "build.review.await-review"
93
+ And ".gtd/REVIEW.md" contains:
94
+ """
95
+ ## AC-1 ignores floating point
96
+
97
+ _critical: judge unsure_
98
+ """
99
+ And ".gtd/REVIEW.md" contains:
100
+ """
101
+ ## Subtract
102
+
103
+ _could_
104
+ """
105
+ And the commit subjects end with:
106
+ """
107
+ gtd(agent): build.review.reviewing → build.review.ranking
108
+ gtd(judge): build.review.ranking
109
+ gtd(judge): build.review.ranking → build.review.carrying
110
+ gtd(check): build.review.carrying → build.review.await-review
111
+ """