@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
@@ -2,9 +2,10 @@
2
2
  Feature: the review door fixes its notes in the same process
3
3
 
4
4
  `gtd door review` runs the feature's review over everything since its base:
5
- the triaged quality lap writes `.gtd/REVIEW.md`, the human answers each note,
6
- and what they ask for is fixed in place under the health loop before the
7
- review comes back. A sign-off ends the process; nothing is left for `fix`.
5
+ the triaged quality lap and its finding gate, then a reviewer's walkthrough
6
+ of the change with the lap's summary, where the human's notes are fixed in
7
+ place under the health loop before the review comes back. A sign-off ends
8
+ the process; nothing is left for `fix`.
8
9
 
9
10
  Background:
10
11
  Given the shipped workflows, as gtd install scaffolds them
@@ -24,15 +25,27 @@ Feature: the review door fixes its notes in the same process
24
25
 
25
26
  Add a test for add.
26
27
  """
27
- And step "build.quality.sorting" runs its script
28
28
  And judge step "build.quality.triage" answers:
29
29
  """
30
30
  [{"id":"Q1","answer":"design","p":0.9}]
31
31
  """
32
- And step "build.review.composing" runs its script
32
+ And step "build.quality.gating" runs its script
33
+ And step "build.quality.gate" changes nothing
34
+ And step "build.quality.closing" runs its script
35
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
36
+ """
37
+ # Review: abc1234
38
+
39
+ <!-- base: 0000000 -->
40
+
41
+ ## Calculator
42
+
43
+ - [ ] ./src/calc.ts#1-1 — add(a, b)
44
+ """
45
+ And step "build.review.carrying" runs its script
33
46
  And step "build.review.closing" runs its script
34
47
 
35
- Scenario: the review is composed from the lap's notes, with answer boxes
48
+ Scenario: the review is a walkthrough, its summary listing what the finding gate dropped
36
49
  When gtd opens door "review" with "HEAD~1"
37
50
  And the process is driven
38
51
  Then the process rests at "build.review.await-review"
@@ -41,72 +54,65 @@ Feature: the review door fixes its notes in the same process
41
54
  gtd(human): review-gate.check
42
55
  gtd(check): review-gate.check → build.quality.correctness.reviewing
43
56
  gtd(agent): build.quality.correctness.reviewing → build.quality.triage
44
- gtd(judge): build.quality.triage → build.quality.sorting
45
- gtd(check): build.quality.sorting → build.review.composing
46
- gtd(check): build.review.composing → build.review.await-review
57
+ gtd(judge): build.quality.triage → build.quality.gating
58
+ gtd(check): build.quality.gating → build.quality.gate
59
+ gtd(human): build.quality.gate → build.quality.closing
60
+ gtd(check): build.quality.closing → build.review.reviewing
61
+ gtd(agent): build.review.reviewing → build.review.carrying
62
+ gtd(check): build.review.carrying → build.review.await-review
47
63
  """
48
64
  And ".gtd/REVIEW.md" contains:
49
65
  """
50
- ## add has no test
51
-
52
- Add a test for add.
66
+ ## Calculator
53
67
 
54
- Tick exactly one:
68
+ - [ ] ./src/calc.ts#1-1 — add(a, b)
55
69
 
56
- - [ ] fix as suggested
57
- - [ ] won't fix
58
- - [ ] _your answer_
70
+ ## Quality lap
59
71
  """
60
-
61
- Scenario: a won't-fix signs off, and no review is left for fix
62
- Given judge step "build.review.standards.judge" answers:
72
+ And ".gtd/REVIEW.md" contains:
63
73
  """
64
- [{"id":"standard-1","answer":"no","p":0.9}]
74
+ Dropped at the finding gate:
75
+
76
+ - add has no test (lens correctness)
65
77
  """
66
- When gtd opens door "review" with "HEAD~1"
67
- And the process is driven
68
- And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
69
- And gtd lands
78
+ And ".gtd/REVIEW.md" does not contain "Tick exactly one:"
79
+ When gtd lands
70
80
  And the process is driven
71
81
  Then the process rests at "idle"
72
82
  And ".gtd/REVIEW.md" does not exist
73
83
  And ".gtd/REVIEW.orig.md" does not exist
74
84
 
75
- Scenario: a note ticked fix as suggested is fixed in place, then reviewed again
76
- Given step "build.review.fix.nits.fixing" writes "src/calc.test.ts" with:
85
+ Scenario: a nit note is fixed in place, then reviewed again
86
+ Given judge step "build.review.triage" answers:
87
+ """
88
+ [{"id":"note-1","answer":"nit","p":0.9}]
89
+ """
90
+ And step "build.review.fix.nits.fixing" writes "src/calc.test.ts" with:
77
91
  """
78
92
  it("adds", () => {})
79
93
  """
80
94
  When gtd opens door "review" with "HEAD~1"
81
95
  And the process is driven
82
- And the text "- [ ] fix as suggested" in ".gtd/REVIEW.md" is replaced with "- [x] fix as suggested"
96
+ And the text "— add(a, b)" in ".gtd/REVIEW.md" is replaced with "— add(a, b) — add a test for it"
83
97
  And gtd lands
84
98
  And the process is driven
85
99
  Then the process rests at "build.review.await-review"
86
100
  And the commit subjects end with:
87
101
  """
88
- gtd(human): build.review.await-review → build.review.fix.nits.fixing
102
+ gtd(human): build.review.await-review → build.review.triage
103
+ gtd(judge): build.review.triage → build.review.fix.nits.fixing
89
104
  gtd(agent): build.review.fix.nits.fixing → build.health.check
90
105
  gtd(check): build.health.check → build.review.closing
91
- gtd(check): build.review.closing → build.review.composing
92
- gtd(check): build.review.composing → build.review.await-review
106
+ gtd(check): build.review.closing → build.review.reviewing
107
+ gtd(agent): build.review.reviewing → build.review.carrying
108
+ gtd(check): build.review.carrying → build.review.await-review
93
109
  """
94
- And ".gtd/REVIEW.md" contains "## Fixed from your notes"
95
110
  And "src/calc.test.ts" contains "adds"
96
- When gtd lands
97
- And the process is driven
98
- Then the process rests at "idle"
99
- And ".gtd/REVIEW.md" does not exist
100
- And ".gtd/REVIEW.orig.md" does not exist
101
111
 
102
- Scenario: an own answer asking for a change is fixed in place, then reviewed again
103
- Given judge step "build.review.standards.judge" answers:
104
- """
105
- [{"id":"standard-1","answer":"no","p":0.9}]
106
- """
107
- And judge step "build.review.triage" answers:
112
+ Scenario: a note asking for a change is fixed in place, then reviewed again
113
+ Given judge step "build.review.triage" answers:
108
114
  """
109
- [{"id":"answer-1","answer":"edit","p":0.9}]
115
+ [{"id":"note-1","answer":"edit","p":0.9}]
110
116
  """
111
117
  And step "build.review.collecting" writes ".gtd/REQUIREMENTS.md" with:
112
118
  """
@@ -125,14 +131,13 @@ Feature: the review door fixes its notes in the same process
125
131
  And step "feedback.closing" runs its script
126
132
  When gtd opens door "review" with "HEAD~1"
127
133
  And the process is driven
128
- And the text "- [ ] _your answer_" in ".gtd/REVIEW.md" is replaced with "- [x] test negatives too"
134
+ And the text "— add(a, b)" in ".gtd/REVIEW.md" is replaced with "— add(a, b) — test negatives too"
129
135
  And gtd lands
130
136
  And the process is driven
131
137
  Then the process rests at "build.review.await-review"
132
138
  And the commit subjects end with:
133
139
  """
134
- gtd(human): build.review.await-review → build.review.standards.judge
135
- gtd(judge): build.review.standards.judge → build.review.triage
140
+ gtd(human): build.review.await-review → build.review.triage
136
141
  gtd(judge): build.review.triage → build.review.closing
137
142
  gtd(check): build.review.closing → build.review.collecting
138
143
  gtd(agent): build.review.collecting → re-unwind
@@ -141,9 +146,12 @@ Feature: the review door fixes its notes in the same process
141
146
  gtd(check): feedback.closing → build.health.check
142
147
  gtd(check): build.health.check → build.quality.correctness.reviewing
143
148
  gtd(agent): build.quality.correctness.reviewing → build.quality.triage
144
- gtd(judge): build.quality.triage → build.quality.sorting
145
- gtd(check): build.quality.sorting → build.review.composing
146
- gtd(check): build.review.composing → build.review.await-review
149
+ gtd(judge): build.quality.triage → build.quality.gating
150
+ gtd(check): build.quality.gating → build.quality.gate
151
+ gtd(human): build.quality.gate → build.quality.closing
152
+ gtd(check): build.quality.closing → build.review.reviewing
153
+ gtd(agent): build.review.reviewing → build.review.carrying
154
+ gtd(check): build.review.carrying → build.review.await-review
147
155
  """
148
156
  And ".gtd/REQUIREMENTS.md" does not exist
149
157
  And "src/calc.test.ts" contains "adds negatives"
@@ -158,17 +166,12 @@ Feature: the review door fixes its notes in the same process
158
166
  Then opening door "review" is refused with ".gtd/REQUIREMENTS.md is left over"
159
167
 
160
168
  Scenario: a note added with its own boxes is fixed as ticked, never judged
161
- Given judge step "build.review.standards.judge" answers:
162
- """
163
- [{"id":"standard-1","answer":"no","p":0.9}]
164
- """
165
- And step "build.review.fix.nits.fixing" writes "src/calc.ts" with:
169
+ Given step "build.review.fix.nits.fixing" writes "src/calc.ts" with:
166
170
  """
167
171
  export const add = (a: number, b: number): number => a + b
168
172
  """
169
173
  When gtd opens door "review" with "HEAD~1"
170
174
  And the process is driven
171
- And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
172
175
  And ".gtd/REVIEW.md" gets appended:
173
176
  """
174
177
  ## src/calc.ts:1 (thread PRRT_1)
@@ -186,11 +189,11 @@ Feature: the review door fixes its notes in the same process
186
189
  Then the process rests at "build.review.await-review"
187
190
  And the commit subjects end with:
188
191
  """
189
- gtd(human): build.review.await-review → build.review.standards.judge
190
- gtd(judge): build.review.standards.judge → build.review.fix.nits.fixing
192
+ gtd(human): build.review.await-review → build.review.fix.nits.fixing
191
193
  gtd(agent): build.review.fix.nits.fixing → build.health.check
192
194
  gtd(check): build.health.check → build.review.closing
193
- gtd(check): build.review.closing → build.review.composing
194
- gtd(check): build.review.composing → build.review.await-review
195
+ gtd(check): build.review.closing → build.review.reviewing
196
+ gtd(agent): build.review.reviewing → build.review.carrying
197
+ gtd(check): build.review.carrying → build.review.await-review
195
198
  """
196
199
  And "src/calc.ts" contains "): number =>"
@@ -117,20 +117,6 @@ describe("learnStandards", () => {
117
117
  })
118
118
 
119
119
  describe("learnStandards, from a won't-fix", () => {
120
- it("states the finding, not gtd's banner", async () => {
121
- state.answers = { "standard-1": yes }
122
- await learnStandards([
123
- {
124
- anchor: "Slow loop",
125
- // As the review parser hands it over: paragraphs joined by a space.
126
- body: "_The fixer left this unresolved._ The loop is quadratic.",
127
- choice: "wontfix",
128
- text: "",
129
- },
130
- ])
131
- expect(kept()).toBe(file("- Won't fix: Slow loop — The loop is quadratic."))
132
- })
133
-
134
120
  it("replaces an old standard hand-edited in case only", async () => {
135
121
  state.files[STANDARDS] = file(
136
122
  "- use classes.",
@@ -32,13 +32,8 @@ interface Candidate {
32
32
 
33
33
  const SUMMARY_MAX = 200
34
34
 
35
- // The banners composedReview opens a note with say nothing about the finding; the
36
- // parsed body has its paragraphs joined, so they are matched by their own text.
37
- const BANNER =
38
- /^_(?:The fixer left this unresolved\.|A fix turn removed this while renaming another finding — check whether it was fixed\.|No detail given\.)_\s*/
39
-
40
35
  const summary = (body: string): string => {
41
- const text = oneLine(body).replace(BANNER, "")
36
+ const text = oneLine(body)
42
37
  return text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX - 1)}…` : text
43
38
  }
44
39
 
@@ -1,17 +1,24 @@
1
1
  import {
2
2
  answered,
3
3
  head,
4
+ human,
4
5
  judge,
5
6
  mark,
6
7
  numeric,
7
8
  read,
8
- removeScript,
9
+ refuse,
9
10
  run,
10
11
  sectionBodies,
11
12
  vars,
12
13
  writeScript,
13
14
  type JudgeQuestion,
14
15
  } from "../../flows/index.js"
16
+ import {
17
+ findingEntries,
18
+ formatFinding,
19
+ steeringFormatFor,
20
+ type FindingEntry,
21
+ } from "../../steering/index.js"
15
22
  import { FIX_CAP, guarded, healthy, type EscalationCount, type Guard } from "./health.js"
16
23
  import { fixCheck, fixQuality, QUALITY } from "./steps.js"
17
24
  import * as t from "./text.js"
@@ -27,19 +34,27 @@ export interface Finding {
27
34
 
28
35
  type FindingKind = "mechanical" | "noise" | "design"
29
36
 
30
- /** What the triaged quality lap did with its findings, for the review to list. */
37
+ /** What the triaged quality lap and its finding gate did with the findings, for the review to list. */
31
38
  export interface LensReport {
32
- /** Design findings, then the mechanical ones the fix cap left. */
33
- readonly notes: readonly (Finding &
34
- Origin & {
35
- readonly unresolved?: true
36
- readonly unclear?: true
37
- })[]
39
+ /** Fixed by the fix turns the judge sent them to, with the fixing commit. */
38
40
  readonly fixed: readonly (Finding & Origin & { readonly commit: string })[]
39
- readonly noise: readonly (Finding & Origin & { readonly p: number })[]
41
+ /** Ticked at the finding gate and fixed, with the fixing commit. */
42
+ readonly gateFixed: readonly (Finding & Origin & { readonly commit: string })[]
43
+ /** Left unticked at the finding gate. */
44
+ readonly dropped: readonly (Finding & Origin)[]
45
+ /** Called noise by the judge and left unticked at the finding gate; `p` absent on one the human added there. */
46
+ readonly noise: readonly (Finding & Origin & { readonly p?: number })[]
47
+ /** Ticked at the finding gate, still open after the fix turns. */
48
+ readonly unresolved: readonly (Finding & Origin & { readonly unclear?: true })[]
40
49
  }
41
50
 
42
- export const NO_FINDINGS: LensReport = { notes: [], fixed: [], noise: [] }
51
+ export const NO_FINDINGS: LensReport = {
52
+ fixed: [],
53
+ gateFixed: [],
54
+ dropped: [],
55
+ noise: [],
56
+ unresolved: [],
57
+ }
43
58
 
44
59
  const UNTITLED = "Quality findings"
45
60
 
@@ -138,14 +153,117 @@ export interface TriageOptions {
138
153
  readonly lensOf?: ((f: Finding) => string | undefined) | undefined
139
154
  }
140
155
 
156
+ type Note = Finding & Origin & { readonly unresolved?: true; readonly unclear?: true }
157
+
158
+ interface Fixing {
159
+ readonly fixed: readonly (Finding & Origin & { readonly commit: string })[]
160
+ readonly open: readonly (Finding & Origin)[]
161
+ readonly unclear: readonly (Finding & Origin)[]
162
+ }
163
+
164
+ const NOTHING_FIXED: Fixing = { fixed: [], open: [], unclear: [] }
165
+
166
+ /** Up to `FIX_CAP` fix turns over `.gtd/QUALITY.md`, which holds one chunk per finding in `findings`. */
167
+ const fixTurns = async (
168
+ findings: readonly (Finding & Origin)[],
169
+ options: TriageOptions,
170
+ ): Promise<Fixing> => {
171
+ const fixed: (Finding & Origin & { commit: string })[] = []
172
+ const unclear: (Finding & Origin)[] = []
173
+ let open: readonly (Finding & Origin)[] = findings
174
+ for (let turns = 0; turns < FIX_CAP && open.length > 0; turns++) {
175
+ await guarded(() => fixQuality(options.rules), options.guard)()
176
+ const turn = afterFix(open, findingsOf(read(QUALITY) ?? ""))
177
+ const commit = head()
178
+ fixed.push(...turn.fixed.map((f) => ({ ...f, commit })))
179
+ unclear.push(...turn.unclear)
180
+ // A turn that renamed a chunk hands back fresh parses: keep the lens a same-titled finding had.
181
+ open = turn.open.map((f) => ({ ...findings.find((m) => m.title === f.title), ...f }))
182
+ }
183
+ const { escalations, guard, guardRests, rules } = options
184
+ await healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules })
185
+ return { fixed, open, unclear }
186
+ }
187
+
188
+ /** Matches each entry the human left to the finding it was written from, by title, first unused first; `left` is what no entry claimed. */
189
+ const matcher = <F extends Finding>(findings: readonly F[]) => {
190
+ const left = [...findings]
191
+ const of = (e: FindingEntry): F | undefined => {
192
+ const at = left.findIndex((f) => f.title === e.title)
193
+ return at < 0 ? undefined : left.splice(at, 1)[0]
194
+ }
195
+ return { of, left }
196
+ }
197
+
198
+ const gateProblems = (text: string): readonly string[] =>
199
+ steeringFormatFor("findings")!
200
+ .validate(text)
201
+ .map((f) => ` - ${formatFinding(QUALITY, f)}`)
202
+
141
203
  /**
142
- * Judge each finding in `.gtd/QUALITY.md`, set the noise and design ones
143
- * aside, and fix the mechanical ones for up to `FIX_CAP` turns. Leaves
144
- * `.gtd/QUALITY.md` holding only what the fixer left: the caller must remove
145
- * it, or the next lap appends to stale findings.
204
+ * The human gate over the open notes and the noise: a ticked finding is
205
+ * fixed — never re-planned — and the rest dropped. No notes and no noise, no gate.
206
+ */
207
+ const findingGate = async (
208
+ notes: readonly Note[],
209
+ noise: readonly (Finding & Origin & { readonly p: number })[],
210
+ options: TriageOptions,
211
+ ): Promise<Omit<LensReport, "fixed">> => {
212
+ if (notes.length + noise.length === 0) return NO_FINDINGS
213
+ const written = t.findingGateFile(notes, noise)
214
+ await run("quality.gating", writeScript(QUALITY, written), {
215
+ label: "Listing the findings for you",
216
+ })
217
+ await human("quality.gate", {
218
+ message: t.findingGateMessage(),
219
+ label: "Deciding on the quality findings",
220
+ file: QUALITY,
221
+ mode: "findings",
222
+ acceptClean: true,
223
+ })
224
+ // A deleted file decides nothing: every finding is dropped, as if untouched.
225
+ const answered = read(QUALITY) ?? written
226
+ const problems = gateProblems(answered)
227
+ if (problems.length > 0)
228
+ refuse(
229
+ `gtd land: finding-gate: ${problems.length} problem(s) in ${QUALITY} — keep one \`- [ ] fix\` box per finding, ticked to fix it or unticked to drop it, and write guidance only under a ticked one:\n${problems.join("\n")}`,
230
+ )
231
+ const noteOf = matcher(notes)
232
+ const noiseOf = matcher(noise)
233
+ // An entry the human added is a finding of its own.
234
+ const decided = findingEntries(answered).map((e) => ({
235
+ e,
236
+ finding: (e.noise ? noiseOf.of(e) : noteOf.of(e)) ?? { id: "", title: e.title, body: e.body },
237
+ }))
238
+ // The finding as gtd wrote it: a human steers with guidance, not by editing its text.
239
+ const ticked = decided
240
+ .filter(({ e }) => e.fix)
241
+ .map(({ e, finding }) => ({ ...finding, body: t.withGuidance(finding.body, e.guidance) }))
242
+ let fixing = NOTHING_FIXED
243
+ if (ticked.length > 0) {
244
+ await run("quality.picking", writeScript(QUALITY, t.qualityFindingsFile(ticked)), {
245
+ label: "Keeping the findings you ticked",
246
+ })
247
+ fixing = await fixTurns(ticked, options)
248
+ }
249
+ const unticked = (noisy: boolean) =>
250
+ decided.filter(({ e }) => !e.fix && e.noise === noisy).map(({ finding }) => finding)
251
+ // A finding whose entry the human deleted is dropped too.
252
+ return {
253
+ gateFixed: fixing.fixed,
254
+ dropped: [...unticked(false), ...noteOf.left],
255
+ noise: [...unticked(true), ...noiseOf.left],
256
+ unresolved: [...fixing.open, ...fixing.unclear.map((f) => ({ ...f, unclear: true as const }))],
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Judge each finding in `.gtd/QUALITY.md` and fix the mechanical ones for up
262
+ * to `FIX_CAP` turns; the rest — design, unresolved and noise — go to the
263
+ * finding gate. Leaves `.gtd/QUALITY.md` holding only what the last fixer
264
+ * left: the caller must remove it, or the next lap appends to stale findings.
146
265
  */
147
266
  export const triageFindings = async (options: TriageOptions): Promise<LensReport> => {
148
- const { escalations, guard, guardRests, rules } = options
149
267
  const withLens = (f: Finding): Finding & Origin => {
150
268
  const lens = options.lensOf?.(f)
151
269
  return lens === undefined ? f : { ...f, lens }
@@ -153,36 +271,24 @@ export const triageFindings = async (options: TriageOptions): Promise<LensReport
153
271
  const findings = findingsOf(read(QUALITY) ?? "").map(withLens)
154
272
  if (findings.length === 0) return NO_FINDINGS
155
273
  const { mechanical, noise, design } = await classify(findings)
156
- // Rewritten whenever it is not one chunk per mechanical finding: the fixer deletes chunks.
157
- const sorted = t.qualityFindingsFile(mechanical)
158
- if (read(QUALITY) !== sorted) {
159
- await run(
160
- "quality.sorting",
161
- mechanical.length === 0 ? removeScript([QUALITY]) : writeScript(QUALITY, sorted),
162
- { label: "Setting findings aside for the review" },
163
- )
274
+ let auto = NOTHING_FIXED
275
+ if (mechanical.length > 0) {
276
+ // Rewritten whenever it is not one chunk per mechanical finding: the fixer deletes chunks.
277
+ const sorted = t.qualityFindingsFile(mechanical)
278
+ if (read(QUALITY) !== sorted)
279
+ await run("quality.sorting", writeScript(QUALITY, sorted), {
280
+ label: "Setting findings aside for the finding gate",
281
+ })
282
+ auto = await fixTurns(mechanical, options)
164
283
  }
165
- const fixed: (Finding & { commit: string })[] = []
166
- const unclear: Finding[] = []
167
- let open: readonly Finding[] = mechanical
168
- for (let turns = 0; turns < FIX_CAP && open.length > 0; turns++) {
169
- await guarded(() => fixQuality(rules), guard)()
170
- const turn = afterFix(open, findingsOf(read(QUALITY) ?? ""))
171
- const commit = head()
172
- fixed.push(...turn.fixed.map((f) => ({ ...f, commit })))
173
- unclear.push(...turn.unclear)
174
- // A turn that renamed a chunk hands back fresh parses: keep the lens a same-titled finding had.
175
- open = turn.open.map((f) => ({ ...mechanical.find((m) => m.title === f.title), ...f }))
176
- }
177
- if (mechanical.length > 0)
178
- await healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules })
179
- return {
180
- notes: [
284
+ const gated = await findingGate(
285
+ [
181
286
  ...design,
182
- ...open.map((f) => ({ ...f, unresolved: true as const })),
183
- ...unclear.map((f) => ({ ...f, unclear: true as const })),
287
+ ...auto.open.map((f) => ({ ...f, unresolved: true as const })),
288
+ ...auto.unclear.map((f) => ({ ...f, unclear: true as const })),
184
289
  ],
185
- fixed,
186
290
  noise,
187
- }
291
+ options,
292
+ )
293
+ return { ...gated, fixed: auto.fixed }
188
294
  }