@pmelab/gtd 58.1.0 → 59.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 (142) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +115 -390
  3. package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
  4. package/dist/gtd.bundle.mjs +76 -59
  5. package/dist/testkit/gherkin.mjs +30 -2
  6. package/dist/testkit.mjs +1 -1
  7. package/package.json +3 -42
  8. package/skills/authoring/SKILL.md +31 -25
  9. package/src/cli/Cli.test.ts +15 -19
  10. package/src/cli/Cli.ts +2 -4
  11. package/src/cli/Program.test.ts +60 -29
  12. package/src/cli/Program.ts +5 -7
  13. package/src/edge/Edge.ts +3 -3
  14. package/src/flags/index.test.ts +0 -13
  15. package/src/flags/index.ts +1 -3
  16. package/src/gherkin/git-control.feature +78 -0
  17. package/src/gherkin/index.ts +35 -1
  18. package/src/install/Install.test.ts +14 -11
  19. package/src/install/Install.ts +38 -15
  20. package/src/install/index.ts +1 -2
  21. package/src/replay/Replay.test.ts +1 -1
  22. package/src/replay/index.ts +1 -1
  23. package/src/step/planStart.ts +6 -1
  24. package/src/testkit/TestRepo.test.ts +118 -2
  25. package/src/testkit/TestRepo.ts +57 -6
  26. package/src/testkit/index.ts +1 -0
  27. package/src/twins/EmittedScriptRecognizer.ts +12 -14
  28. package/src/twins/FlowScripts.ts +10 -9
  29. package/src/twins/InMemRepo.ts +26 -1
  30. package/src/twins/Layers.ts +12 -1
  31. package/src/twins/index.ts +6 -1
  32. package/src/workflow/index.ts +1 -0
  33. package/src/workflow/load.test.ts +138 -113
  34. package/src/workflow/load.ts +1 -1
  35. package/src/workflow/vars.test.ts +30 -3
  36. package/src/workflow/vars.ts +34 -2
  37. package/src/workflow-kit/index.ts +1 -1
  38. package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
  39. package/dist/chunks/review-Bfaror38.mjs +0 -427
  40. package/dist/chunks/tail-Bd7L940p.mjs +0 -355
  41. package/dist/chunks/testkit-D1-auYau.mjs +0 -10362
  42. package/dist/workflows/cleanup.mjs +0 -36
  43. package/dist/workflows/feature.mjs +0 -1018
  44. package/dist/workflows/fix.mjs +0 -41
  45. package/dist/workflows/review.mjs +0 -45
  46. package/dist/workflows/rollout.mjs +0 -48
  47. package/dist/workflows.mjs +0 -40651
  48. package/src/install/ConfigScaffold.ts +0 -22
  49. package/src/workflows/cleanup/cleanup-path.feature +0 -94
  50. package/src/workflows/cleanup/cleanup.feature +0 -315
  51. package/src/workflows/cleanup/cleanup.test.ts +0 -276
  52. package/src/workflows/cleanup/cleanup.ts +0 -52
  53. package/src/workflows/cleanup/index.ts +0 -6
  54. package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
  55. package/src/workflows/feature/acceptance-criteria.feature +0 -389
  56. package/src/workflows/feature/criteria-coverage.feature +0 -102
  57. package/src/workflows/feature/criteria.test.ts +0 -147
  58. package/src/workflows/feature/criteria.ts +0 -267
  59. package/src/workflows/feature/fast-path.feature +0 -93
  60. package/src/workflows/feature/feature.test.ts +0 -338
  61. package/src/workflows/feature/feature.ts +0 -156
  62. package/src/workflows/feature/green.ts +0 -19
  63. package/src/workflows/feature/index.ts +0 -18
  64. package/src/workflows/feature/lens-models.feature +0 -69
  65. package/src/workflows/feature/lens-review.feature +0 -566
  66. package/src/workflows/feature/naive-acceptance.feature +0 -147
  67. package/src/workflows/feature/naive-test.feature +0 -263
  68. package/src/workflows/feature/one-step-land.feature +0 -93
  69. package/src/workflows/feature/planning.ts +0 -41
  70. package/src/workflows/feature/refactor-absorbs.feature +0 -131
  71. package/src/workflows/feature/refactor-first.feature +0 -158
  72. package/src/workflows/feature/refactor-lens-models.feature +0 -79
  73. package/src/workflows/feature/refactor-rebuild.feature +0 -273
  74. package/src/workflows/feature/refactor-review.feature +0 -1528
  75. package/src/workflows/feature/refactor.test.ts +0 -1220
  76. package/src/workflows/feature/refactor.ts +0 -368
  77. package/src/workflows/feature/release-flag.feature +0 -167
  78. package/src/workflows/feature/release-flag.test.ts +0 -49
  79. package/src/workflows/feature/release-flag.ts +0 -46
  80. package/src/workflows/feature/review-gate.feature +0 -241
  81. package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
  82. package/src/workflows/feature/risk-tier.feature +0 -351
  83. package/src/workflows/feature/seam-rejection.feature +0 -146
  84. package/src/workflows/feature/seam.feature +0 -297
  85. package/src/workflows/feature/seam.test.ts +0 -527
  86. package/src/workflows/feature/seam.ts +0 -426
  87. package/src/workflows/feature/standards.feature +0 -354
  88. package/src/workflows/feature/steps.test.ts +0 -65
  89. package/src/workflows/feature/steps.ts +0 -45
  90. package/src/workflows/feature/tier-pin.feature +0 -66
  91. package/src/workflows/feature/tier.test.ts +0 -166
  92. package/src/workflows/feature/tier.ts +0 -56
  93. package/src/workflows/fix/fix-lens-origin.feature +0 -114
  94. package/src/workflows/fix/fix-quality.feature +0 -212
  95. package/src/workflows/fix/fix.feature +0 -79
  96. package/src/workflows/fix/fix.test.ts +0 -74
  97. package/src/workflows/fix/fix.ts +0 -52
  98. package/src/workflows/fix/index.ts +0 -6
  99. package/src/workflows/index.ts +0 -47
  100. package/src/workflows/review/index.ts +0 -6
  101. package/src/workflows/review/review-door.feature +0 -199
  102. package/src/workflows/review/review.test.ts +0 -27
  103. package/src/workflows/review/review.ts +0 -60
  104. package/src/workflows/rollout/index.ts +0 -6
  105. package/src/workflows/rollout/rollout.feature +0 -510
  106. package/src/workflows/rollout/rollout.test.ts +0 -263
  107. package/src/workflows/rollout/rollout.ts +0 -82
  108. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
  109. package/src/workflows/shared/access.test.ts +0 -62
  110. package/src/workflows/shared/access.ts +0 -24
  111. package/src/workflows/shared/config.fixture.ts +0 -12
  112. package/src/workflows/shared/fences.ts +0 -24
  113. package/src/workflows/shared/health.test.ts +0 -148
  114. package/src/workflows/shared/health.ts +0 -191
  115. package/src/workflows/shared/learn.test.ts +0 -140
  116. package/src/workflows/shared/learn.ts +0 -201
  117. package/src/workflows/shared/lenses.test.ts +0 -39
  118. package/src/workflows/shared/lenses.ts +0 -294
  119. package/src/workflows/shared/options.test.ts +0 -157
  120. package/src/workflows/shared/options.ts +0 -92
  121. package/src/workflows/shared/progress.feature +0 -124
  122. package/src/workflows/shared/prose.ts +0 -263
  123. package/src/workflows/shared/recheck.feature +0 -60
  124. package/src/workflows/shared/registry.test.ts +0 -345
  125. package/src/workflows/shared/registry.ts +0 -183
  126. package/src/workflows/shared/reserved.ts +0 -11
  127. package/src/workflows/shared/review.test.ts +0 -1062
  128. package/src/workflows/shared/review.ts +0 -446
  129. package/src/workflows/shared/skills.test.ts +0 -121
  130. package/src/workflows/shared/skills.ts +0 -49
  131. package/src/workflows/shared/standards.test.ts +0 -90
  132. package/src/workflows/shared/standards.ts +0 -156
  133. package/src/workflows/shared/steps.test.ts +0 -219
  134. package/src/workflows/shared/steps.ts +0 -201
  135. package/src/workflows/shared/tail.test.ts +0 -454
  136. package/src/workflows/shared/tail.ts +0 -264
  137. package/src/workflows/shared/text.fixture.ts +0 -128
  138. package/src/workflows/shared/text.test.ts +0 -1430
  139. package/src/workflows/shared/text.ts +0 -1700
  140. package/src/workflows/shared/threads.test.ts +0 -129
  141. package/src/workflows/shared/vars.ts +0 -47
  142. package/src/workflows/shared/workflows.steps.ts +0 -54
@@ -1,427 +0,0 @@
1
- import { answered, changesSince, head, judge, numeric, read, refuse, requireRevert, restoreScript, run, vars, writeScript } from "../flows.mjs";
2
- import { S as reviewSections, _ as REVIEW_SECTIONS, v as reviewChunks, x as reviewRisks } from "./steering-CCNvngSH.mjs";
3
- import { review } from "../workflow-kit.mjs";
4
- import { $ as nitFixBlock, A as reviewing, At as parseStandards, C as fixCheck, Dt as conflictTitle, E as fixRisks, Et as conflictFor, Mt as standardKey, N as buildReviewTriageMessage, O as reviewMissing, Ot as isConflictTitle, S as collecting, Tt as conflictBody, b as awaitReview, bt as withSummary, ct as reviewEditNotesCapture, d as healthy, ft as reviewNotesCapture, jt as renderStandards, kt as oneLine, lt as reviewEditsCapture, u as guarded, v as REVIEW, w as fixNits, wt as STANDARDS, y as answerReviewQuestions } from "./reserved-lMYCaapY.mjs";
5
- import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-Bd7L940p.mjs";
6
- //#region src/workflows/shared/learn.ts
7
- const SUMMARY_MAX = 200;
8
- const summary = (body) => {
9
- const text = oneLine(body);
10
- return text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX - 1)}…` : text;
11
- };
12
- /** The standard a won't-fix or an own answer would make; a fix states none. */
13
- const candidateOf = (d) => {
14
- const anchor = oneLine(d.anchor);
15
- if (d.choice === "wontfix") {
16
- const why = summary(d.body);
17
- return [{
18
- decision: d,
19
- text: `Won't fix: ${anchor}${why === "" ? "" : ` — ${why}`}`
20
- }];
21
- }
22
- if (d.choice === "answer") return [{
23
- decision: d,
24
- text: `${oneLine(d.text)} (re: ${anchor})`
25
- }];
26
- return [];
27
- };
28
- const without = (list, drop) => {
29
- const keys = new Set(drop.map(standardKey));
30
- return list.filter((s) => !keys.has(standardKey(s)));
31
- };
32
- const adding = (list, text) => list.some((s) => standardKey(s) === standardKey(text)) ? [...list] : [...list, text];
33
- /**
34
- * Keep the new one, keep the old one, or replace both with the human's own
35
- * wording. Another open conflict stops naming a replaced standard: the human
36
- * never judged it against the replacement, so keeping it must not drop that.
37
- */
38
- const resolve = (standards, d) => {
39
- const c = conflictFor(standards, d.anchor);
40
- if (c === void 0) return standards;
41
- const others = standards.undecided.filter((u) => u !== c);
42
- if (d.choice === "wontfix") return {
43
- decided: standards.decided,
44
- undecided: others
45
- };
46
- const kept = d.choice === "fix" ? c.text : oneLine(d.text);
47
- const undecided = others.map((u) => ({
48
- text: u.text,
49
- olds: without(u.olds, c.olds)
50
- }));
51
- return {
52
- decided: adding(without(standards.decided, c.olds), kept),
53
- undecided
54
- };
55
- };
56
- const contradicts = (i, id, other) => ({
57
- id: `standard-${i}.contradicts-${id}`,
58
- primitive: "noul",
59
- instructions: `Does the "Standard:" line of evidence "standard-${i}" contradict ${other}?`,
60
- criteria: "yes: following one breaks the other. no: both can be followed — they are compatible or unrelated."
61
- });
62
- const questionsFor = (i, anchor, decided) => [
63
- {
64
- id: `standard-${i}`,
65
- primitive: "noul",
66
- instructions: `Is the human's answer to review note "${anchor}" (evidence "standard-${i}") a reusable standard for this codebase — a rule later changes and reviews should follow — rather than a call about this one note? Its "Standard:" line is the wording gtd would keep.`,
67
- criteria: "yes: it states a rule or preference that applies beyond this note. no: it is specific to this note, a one-off call, or unclear."
68
- },
69
- ...decided.map((_, k) => contradicts(i, `${k + 1}`, `kept standard ${k + 1} (evidence "kept-${k + 1}")`)),
70
- ...Array.from({ length: i - 1 }, (_, j) => contradicts(i, `new-${j + 1}`, `the "Standard:" line of evidence "standard-${j + 1}"`))
71
- ];
72
- const evidenceOf = (c) => [
73
- `Note: ${c.decision.anchor}`,
74
- `Reviewer's text: ${c.decision.body}`,
75
- `Human's answer: ${c.decision.choice === "wontfix" ? "won't fix" : c.decision.text}`,
76
- `Standard: ${c.text}`
77
- ].join("\n");
78
- /**
79
- * One judge rest over every candidate. Only a confident `yes` on uncut
80
- * evidence makes a standard; a contradiction the judge does not confidently
81
- * rule out goes to the human instead of overwriting what they decided before.
82
- * A cut kept standard leaves the round unlearned: raise `judgeBudgetBytes`.
83
- */
84
- const judged = async (candidates, decided, minP) => {
85
- const { answers, truncated } = await judge("review.standards.judge", {
86
- questions: candidates.flatMap((c, i) => questionsFor(i + 1, c.decision.anchor, decided)),
87
- evidence: Object.fromEntries([...candidates.map((c, i) => [`standard-${i + 1}`, evidenceOf(c)]), ...decided.map((s, k) => [`kept-${k + 1}`, s])]),
88
- message: "Judging whether your answers are standards to keep: run `gtd judge run`, or land without a verdict, which keeps none.",
89
- label: "Learning your standards"
90
- });
91
- const ruledOut = (i, id) => answered(answers[`standard-${i}.contradicts-${id}`], "no", minP);
92
- const added = [];
93
- const raised = [];
94
- candidates.forEach((c, n) => {
95
- const i = n + 1;
96
- if (truncated.includes(`standard-${i}`)) return;
97
- if (decided.some((_, k) => truncated.includes(`kept-${k + 1}`))) return;
98
- if (!answered(answers[`standard-${i}`], "yes", minP)) return;
99
- const olds = [...decided.filter((_, k) => !ruledOut(i, `${k + 1}`)), ...added.filter((a) => !ruledOut(i, `new-${a.i}`)).map((a) => a.text)];
100
- if (olds.length === 0) added.push({
101
- i,
102
- text: c.text
103
- });
104
- else raised.push({
105
- text: c.text,
106
- olds
107
- });
108
- });
109
- return {
110
- added: added.map((a) => a.text),
111
- raised
112
- };
113
- };
114
- /**
115
- * Settles every answer to a standard conflict, then learns standards from
116
- * the won't-fix ticks and own answers: kept in `.gtd/STANDARDS.md`, a
117
- * contradicting one as a new conflict note in `.gtd/REVIEW.md`.
118
- */
119
- const learnStandards = async (decisions) => {
120
- const before = parseStandards(read(".gtd/STANDARDS.md") ?? "");
121
- const rest = decisions.filter((d) => !isConflictTitle(d.anchor));
122
- const settled = decisions.filter((d) => isConflictTitle(d.anchor)).reduce(resolve, before);
123
- const known = new Set([...settled.decided, ...settled.undecided.map((c) => c.text)].map(standardKey));
124
- const candidates = rest.flatMap(candidateOf).filter((c) => {
125
- const key = standardKey(c.text);
126
- if (known.has(key)) return false;
127
- known.add(key);
128
- return true;
129
- });
130
- const minP = numeric(vars.standardMinP, Infinity);
131
- const { added, raised } = candidates.length === 0 || !Number.isFinite(minP) ? {
132
- added: [],
133
- raised: []
134
- } : await judged(candidates, settled.decided, minP);
135
- const after = {
136
- decided: [...settled.decided, ...added],
137
- undecided: [...settled.undecided, ...raised]
138
- };
139
- if (renderStandards(after) !== renderStandards(before)) await run("review.standards.recording", writeScript(STANDARDS, renderStandards(after)), { label: "Recording your standards" });
140
- if (raised.length > 0) {
141
- const chunks = raised.map((c) => `## ${conflictTitle(c)}\n\n${conflictBody(c)}`);
142
- await run("review.standards.raising", writeScript(REVIEW, `${(read(".gtd/REVIEW.md") ?? "").trimEnd()}\n\n${chunks.join("\n\n")}\n`), {
143
- label: "Asking about a contradicting standard",
144
- file: REVIEW
145
- });
146
- }
147
- };
148
- const settlesStandard = (d) => isConflictTitle(d.anchor);
149
- const standardsPending = () => parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided.length > 0;
150
- const standardsAddedSince = (base) => {
151
- const change = changesSince(base).get(STANDARDS);
152
- if (change === void 0) return [];
153
- const was = new Set(parseStandards(change.before ?? "").decided);
154
- return parseStandards(change.after ?? "").decided.filter((s) => !was.has(s));
155
- };
156
- //#endregion
157
- //#region src/workflows/shared/review.ts
158
- const VERDICTS = [
159
- "edit",
160
- "question",
161
- "nit",
162
- "praise"
163
- ];
164
- /** One four-way choice per note the human added to `.gtd/REVIEW.md`. */
165
- const verdictQuestion = (note) => ({
166
- id: note.id,
167
- primitive: "choice",
168
- 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.`,
169
- criteria: "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."
170
- });
171
- const TIER_ORDER = {
172
- critical: 0,
173
- should: 1,
174
- could: 2
175
- };
176
- /** Capture order: critical, should, could; within a tier one-way doors, then new concepts. */
177
- const captureKey = (s) => s === void 0 ? 2 : TIER_ORDER[s.tier] * 3 + s.rank;
178
- const SEVERITY_ANSWERS = {
179
- security: ["yes", "no"],
180
- reversibility: ["one-way", "two-way"],
181
- blastRadius: [
182
- "local",
183
- "module",
184
- "contract"
185
- ],
186
- newConcept: ["yes", "no"]
187
- };
188
- /** Anything unanswered, unrecognised, cut or below `minP` is `critical`, never `could`. */
189
- const severityOf = (note, judgment, minP) => {
190
- const ans = (q) => judgment.answers[`${note.id}.${q}`];
191
- const a = (q) => ans(q)?.answer;
192
- const rank = () => a("reversibility") === "one-way" ? 0 : a("newConcept") === "yes" ? 1 : 2;
193
- if (note.forced) return {
194
- tier: "critical",
195
- reason: "critical: forced by `!`",
196
- rank: 2
197
- };
198
- if (judgment.truncated.includes(note.id) || Object.entries(SEVERITY_ANSWERS).some(([q, allowed]) => !allowed.includes(a(q) ?? "") || !((ans(q)?.p ?? NaN) >= minP))) return {
199
- tier: "critical",
200
- reason: "critical: judge unsure",
201
- rank: 2
202
- };
203
- const critical = [...a("security") === "yes" ? ["security"] : [], ...a("reversibility") === "one-way" ? ["one-way door"] : []];
204
- if (critical.length > 0) return {
205
- tier: "critical",
206
- reason: `critical: ${critical.join(", ")}`,
207
- rank: rank(),
208
- ...a("security") === "yes" ? { security: true } : {}
209
- };
210
- const should = [...a("blastRadius") === "contract" ? ["contract"] : [], ...a("newConcept") === "yes" ? ["new concept"] : []];
211
- if (should.length > 0) return {
212
- tier: "should",
213
- reason: `should: ${should.join(", ")}`,
214
- rank: rank()
215
- };
216
- return {
217
- tier: "could",
218
- reason: "could",
219
- rank: 2
220
- };
221
- };
222
- /** `asks` names the change in full, `change` refers back to it. */
223
- const severityQuestions = (id, asks, change) => [
224
- {
225
- id: `${id}.security`,
226
- primitive: "noul",
227
- instructions: `Does ${asks} touch security — authentication, authorisation, secrets, untrusted input or data exposure?`,
228
- criteria: "yes: it changes or could weaken a security property. no: it does not."
229
- },
230
- {
231
- id: `${id}.reversibility`,
232
- primitive: "choice",
233
- instructions: `Once ${change} ships, can it be undone without breaking anyone who already depends on it?`,
234
- criteria: "one-way: a published API, file format, persisted data, protocol or other contract that cannot be taken back. two-way: can be reverted freely."
235
- },
236
- {
237
- id: `${id}.blastRadius`,
238
- primitive: "choice",
239
- instructions: `How far does ${change} reach?`,
240
- criteria: "local: one function or file. module: a whole module or several files. contract: a public interface other code or users depend on."
241
- },
242
- {
243
- id: `${id}.newConcept`,
244
- primitive: "noul",
245
- 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.`,
246
- criteria: "yes: a genuinely new concept. no: reuses or extends an existing one."
247
- }
248
- ];
249
- /**
250
- * One judge rest: a verdict per unforced note (a forced note is asked
251
- * nothing) plus four severity answers. Dismissing a note is the risky
252
- * direction, so only a confident non-`edit` verdict on uncut evidence counts;
253
- * a cut, unanswered or below-floor note is an `edit`. Severity answers on a
254
- * non-`edit` note are ignored; on an `edit` they order the collecting turn.
255
- */
256
- const triage = (severities) => async (notes) => {
257
- const asked = notes.filter((n) => !n.forced);
258
- const judgment = asked.length === 0 ? {
259
- answers: {},
260
- truncated: []
261
- } : await judge("review.triage", {
262
- questions: asked.flatMap((n) => [verdictQuestion(n), ...severityQuestions(n.id, `the change the human asks for in note "${n.id}" (on "${n.anchor}")`, `the change in note "${n.id}"`)]),
263
- evidence: Object.fromEntries(asked.map((n) => [n.id, `Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`])),
264
- message: buildReviewTriageMessage(),
265
- label: "Judging each note"
266
- });
267
- const { answers, truncated } = judgment;
268
- const minP = numeric(vars.reviewNoteActionable, Infinity);
269
- const severityMinP = numeric(vars.reviewSeverityMinP, Infinity);
270
- severities.clear();
271
- return new Map(notes.map((n) => {
272
- const verdict = n.forced ? void 0 : VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP));
273
- if (verdict !== void 0 && !truncated.includes(n.id)) return [n.id, verdict];
274
- severities.set(n.id, severityOf(n, judgment, severityMinP));
275
- return [n.id, "edit"];
276
- }));
277
- };
278
- const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
279
- /** Refuses a review with a chunk pointing at no code; a range with no code change has nothing to point at. */
280
- const requirePointers = (review, codeChanged) => {
281
- const chunks = reviewChunks(review);
282
- const problems = [
283
- ...chunks.filter((c) => c.pointers === 0 && codeChanged).map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — points at no code`),
284
- ...chunks.filter((c) => isConflictTitle(c.title)).map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — gtd writes conflicts; leave it out`),
285
- ...chunks.filter((c, i) => chunks.findIndex((d) => d.title === c.title) !== i).map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — a second chunk with this title`),
286
- ...reviewSections(review).map((s) => ` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} — write \`## \` chunks only`)
287
- ];
288
- if (problems.length === 0) return;
289
- refuse(`gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code:\n${problems.join("\n")}`);
290
- };
291
- /**
292
- * A reviewer walks the human through everything since `base`, every chunk
293
- * pointing at its code; gtd then appends the summary chunks.
294
- */
295
- const walkthrough = async (base, summary, lap, carry, nitFix, afterRiskFix) => {
296
- await reviewing(base, carry, lap, nitFixBlock(nitFix));
297
- const written = read(REVIEW);
298
- if (written === void 0) return;
299
- requirePointers(written, changesSince(base).some((c) => isCode(c.path)));
300
- if (!afterRiskFix && reviewRisks(written).length > 0) return;
301
- const full = withSummary(written, summary);
302
- if (full !== written) await run("review.carrying", writeScript(REVIEW, full), {
303
- label: "Adding the quality lap's summary",
304
- file: REVIEW,
305
- base
306
- });
307
- };
308
- /**
309
- * A reviewer's walkthrough of everything since `base` reaches a human, who
310
- * may comment: each note is judged — edits end the review as `feedback` for
311
- * the caller to build, questions are answered in place, nits fixed in one
312
- * batch, praise dropped.
313
- */
314
- const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
315
- const { guard, guardRests, fixAccess, rules } = options;
316
- const feedback = options.feedback ?? {
317
- file: ".gtd/REQUIREMENTS.md",
318
- collecting,
319
- unmatched: "gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing"
320
- };
321
- const severities = /* @__PURE__ */ new Map();
322
- let nitFix;
323
- let riskFixed = false;
324
- return review(base, {
325
- file: REVIEW,
326
- feedback: feedback.file,
327
- isCode,
328
- reviewing: (b, carry) => {
329
- const fixedAt = nitFix;
330
- const afterRiskFix = riskFixed;
331
- riskFixed = false;
332
- const summary = {
333
- report: options.report ?? NO_FINDINGS,
334
- standards: {
335
- added: standardsAddedSince(b),
336
- undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
337
- }
338
- };
339
- return guarded(() => walkthrough(b, summary, options.lap ?? "first", carry, fixedAt, afterRiskFix), guard)();
340
- },
341
- fixRisks: guarded((notes) => {
342
- riskFixed = true;
343
- return fixRisks(notes, fixAccess, rules);
344
- }, guard),
345
- keepGreen: () => healthy(() => fixCheck(rules), {
346
- escalations,
347
- guard,
348
- guardRests,
349
- rules
350
- }),
351
- awaitReview: (b) => {
352
- nitFix = void 0;
353
- return awaitReview(b);
354
- },
355
- reviewMissing,
356
- collecting: feedback.collecting,
357
- triage: triage(severities),
358
- settles: settlesStandard,
359
- learn: learnStandards,
360
- pending: standardsPending,
361
- answerQuestions: answerReviewQuestions,
362
- fixNits: async (notes) => {
363
- await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
364
- nitFix = head();
365
- },
366
- close: closeReview,
367
- captures: {
368
- edits: reviewEditsCapture,
369
- notes: reviewNotesCapture,
370
- editNotes: (round, edits, answeredAt) => reviewEditNotesCapture(round, edits.map((n, i) => ({
371
- n,
372
- i,
373
- s: severities.get(n.id),
374
- key: captureKey(severities.get(n.id))
375
- })).sort((x, y) => x.key - y.key || x.i - y.i).map(({ n, s }) => ({
376
- id: n.id,
377
- anchor: n.anchor,
378
- text: n.forced ? n.text.replace(/^!\s*/, "") : n.text,
379
- ...s === void 0 ? {} : { label: s.reason }
380
- })), answeredAt)
381
- },
382
- refusals: {
383
- deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
384
- unmatched: feedback.unmatched,
385
- ...feedback.dropped === void 0 ? {} : { dropped: feedback.dropped }
386
- }
387
- });
388
- };
389
- function buildTail(first, second, ...rest) {
390
- const options = callForm(first, second, rest);
391
- if (options === void 0) return Promise.reject(/* @__PURE__ */ new Error("buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither"));
392
- const [base, form] = options;
393
- return tail((escalations, report) => humanReview(base, escalations, {
394
- ...form,
395
- report
396
- }), form);
397
- }
398
- const isOptionalBoolean = (value) => value == null || typeof value === "boolean";
399
- const legacyForm = (fixFirst, base, green, options) => isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options) ? [base, {
400
- ...options,
401
- fixFirst: fixFirst ?? void 0,
402
- green: green ?? void 0
403
- }] : void 0;
404
- const currentForm = (base, options, third, fourth) => isOptionalObject(options) && third == null && fourth == null ? [base, options ?? {}] : void 0;
405
- const callForm = (first, second, [third, fourth, ...extra]) => {
406
- if (extra.some((value) => value != null)) return void 0;
407
- if (typeof second === "string") return legacyForm(first, second, third, fourth);
408
- if (typeof first === "string") return currentForm(first, second, third, fourth);
409
- };
410
- /**
411
- * Undo the human's review-round code edits. A path changed since the human
412
- * left it is not overwritten; requireRevert names it instead.
413
- */
414
- const reUnwind = async (feedback) => {
415
- const { base, restoreFrom, edited } = feedback;
416
- await run("re-unwind", restoreScript(base, {
417
- restore: edited.filter((c) => c.status !== "added").map((c) => c.path),
418
- remove: edited.filter((c) => c.status === "added").map((c) => c.path)
419
- }, restoreFrom), {
420
- label: "Re-unwinding your review edit",
421
- file: REVIEW,
422
- base
423
- });
424
- requireRevert(edited, base);
425
- };
426
- //#endregion
427
- export { isCode as n, reUnwind as r, buildTail as t };