@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +60 -61
- package/dist/chunks/{reserved-DHafcW7t.mjs → reserved-B8EC-WE8.mjs} +77 -203
- package/dist/chunks/{review-BJGKwlYv.mjs → review-HdrX0FJ7.mjs} +30 -125
- package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
- package/dist/chunks/{tail-B9Hw2AFB.mjs → tail-BSLah2mA.mjs} +144 -56
- package/dist/gtd.bundle.mjs +259 -22
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +6 -6
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +551 -66
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +14 -16
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +522 -453
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +3 -3
- package/src/cli/Cli.ts +5 -5
- package/src/edge/Edge.test.ts +1 -1
- package/src/steering/FORMATS.md +5 -1
- package/src/steering/SteeringFormats.test.ts +2 -2
- package/src/steering/contract.test.ts +1 -0
- package/src/steering/findings.test.ts +47 -0
- package/src/steering/findings.ts +350 -0
- package/src/steering/fixtures/findings/gate.md +34 -0
- package/src/steering/fixtures/findings/gate.read.json +87 -0
- package/src/steering/index.test.ts +2 -2
- package/src/steering/index.ts +4 -3
- package/src/steering/review.test.ts +1 -75
- package/src/steering/review.ts +1 -74
- package/src/workflow/compile.test.ts +1 -0
- package/src/workflows/feature/criteria-coverage.feature +1 -6
- package/src/workflows/feature/feature.test.ts +1 -1
- package/src/workflows/feature/feature.ts +12 -20
- package/src/workflows/feature/lens-review.feature +323 -171
- package/src/workflows/feature/refactor-absorbs.feature +1 -7
- package/src/workflows/feature/refactor-first.feature +2 -10
- package/src/workflows/feature/refactor-rebuild.feature +4 -24
- package/src/workflows/feature/refactor-review.feature +33 -117
- package/src/workflows/feature/refactor.test.ts +0 -2
- package/src/workflows/feature/refactor.ts +0 -1
- package/src/workflows/feature/review-gate.feature +6 -22
- package/src/workflows/feature/reviewer-walkthrough.feature +16 -103
- package/src/workflows/feature/standards.feature +75 -34
- package/src/workflows/fix/fix-lens-origin.feature +20 -7
- package/src/workflows/fix/fix-quality.feature +102 -23
- package/src/workflows/review/review-door.feature +62 -59
- package/src/workflows/shared/learn.test.ts +0 -14
- package/src/workflows/shared/learn.ts +1 -6
- package/src/workflows/shared/lenses.ts +149 -43
- package/src/workflows/shared/review.test.ts +27 -26
- package/src/workflows/shared/review.ts +29 -196
- package/src/workflows/shared/tail.ts +18 -9
- package/src/workflows/shared/text.test.ts +103 -288
- package/src/workflows/shared/text.ts +131 -329
- package/src/workflows/feature/review-answers.feature +0 -324
- package/src/workflows/feature/severity-budget.feature +0 -116
- 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
|
|
6
|
-
|
|
7
|
-
review comes back. A sign-off ends
|
|
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.
|
|
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
|
|
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.
|
|
45
|
-
gtd(check): build.quality.
|
|
46
|
-
gtd(
|
|
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
|
-
##
|
|
51
|
-
|
|
52
|
-
Add a test for add.
|
|
66
|
+
## Calculator
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
- [ ] ./src/calc.ts#1-1 — add(a, b)
|
|
55
69
|
|
|
56
|
-
|
|
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
|
-
|
|
74
|
+
Dropped at the finding gate:
|
|
75
|
+
|
|
76
|
+
- add has no test (lens correctness)
|
|
65
77
|
"""
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
76
|
-
Given step "build.review.
|
|
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 "
|
|
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.
|
|
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.
|
|
92
|
-
gtd(
|
|
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:
|
|
103
|
-
Given judge step "build.review.
|
|
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":"
|
|
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 "
|
|
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.
|
|
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.
|
|
145
|
-
gtd(check): build.quality.
|
|
146
|
-
gtd(
|
|
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
|
|
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.
|
|
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.
|
|
194
|
-
gtd(
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 = {
|
|
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
|
-
*
|
|
143
|
-
*
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
166
|
-
|
|
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
|
}
|