@pmelab/gtd 58.2.0 → 59.0.1
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 +120 -395
- package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-07Z_ENch.mjs} +13503 -3460
- package/dist/chunks/{wire-C12dahrH.mjs → wire-BTUhRGXj.mjs} +1 -1
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +484 -497
- package/dist/testkit/gherkin.mjs +145 -65
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +1 -1
- package/package.json +3 -42
- package/skills/authoring/SKILL.md +34 -27
- package/src/cli/Cli.test.ts +148 -71
- package/src/cli/Cli.ts +384 -463
- package/src/cli/Help.test.ts +47 -0
- package/src/cli/Help.ts +70 -0
- package/src/cli/Program.test.ts +127 -33
- package/src/cli/Program.ts +70 -25
- package/src/edge/Edge.ts +4 -4
- package/src/emit/Emit.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/flags/index.test.ts +0 -13
- package/src/flags/index.ts +1 -3
- package/src/flows/factory.ts +3 -1
- package/src/gherkin/index.ts +135 -71
- package/src/gherkin/messages.test.ts +90 -0
- package/src/gherkin/messages.ts +84 -0
- package/src/install/Install.test.ts +14 -11
- package/src/install/Install.ts +38 -15
- package/src/install/index.ts +1 -2
- package/src/replay/Replay.test.ts +1 -1
- package/src/replay/index.ts +1 -1
- package/src/step/planStart.ts +6 -1
- package/src/testkit/TestRepo.test.ts +47 -2
- package/src/testkit/TestRepo.ts +51 -6
- package/src/twins/EmittedScriptRecognizer.ts +5 -8
- package/src/twins/FlowScripts.ts +1 -1
- package/src/wire/Land.ts +1 -1
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +138 -113
- package/src/workflow/load.ts +7 -7
- package/src/workflow/skills.test.ts +5 -5
- package/src/workflow/skills.ts +1 -1
- package/src/workflow/vars.test.ts +30 -3
- package/src/workflow/vars.ts +34 -2
- package/src/workflow-kit/index.ts +1 -1
- package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
- package/dist/chunks/review-Bfaror38.mjs +0 -427
- package/dist/chunks/tail-Bd7L940p.mjs +0 -355
- package/dist/chunks/testkit-JDtpNKyi.mjs +0 -10410
- package/dist/workflows/cleanup.mjs +0 -36
- package/dist/workflows/feature.mjs +0 -1018
- package/dist/workflows/fix.mjs +0 -41
- package/dist/workflows/review.mjs +0 -45
- package/dist/workflows/rollout.mjs +0 -48
- package/dist/workflows.mjs +0 -40651
- package/src/install/ConfigScaffold.ts +0 -22
- package/src/workflows/cleanup/cleanup-path.feature +0 -94
- package/src/workflows/cleanup/cleanup.feature +0 -315
- package/src/workflows/cleanup/cleanup.test.ts +0 -276
- package/src/workflows/cleanup/cleanup.ts +0 -52
- package/src/workflows/cleanup/index.ts +0 -6
- package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
- package/src/workflows/feature/acceptance-criteria.feature +0 -389
- package/src/workflows/feature/criteria-coverage.feature +0 -102
- package/src/workflows/feature/criteria.test.ts +0 -147
- package/src/workflows/feature/criteria.ts +0 -267
- package/src/workflows/feature/fast-path.feature +0 -93
- package/src/workflows/feature/feature.test.ts +0 -338
- package/src/workflows/feature/feature.ts +0 -156
- package/src/workflows/feature/green.ts +0 -19
- package/src/workflows/feature/index.ts +0 -18
- package/src/workflows/feature/lens-models.feature +0 -69
- package/src/workflows/feature/lens-review.feature +0 -566
- package/src/workflows/feature/naive-acceptance.feature +0 -147
- package/src/workflows/feature/naive-test.feature +0 -263
- package/src/workflows/feature/one-step-land.feature +0 -93
- package/src/workflows/feature/planning.ts +0 -41
- package/src/workflows/feature/refactor-absorbs.feature +0 -131
- package/src/workflows/feature/refactor-first.feature +0 -158
- package/src/workflows/feature/refactor-lens-models.feature +0 -79
- package/src/workflows/feature/refactor-rebuild.feature +0 -273
- package/src/workflows/feature/refactor-review.feature +0 -1528
- package/src/workflows/feature/refactor.test.ts +0 -1220
- package/src/workflows/feature/refactor.ts +0 -368
- package/src/workflows/feature/release-flag.feature +0 -167
- package/src/workflows/feature/release-flag.test.ts +0 -49
- package/src/workflows/feature/release-flag.ts +0 -46
- package/src/workflows/feature/review-gate.feature +0 -241
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
- package/src/workflows/feature/risk-tier.feature +0 -351
- package/src/workflows/feature/seam-rejection.feature +0 -146
- package/src/workflows/feature/seam.feature +0 -297
- package/src/workflows/feature/seam.test.ts +0 -527
- package/src/workflows/feature/seam.ts +0 -426
- package/src/workflows/feature/standards.feature +0 -354
- package/src/workflows/feature/steps.test.ts +0 -65
- package/src/workflows/feature/steps.ts +0 -45
- package/src/workflows/feature/tier-pin.feature +0 -66
- package/src/workflows/feature/tier.test.ts +0 -166
- package/src/workflows/feature/tier.ts +0 -56
- package/src/workflows/fix/fix-lens-origin.feature +0 -114
- package/src/workflows/fix/fix-quality.feature +0 -212
- package/src/workflows/fix/fix.feature +0 -79
- package/src/workflows/fix/fix.test.ts +0 -74
- package/src/workflows/fix/fix.ts +0 -52
- package/src/workflows/fix/index.ts +0 -6
- package/src/workflows/index.ts +0 -47
- package/src/workflows/review/index.ts +0 -6
- package/src/workflows/review/review-door.feature +0 -199
- package/src/workflows/review/review.test.ts +0 -27
- package/src/workflows/review/review.ts +0 -60
- package/src/workflows/rollout/index.ts +0 -6
- package/src/workflows/rollout/rollout.feature +0 -510
- package/src/workflows/rollout/rollout.test.ts +0 -263
- package/src/workflows/rollout/rollout.ts +0 -82
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
- package/src/workflows/shared/access.test.ts +0 -62
- package/src/workflows/shared/access.ts +0 -24
- package/src/workflows/shared/config.fixture.ts +0 -12
- package/src/workflows/shared/fences.ts +0 -24
- package/src/workflows/shared/health.test.ts +0 -148
- package/src/workflows/shared/health.ts +0 -191
- package/src/workflows/shared/learn.test.ts +0 -140
- package/src/workflows/shared/learn.ts +0 -201
- package/src/workflows/shared/lenses.test.ts +0 -39
- package/src/workflows/shared/lenses.ts +0 -294
- package/src/workflows/shared/options.test.ts +0 -157
- package/src/workflows/shared/options.ts +0 -92
- package/src/workflows/shared/progress.feature +0 -124
- package/src/workflows/shared/prose.ts +0 -263
- package/src/workflows/shared/recheck.feature +0 -60
- package/src/workflows/shared/registry.test.ts +0 -345
- package/src/workflows/shared/registry.ts +0 -183
- package/src/workflows/shared/reserved.ts +0 -11
- package/src/workflows/shared/review.test.ts +0 -1062
- package/src/workflows/shared/review.ts +0 -446
- package/src/workflows/shared/skills.test.ts +0 -121
- package/src/workflows/shared/skills.ts +0 -49
- package/src/workflows/shared/standards.test.ts +0 -90
- package/src/workflows/shared/standards.ts +0 -156
- package/src/workflows/shared/steps.test.ts +0 -219
- package/src/workflows/shared/steps.ts +0 -201
- package/src/workflows/shared/tail.test.ts +0 -454
- package/src/workflows/shared/tail.ts +0 -264
- package/src/workflows/shared/text.fixture.ts +0 -128
- package/src/workflows/shared/text.test.ts +0 -1430
- package/src/workflows/shared/text.ts +0 -1700
- package/src/workflows/shared/threads.test.ts +0 -129
- package/src/workflows/shared/vars.ts +0 -47
- package/src/workflows/shared/workflows.steps.ts +0 -54
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
answered,
|
|
3
|
-
changesSince,
|
|
4
|
-
head,
|
|
5
|
-
judge,
|
|
6
|
-
numeric,
|
|
7
|
-
read,
|
|
8
|
-
refuse,
|
|
9
|
-
requireRevert,
|
|
10
|
-
restoreScript,
|
|
11
|
-
run,
|
|
12
|
-
vars,
|
|
13
|
-
writeScript,
|
|
14
|
-
type JudgeQuestion,
|
|
15
|
-
type Judgment,
|
|
16
|
-
type ScopeAccess,
|
|
17
|
-
} from "../../flows/index.js"
|
|
18
|
-
import {
|
|
19
|
-
REVIEW_SECTIONS,
|
|
20
|
-
reviewChunks,
|
|
21
|
-
reviewRisks,
|
|
22
|
-
reviewSections,
|
|
23
|
-
type ReviewNote,
|
|
24
|
-
} from "../../steering/index.js"
|
|
25
|
-
import * as kit from "../../workflow-kit/index.js"
|
|
26
|
-
import { guarded, healthy, type EscalationCount } from "./health.js"
|
|
27
|
-
import { NO_FINDINGS, type LensReport } from "./lenses.js"
|
|
28
|
-
import {
|
|
29
|
-
answerReviewQuestions,
|
|
30
|
-
awaitReview,
|
|
31
|
-
collecting,
|
|
32
|
-
fixCheck,
|
|
33
|
-
fixNits,
|
|
34
|
-
fixRisks,
|
|
35
|
-
REQUIREMENTS,
|
|
36
|
-
REVIEW,
|
|
37
|
-
reviewing,
|
|
38
|
-
reviewMissing,
|
|
39
|
-
} from "./steps.js"
|
|
40
|
-
import { learnStandards, settlesStandard, standardsAddedSince, standardsPending } from "./learn.js"
|
|
41
|
-
import { isConflictTitle, parseStandards, STANDARDS } from "./standards.js"
|
|
42
|
-
import { closeReview, isOptionalObject, tail, type TailOptions } from "./tail.js"
|
|
43
|
-
import * as t from "./text.js"
|
|
44
|
-
|
|
45
|
-
export type { ReviewOutcome } from "../../workflow-kit/index.js"
|
|
46
|
-
|
|
47
|
-
const VERDICTS: readonly kit.Verdict[] = ["edit", "question", "nit", "praise"]
|
|
48
|
-
|
|
49
|
-
/** One four-way choice per note the human added to `.gtd/REVIEW.md`. */
|
|
50
|
-
const verdictQuestion = (note: ReviewNote): JudgeQuestion => ({
|
|
51
|
-
id: note.id,
|
|
52
|
-
primitive: "choice",
|
|
53
|
-
instructions: `Classify the human's note on review item "${note.anchor}" (in .gtd/REVIEW.md). Its evidence holds the anchor, the reviewer's text and the human's text.`,
|
|
54
|
-
criteria:
|
|
55
|
-
"edit: a request to change behaviour, design or scope — anything needing a plan, or any note you are unsure about. question: asks something and wants an answer, with no change requested. nit: a small, local, unambiguous fix (naming, typo, formatting, comment wording) needing no re-plan. praise: an approving remark with nothing to do.",
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
type Tier = "critical" | "should" | "could"
|
|
59
|
-
|
|
60
|
-
const TIER_ORDER: Readonly<Record<Tier, number>> = { critical: 0, should: 1, could: 2 }
|
|
61
|
-
|
|
62
|
-
/** Capture order: critical, should, could; within a tier one-way doors, then new concepts. */
|
|
63
|
-
const captureKey = (s: Severity | undefined): number =>
|
|
64
|
-
s === undefined ? 2 : TIER_ORDER[s.tier] * 3 + s.rank
|
|
65
|
-
|
|
66
|
-
const SEVERITY_ANSWERS: Readonly<Record<string, readonly string[]>> = {
|
|
67
|
-
security: ["yes", "no"],
|
|
68
|
-
reversibility: ["one-way", "two-way"],
|
|
69
|
-
blastRadius: ["local", "module", "contract"],
|
|
70
|
-
newConcept: ["yes", "no"],
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface Severity {
|
|
74
|
-
readonly tier: Tier
|
|
75
|
-
readonly reason: string
|
|
76
|
-
/** Capture order: 0 one-way door, 1 new concept, 2 everything else. */
|
|
77
|
-
readonly rank: 0 | 1 | 2
|
|
78
|
-
readonly security?: true
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Anything unanswered, unrecognised, cut or below `minP` is `critical`, never `could`. */
|
|
82
|
-
export const severityOf = (
|
|
83
|
-
note: { readonly id: string; readonly forced?: boolean | undefined },
|
|
84
|
-
judgment: Judgment,
|
|
85
|
-
minP: number,
|
|
86
|
-
): Severity => {
|
|
87
|
-
const ans = (q: string) => judgment.answers[`${note.id}.${q}`]
|
|
88
|
-
const a = (q: string): string | undefined => ans(q)?.answer
|
|
89
|
-
const rank = (): 0 | 1 | 2 =>
|
|
90
|
-
a("reversibility") === "one-way" ? 0 : a("newConcept") === "yes" ? 1 : 2
|
|
91
|
-
if (note.forced) return { tier: "critical", reason: "critical: forced by `!`", rank: 2 }
|
|
92
|
-
const unsure =
|
|
93
|
-
judgment.truncated.includes(note.id) ||
|
|
94
|
-
Object.entries(SEVERITY_ANSWERS).some(
|
|
95
|
-
([q, allowed]) => !allowed.includes(a(q) ?? "") || !((ans(q)?.p ?? NaN) >= minP),
|
|
96
|
-
)
|
|
97
|
-
if (unsure) return { tier: "critical", reason: "critical: judge unsure", rank: 2 }
|
|
98
|
-
const critical = [
|
|
99
|
-
...(a("security") === "yes" ? ["security"] : []),
|
|
100
|
-
...(a("reversibility") === "one-way" ? ["one-way door"] : []),
|
|
101
|
-
]
|
|
102
|
-
if (critical.length > 0)
|
|
103
|
-
return {
|
|
104
|
-
tier: "critical",
|
|
105
|
-
reason: `critical: ${critical.join(", ")}`,
|
|
106
|
-
rank: rank(),
|
|
107
|
-
...(a("security") === "yes" ? { security: true as const } : {}),
|
|
108
|
-
}
|
|
109
|
-
const should = [
|
|
110
|
-
...(a("blastRadius") === "contract" ? ["contract"] : []),
|
|
111
|
-
...(a("newConcept") === "yes" ? ["new concept"] : []),
|
|
112
|
-
]
|
|
113
|
-
if (should.length > 0)
|
|
114
|
-
return { tier: "should", reason: `should: ${should.join(", ")}`, rank: rank() }
|
|
115
|
-
return { tier: "could", reason: "could", rank: 2 }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** `asks` names the change in full, `change` refers back to it. */
|
|
119
|
-
const severityQuestions = (id: string, asks: string, change: string): JudgeQuestion[] => [
|
|
120
|
-
{
|
|
121
|
-
id: `${id}.security`,
|
|
122
|
-
primitive: "noul",
|
|
123
|
-
instructions: `Does ${asks} touch security — authentication, authorisation, secrets, untrusted input or data exposure?`,
|
|
124
|
-
criteria: "yes: it changes or could weaken a security property. no: it does not.",
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
id: `${id}.reversibility`,
|
|
128
|
-
primitive: "choice",
|
|
129
|
-
instructions: `Once ${change} ships, can it be undone without breaking anyone who already depends on it?`,
|
|
130
|
-
criteria:
|
|
131
|
-
"one-way: a published API, file format, persisted data, protocol or other contract that cannot be taken back. two-way: can be reverted freely.",
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
id: `${id}.blastRadius`,
|
|
135
|
-
primitive: "choice",
|
|
136
|
-
instructions: `How far does ${change} reach?`,
|
|
137
|
-
criteria:
|
|
138
|
-
"local: one function or file. module: a whole module or several files. contract: a public interface other code or users depend on.",
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
id: `${id}.newConcept`,
|
|
142
|
-
primitive: "noul",
|
|
143
|
-
instructions: `Does ${change} introduce a concept new to this codebase — a new abstraction, entity or term? Judge from the evidence, never a glossary; reusing or extending an existing concept is not new.`,
|
|
144
|
-
criteria: "yes: a genuinely new concept. no: reuses or extends an existing one.",
|
|
145
|
-
},
|
|
146
|
-
]
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* One judge rest: a verdict per unforced note (a forced note is asked
|
|
150
|
-
* nothing) plus four severity answers. Dismissing a note is the risky
|
|
151
|
-
* direction, so only a confident non-`edit` verdict on uncut evidence counts;
|
|
152
|
-
* a cut, unanswered or below-floor note is an `edit`. Severity answers on a
|
|
153
|
-
* non-`edit` note are ignored; on an `edit` they order the collecting turn.
|
|
154
|
-
*/
|
|
155
|
-
const triage =
|
|
156
|
-
(severities: Map<string, Severity>) =>
|
|
157
|
-
async (notes: readonly ReviewNote[]): Promise<ReadonlyMap<string, kit.Verdict>> => {
|
|
158
|
-
const asked = notes.filter((n) => !n.forced)
|
|
159
|
-
const judgment: Judgment =
|
|
160
|
-
asked.length === 0
|
|
161
|
-
? { answers: {}, truncated: [] }
|
|
162
|
-
: await judge("review.triage", {
|
|
163
|
-
questions: asked.flatMap((n) => [
|
|
164
|
-
verdictQuestion(n),
|
|
165
|
-
...severityQuestions(
|
|
166
|
-
n.id,
|
|
167
|
-
`the change the human asks for in note "${n.id}" (on "${n.anchor}")`,
|
|
168
|
-
`the change in note "${n.id}"`,
|
|
169
|
-
),
|
|
170
|
-
]),
|
|
171
|
-
evidence: Object.fromEntries(
|
|
172
|
-
asked.map((n) => [
|
|
173
|
-
n.id,
|
|
174
|
-
`Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`,
|
|
175
|
-
]),
|
|
176
|
-
),
|
|
177
|
-
message: t.buildReviewTriageMessage(),
|
|
178
|
-
label: "Judging each note",
|
|
179
|
-
})
|
|
180
|
-
const { answers, truncated } = judgment
|
|
181
|
-
const minP = numeric(vars.reviewNoteActionable, Infinity)
|
|
182
|
-
const severityMinP = numeric(vars.reviewSeverityMinP, Infinity)
|
|
183
|
-
severities.clear()
|
|
184
|
-
return new Map(
|
|
185
|
-
notes.map((n): [string, kit.Verdict] => {
|
|
186
|
-
const verdict = n.forced
|
|
187
|
-
? undefined
|
|
188
|
-
: VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP))
|
|
189
|
-
if (verdict !== undefined && !truncated.includes(n.id)) return [n.id, verdict]
|
|
190
|
-
severities.set(n.id, severityOf(n, judgment, severityMinP))
|
|
191
|
-
return [n.id, "edit"]
|
|
192
|
-
}),
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export const isCode = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
|
|
197
|
-
|
|
198
|
-
/** Refuses a review with a chunk pointing at no code; a range with no code change has nothing to point at. */
|
|
199
|
-
const requirePointers = (review: string, codeChanged: boolean): void => {
|
|
200
|
-
const chunks = reviewChunks(review)
|
|
201
|
-
const problems = [
|
|
202
|
-
...chunks
|
|
203
|
-
.filter((c) => c.pointers === 0 && codeChanged)
|
|
204
|
-
.map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — points at no code`),
|
|
205
|
-
...chunks
|
|
206
|
-
.filter((c) => isConflictTitle(c.title))
|
|
207
|
-
.map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — gtd writes conflicts; leave it out`),
|
|
208
|
-
// Notes and answers are matched to their chunk by title.
|
|
209
|
-
...chunks
|
|
210
|
-
.filter((c, i) => chunks.findIndex((d) => d.title === c.title) !== i)
|
|
211
|
-
.map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — a second chunk with this title`),
|
|
212
|
-
...reviewSections(review).map(
|
|
213
|
-
(s) =>
|
|
214
|
-
` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} — write \`## \` chunks only`,
|
|
215
|
-
),
|
|
216
|
-
]
|
|
217
|
-
if (problems.length === 0) return
|
|
218
|
-
refuse(
|
|
219
|
-
`gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code:\n${problems.join("\n")}`,
|
|
220
|
-
)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* A reviewer walks the human through everything since `base`, every chunk
|
|
225
|
-
* pointing at its code; gtd then appends the summary chunks.
|
|
226
|
-
*/
|
|
227
|
-
const walkthrough = async (
|
|
228
|
-
base: string,
|
|
229
|
-
summary: t.ReviewSummary,
|
|
230
|
-
lap: t.ReviewRange,
|
|
231
|
-
carry: string | undefined,
|
|
232
|
-
nitFix: string | undefined,
|
|
233
|
-
afterRiskFix: boolean,
|
|
234
|
-
): Promise<void> => {
|
|
235
|
-
await reviewing(base, carry, lap, t.nitFixBlock(nitFix))
|
|
236
|
-
const written = read(REVIEW)
|
|
237
|
-
// A review the reviewer did not write is the round's own refusal or rest.
|
|
238
|
-
if (written === undefined) return
|
|
239
|
-
requirePointers(
|
|
240
|
-
written,
|
|
241
|
-
changesSince(base).some((c) => isCode(c.path)),
|
|
242
|
-
)
|
|
243
|
-
// Its risks are fixed and the review written again, so the re-reviewer edits this draft, never gtd's additions.
|
|
244
|
-
if (!afterRiskFix && reviewRisks(written).length > 0) return
|
|
245
|
-
const full = t.withSummary(written, summary)
|
|
246
|
-
if (full !== written) {
|
|
247
|
-
await run("review.carrying", writeScript(REVIEW, full), {
|
|
248
|
-
label: "Adding the quality lap's summary",
|
|
249
|
-
file: REVIEW,
|
|
250
|
-
base,
|
|
251
|
-
})
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* A reviewer's walkthrough of everything since `base` reaches a human, who
|
|
257
|
-
* may comment: each note is judged — edits end the review as `feedback` for
|
|
258
|
-
* the caller to build, questions are answered in place, nits fixed in one
|
|
259
|
-
* batch, praise dropped.
|
|
260
|
-
*/
|
|
261
|
-
export const humanReview = (
|
|
262
|
-
base: string,
|
|
263
|
-
escalations: EscalationCount = { rounds: 0 },
|
|
264
|
-
options: Omit<BuildTailOptions, "fixFirst" | "green"> = {},
|
|
265
|
-
): Promise<kit.ReviewOutcome> => {
|
|
266
|
-
const { guard, guardRests, fixAccess, rules } = options
|
|
267
|
-
const feedback: FeedbackTarget = options.feedback ?? {
|
|
268
|
-
file: REQUIREMENTS,
|
|
269
|
-
collecting,
|
|
270
|
-
unmatched:
|
|
271
|
-
"gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing",
|
|
272
|
-
}
|
|
273
|
-
const severities = new Map<string, Severity>()
|
|
274
|
-
// Set by a nit fix, so the fresh review that follows names its commit.
|
|
275
|
-
let nitFix: string | undefined
|
|
276
|
-
// Set by a risk fix, so the review written next reaches the human whatever it marks.
|
|
277
|
-
let riskFixed = false
|
|
278
|
-
return kit.review(base, {
|
|
279
|
-
file: REVIEW,
|
|
280
|
-
feedback: feedback.file,
|
|
281
|
-
isCode,
|
|
282
|
-
reviewing: (b, carry) => {
|
|
283
|
-
const fixedAt = nitFix
|
|
284
|
-
const afterRiskFix = riskFixed
|
|
285
|
-
riskFixed = false
|
|
286
|
-
const summary: t.ReviewSummary = {
|
|
287
|
-
report: options.report ?? NO_FINDINGS,
|
|
288
|
-
standards: {
|
|
289
|
-
added: standardsAddedSince(b),
|
|
290
|
-
undecided: parseStandards(read(STANDARDS) ?? "").undecided,
|
|
291
|
-
},
|
|
292
|
-
}
|
|
293
|
-
return guarded(
|
|
294
|
-
() => walkthrough(b, summary, options.lap ?? "first", carry, fixedAt, afterRiskFix),
|
|
295
|
-
guard,
|
|
296
|
-
)()
|
|
297
|
-
},
|
|
298
|
-
fixRisks: guarded((notes) => {
|
|
299
|
-
riskFixed = true
|
|
300
|
-
return fixRisks(notes, fixAccess, rules)
|
|
301
|
-
}, guard),
|
|
302
|
-
keepGreen: () => healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules }),
|
|
303
|
-
// Kept until the human sees a review, so a re-review after a risk fix still names it.
|
|
304
|
-
awaitReview: (b) => {
|
|
305
|
-
nitFix = undefined
|
|
306
|
-
return awaitReview(b)
|
|
307
|
-
},
|
|
308
|
-
reviewMissing,
|
|
309
|
-
collecting: feedback.collecting,
|
|
310
|
-
triage: triage(severities),
|
|
311
|
-
settles: settlesStandard,
|
|
312
|
-
learn: learnStandards,
|
|
313
|
-
pending: standardsPending,
|
|
314
|
-
answerQuestions: answerReviewQuestions,
|
|
315
|
-
fixNits: async (notes) => {
|
|
316
|
-
await guarded((n: typeof notes) => fixNits(n, fixAccess, rules), guard)(notes)
|
|
317
|
-
nitFix = head()
|
|
318
|
-
},
|
|
319
|
-
close: closeReview,
|
|
320
|
-
captures: {
|
|
321
|
-
edits: t.reviewEditsCapture,
|
|
322
|
-
notes: t.reviewNotesCapture,
|
|
323
|
-
editNotes: (round, edits, answeredAt) =>
|
|
324
|
-
t.reviewEditNotesCapture(
|
|
325
|
-
round,
|
|
326
|
-
edits
|
|
327
|
-
.map((n, i) => ({
|
|
328
|
-
n,
|
|
329
|
-
i,
|
|
330
|
-
s: severities.get(n.id),
|
|
331
|
-
key: captureKey(severities.get(n.id)),
|
|
332
|
-
}))
|
|
333
|
-
.sort((x, y) => x.key - y.key || x.i - y.i)
|
|
334
|
-
.map(({ n, s }) => ({
|
|
335
|
-
id: n.id,
|
|
336
|
-
anchor: n.anchor,
|
|
337
|
-
text: n.forced ? n.text.replace(/^!\s*/, "") : n.text,
|
|
338
|
-
...(s === undefined ? {} : { label: s.reason }),
|
|
339
|
-
})),
|
|
340
|
-
answeredAt,
|
|
341
|
-
),
|
|
342
|
-
},
|
|
343
|
-
refusals: {
|
|
344
|
-
deleted:
|
|
345
|
-
"gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
|
|
346
|
-
unmatched: feedback.unmatched,
|
|
347
|
-
...(feedback.dropped === undefined ? {} : { dropped: feedback.dropped }),
|
|
348
|
-
},
|
|
349
|
-
})
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export interface FeedbackTarget {
|
|
353
|
-
readonly file: string
|
|
354
|
-
readonly collecting: (capture: string, round: kit.CollectRound) => Promise<void>
|
|
355
|
-
/** Refusal when the collecting turn changed something but not `file`. */
|
|
356
|
-
readonly unmatched: string
|
|
357
|
-
readonly dropped?: (requested: kit.Requested) => string
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export interface BuildTailOptions extends TailOptions {
|
|
361
|
-
readonly feedback?: FeedbackTarget | undefined
|
|
362
|
-
/** Write access for the nit and risk fix turns; absent: the bundled `access` export decides. */
|
|
363
|
-
readonly fixAccess?: ScopeAccess | undefined
|
|
364
|
-
/** Which round the reviewer writes; absent: `first`. */
|
|
365
|
-
readonly lap?: t.ReviewRange | undefined
|
|
366
|
-
/** What the quality lap and its finding gate did, for the review's summary; absent: nothing. */
|
|
367
|
-
readonly report?: LensReport | undefined
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** The build tail: fix (when entered red), keep green unless `green`, the quality lap, then human review since `base`. */
|
|
371
|
-
export function buildTail(base: string, options?: BuildTailOptions): Promise<kit.ReviewOutcome>
|
|
372
|
-
/** @deprecated Pass `buildTail(base, { fixFirst, green, ...options })`. */
|
|
373
|
-
export function buildTail(
|
|
374
|
-
fixFirst: boolean,
|
|
375
|
-
base: string,
|
|
376
|
-
green?: boolean,
|
|
377
|
-
options?: Omit<BuildTailOptions, "fixFirst" | "green">,
|
|
378
|
-
): Promise<kit.ReviewOutcome>
|
|
379
|
-
export function buildTail(
|
|
380
|
-
first: boolean | string,
|
|
381
|
-
second?: string | BuildTailOptions,
|
|
382
|
-
...rest: unknown[]
|
|
383
|
-
): Promise<kit.ReviewOutcome> {
|
|
384
|
-
const options = callForm(first, second, rest)
|
|
385
|
-
// Before any step runs, so a bad call never leaves a half-walked flow.
|
|
386
|
-
if (options === undefined) {
|
|
387
|
-
return Promise.reject(
|
|
388
|
-
new Error(
|
|
389
|
-
"buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither",
|
|
390
|
-
),
|
|
391
|
-
)
|
|
392
|
-
}
|
|
393
|
-
const [base, form] = options
|
|
394
|
-
return tail((escalations, report) => humanReview(base, escalations, { ...form, report }), form)
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const isOptionalBoolean = (value: unknown): value is boolean | null | undefined =>
|
|
398
|
-
value == null || typeof value === "boolean"
|
|
399
|
-
|
|
400
|
-
const legacyForm = (
|
|
401
|
-
fixFirst: unknown,
|
|
402
|
-
base: string,
|
|
403
|
-
green: unknown,
|
|
404
|
-
options: unknown,
|
|
405
|
-
): [string, BuildTailOptions] | undefined =>
|
|
406
|
-
isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options)
|
|
407
|
-
? [base, { ...options, fixFirst: fixFirst ?? undefined, green: green ?? undefined }]
|
|
408
|
-
: undefined
|
|
409
|
-
|
|
410
|
-
const currentForm = (
|
|
411
|
-
base: string,
|
|
412
|
-
options: unknown,
|
|
413
|
-
third: unknown,
|
|
414
|
-
fourth: unknown,
|
|
415
|
-
): [string, BuildTailOptions] | undefined =>
|
|
416
|
-
isOptionalObject(options) && third == null && fourth == null ? [base, options ?? {}] : undefined
|
|
417
|
-
|
|
418
|
-
// gtd.config.ts is untyped at runtime, so every argument is checked against the form chosen.
|
|
419
|
-
const callForm = (
|
|
420
|
-
first: unknown,
|
|
421
|
-
second: unknown,
|
|
422
|
-
[third, fourth, ...extra]: readonly unknown[],
|
|
423
|
-
): [string, BuildTailOptions] | undefined => {
|
|
424
|
-
if (extra.some((value) => value != null)) return undefined
|
|
425
|
-
if (typeof second === "string") return legacyForm(first, second, third, fourth)
|
|
426
|
-
if (typeof first === "string") return currentForm(first, second, third, fourth)
|
|
427
|
-
return undefined
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Undo the human's review-round code edits. A path changed since the human
|
|
432
|
-
* left it is not overwritten; requireRevert names it instead.
|
|
433
|
-
*/
|
|
434
|
-
export const reUnwind = async (
|
|
435
|
-
feedback: Extract<kit.ReviewOutcome, { verdict: "feedback" }>,
|
|
436
|
-
): Promise<void> => {
|
|
437
|
-
const { base, restoreFrom, edited } = feedback
|
|
438
|
-
const restore = edited.filter((c) => c.status !== "added").map((c) => c.path)
|
|
439
|
-
const remove = edited.filter((c) => c.status === "added").map((c) => c.path)
|
|
440
|
-
await run("re-unwind", restoreScript(base, { restore, remove }, restoreFrom), {
|
|
441
|
-
label: "Re-unwinding your review edit",
|
|
442
|
-
file: REVIEW,
|
|
443
|
-
base,
|
|
444
|
-
})
|
|
445
|
-
requireRevert(edited, base)
|
|
446
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { skills } from "./skills.js"
|
|
3
|
-
import * as workflow from "../index.js"
|
|
4
|
-
|
|
5
|
-
const { defaults } = workflow
|
|
6
|
-
|
|
7
|
-
const bundled = skills(defaults)
|
|
8
|
-
|
|
9
|
-
// Pins the lists by scope full name. It never runs the real flow, so a
|
|
10
|
-
// scope rename is caught by the e2e features that rest on these names
|
|
11
|
-
// (default-workflow, state-skills, scope-skills, quality-review-lap), not here.
|
|
12
|
-
describe("the bundled workflow's skills export", () => {
|
|
13
|
-
it("declares exactly the bundled scopes under the default qualityReviews", () => {
|
|
14
|
-
expect(Object.keys(bundled).sort()).toEqual(
|
|
15
|
-
[
|
|
16
|
-
"design",
|
|
17
|
-
"build",
|
|
18
|
-
"build.health",
|
|
19
|
-
"build.fix.quality",
|
|
20
|
-
"build.review",
|
|
21
|
-
"build.review.fix.nits",
|
|
22
|
-
"build.review.fix.risks",
|
|
23
|
-
"notes",
|
|
24
|
-
"feedback",
|
|
25
|
-
"refactor",
|
|
26
|
-
"refactor.fix.suite",
|
|
27
|
-
"refactor.health",
|
|
28
|
-
"seam",
|
|
29
|
-
"implement",
|
|
30
|
-
"implement.fix.suite",
|
|
31
|
-
"implement.health",
|
|
32
|
-
"build.quality.correctness",
|
|
33
|
-
"build.quality.owasp-security",
|
|
34
|
-
"build.quality.ponytail-review",
|
|
35
|
-
"build.quality.test-audit",
|
|
36
|
-
"build.quality.conventions",
|
|
37
|
-
"build.quality.spec-challenge",
|
|
38
|
-
"build.quality.criteria-coverage",
|
|
39
|
-
"refactor.build",
|
|
40
|
-
"refactor.build.health",
|
|
41
|
-
"refactor.build.fix.quality",
|
|
42
|
-
"refactor.build.review",
|
|
43
|
-
"refactor.build.review.fix.nits",
|
|
44
|
-
"refactor.build.review.fix.risks",
|
|
45
|
-
"refactor.build.quality.correctness",
|
|
46
|
-
"refactor.build.quality.ponytail-review",
|
|
47
|
-
"refactor.build.quality.conventions",
|
|
48
|
-
].sort(),
|
|
49
|
-
)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it("keys no group scope or non-agent step", () => {
|
|
53
|
-
for (const group of [
|
|
54
|
-
"build.fix",
|
|
55
|
-
"build.review.fix",
|
|
56
|
-
"build.quality",
|
|
57
|
-
"implement.fix",
|
|
58
|
-
"seam.judge",
|
|
59
|
-
"seam.steer",
|
|
60
|
-
"seam.by-hand",
|
|
61
|
-
]) {
|
|
62
|
-
expect(bundled).not.toHaveProperty(group)
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it("pins each scope's list", () => {
|
|
67
|
-
expect(bundled).toMatchObject({
|
|
68
|
-
design: ["spec-driven-development", "planning-and-task-breakdown"],
|
|
69
|
-
build: ["debugging-and-error-recovery"],
|
|
70
|
-
"build.health": ["debugging-and-error-recovery"],
|
|
71
|
-
"build.fix.quality": ["incremental-implementation", "code-simplification"],
|
|
72
|
-
"build.review": ["code-review-and-quality"],
|
|
73
|
-
"build.review.fix.nits": ["incremental-implementation", "code-simplification"],
|
|
74
|
-
"build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
|
|
75
|
-
notes: ["incremental-implementation", "code-simplification"],
|
|
76
|
-
feedback: ["test-driven-development", "incremental-implementation"],
|
|
77
|
-
refactor: ["ponytail"],
|
|
78
|
-
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
79
|
-
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
80
|
-
seam: ["incremental-implementation"],
|
|
81
|
-
implement: ["test-driven-development", "incremental-implementation"],
|
|
82
|
-
"implement.fix.suite": ["debugging-and-error-recovery"],
|
|
83
|
-
"implement.health": ["debugging-and-error-recovery"],
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it("keys one refactor lens scope per refactorQualityReviews entry", () => {
|
|
88
|
-
const custom = skills({ ...defaults, refactorQualityReviews: " my-lens ,, correctness" })
|
|
89
|
-
expect(
|
|
90
|
-
Object.keys(custom)
|
|
91
|
-
.filter((k) => k.startsWith("refactor.build.quality."))
|
|
92
|
-
.sort(),
|
|
93
|
-
).toEqual(["refactor.build.quality.correctness", "refactor.build.quality.my-lens"])
|
|
94
|
-
const none = skills({ ...defaults, refactorQualityReviews: "" })
|
|
95
|
-
expect(Object.keys(none).filter((k) => k.startsWith("refactor.build.quality."))).toEqual([])
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it("loads one lens skill per quality lens: correctness its review skill, conventions, spec-challenge and criteria-coverage none, any other itself", () => {
|
|
99
|
-
expect(bundled["build.quality.correctness"]).toEqual(["code-review-and-quality"])
|
|
100
|
-
expect(bundled["build.quality.conventions"]).toEqual([])
|
|
101
|
-
expect(bundled["build.quality.spec-challenge"]).toEqual([])
|
|
102
|
-
expect(bundled["build.quality.criteria-coverage"]).toEqual([])
|
|
103
|
-
expect(bundled["build.quality.owasp-security"]).toEqual(["owasp-security"])
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it("keys one lens scope per qualityReviews entry", () => {
|
|
107
|
-
const custom = skills({ ...defaults, qualityReviews: " my-lens ,, correctness" })
|
|
108
|
-
expect(
|
|
109
|
-
Object.keys(custom)
|
|
110
|
-
.filter((k) => k.startsWith("build.quality."))
|
|
111
|
-
.sort(),
|
|
112
|
-
).toEqual(["build.quality.correctness", "build.quality.my-lens"])
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it("keys a hinted entry by its lens alone, with the lens's own skills", () => {
|
|
116
|
-
const custom = skills({ ...defaults, qualityReviews: "my-lens=smart, correctness = base" })
|
|
117
|
-
expect(custom["build.quality.my-lens"]).toEqual(["my-lens"])
|
|
118
|
-
expect(custom["build.quality.correctness"]).toEqual(["code-review-and-quality"])
|
|
119
|
-
expect(Object.keys(custom).some((k) => k.includes("="))).toBe(false)
|
|
120
|
-
})
|
|
121
|
-
})
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { builtInLenses } from "./steps.js"
|
|
2
|
-
import { lensesOf, lensScopes } from "../../workflow-kit/index.js"
|
|
3
|
-
|
|
4
|
-
// The bundled workflow's default skills, one list per SCOPE, keyed by the
|
|
5
|
-
// scope's full name — the same name a `config.skills` entry addresses. A
|
|
6
|
-
// nested scope with no entry inherits its parent's; group scopes
|
|
7
|
-
// (`implement.fix`, `build.fix`, `build.review.fix`, `build.quality`, `refactor.fix`) run
|
|
8
|
-
// no turn and carry no key. `build.quality.<lens>` is one key per
|
|
9
|
-
// `qualityReviews` entry, so the export is a function of the vars.
|
|
10
|
-
export const skills = (
|
|
11
|
-
vars: Readonly<Record<string, string>>,
|
|
12
|
-
): Readonly<Record<string, readonly string[]>> => {
|
|
13
|
-
const build: Record<string, readonly string[]> = {
|
|
14
|
-
build: ["debugging-and-error-recovery"],
|
|
15
|
-
"build.health": ["debugging-and-error-recovery"],
|
|
16
|
-
"build.fix.quality": ["incremental-implementation", "code-simplification"],
|
|
17
|
-
"build.review": ["code-review-and-quality"],
|
|
18
|
-
"build.review.fix.nits": ["incremental-implementation", "code-simplification"],
|
|
19
|
-
"build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
|
|
20
|
-
}
|
|
21
|
-
return {
|
|
22
|
-
design: ["spec-driven-development", "planning-and-task-breakdown"],
|
|
23
|
-
// `fix`'s turn over the notes a review handed on.
|
|
24
|
-
notes: ["incremental-implementation", "code-simplification"],
|
|
25
|
-
// `review`'s turn building what a review round collected.
|
|
26
|
-
feedback: ["test-driven-development", "incremental-implementation"],
|
|
27
|
-
// The refactor's own turns load `ponytail`; `refactor.build.*` carry the `build.*` lists.
|
|
28
|
-
...build,
|
|
29
|
-
...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
|
|
30
|
-
refactor: ["ponytail"],
|
|
31
|
-
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
32
|
-
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
33
|
-
seam: ["incremental-implementation"],
|
|
34
|
-
implement: ["test-driven-development", "incremental-implementation"],
|
|
35
|
-
"implement.fix.suite": ["debugging-and-error-recovery"],
|
|
36
|
-
// `health` is shared code reached from both `implement` and `build`.
|
|
37
|
-
"implement.health": ["debugging-and-error-recovery"],
|
|
38
|
-
...lensScopes(
|
|
39
|
-
"build.quality",
|
|
40
|
-
lensesOf(vars.qualityReviews ?? ""),
|
|
41
|
-
(lens) => builtInLenses[lens]?.skills ?? [lens],
|
|
42
|
-
),
|
|
43
|
-
...lensScopes(
|
|
44
|
-
"refactor.build.quality",
|
|
45
|
-
lensesOf(vars.refactorQualityReviews ?? ""),
|
|
46
|
-
(lens) => builtInLenses[lens]?.skills ?? [lens],
|
|
47
|
-
),
|
|
48
|
-
}
|
|
49
|
-
}
|