@pmelab/gtd 43.0.0 → 45.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 +33 -13
- package/dist/chunks/{reserved-COEMFEpy.mjs → reserved-ty1mtlnS.mjs} +155 -3
- package/dist/chunks/review-BmoH-9im.mjs +408 -0
- package/dist/chunks/{tail-C_xn8U7U.mjs → tail-CM3sVoAb.mjs} +3 -3
- package/dist/workflow-kit.mjs +22 -10
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +6 -278
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +27 -7
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +379 -66
- package/package.json +1 -1
- package/src/steering/FORMATS.md +4 -4
- package/src/workflow-kit/review.test.ts +80 -2
- package/src/workflow-kit/review.ts +45 -11
- package/src/workflows/feature/coverage.ts +1 -1
- package/src/workflows/feature/feature.test.ts +8 -0
- package/src/workflows/feature/feature.ts +2 -2
- package/src/workflows/feature/index.ts +1 -1
- package/src/workflows/feature/refactor.ts +1 -1
- package/src/workflows/feature/review-answers.feature +12 -3
- package/src/workflows/feature/seam.ts +1 -1
- package/src/workflows/feature/standards.feature +286 -0
- package/src/workflows/feature/steps.test.ts +2 -64
- package/src/workflows/feature/steps.ts +5 -41
- package/src/workflows/fix/fix.feature +20 -21
- package/src/workflows/index.ts +1 -1
- package/src/workflows/review/review-door.feature +158 -0
- package/src/workflows/review/review.test.ts +1 -73
- package/src/workflows/review/review.ts +34 -6
- package/src/workflows/shared/learn.test.ts +154 -0
- package/src/workflows/shared/learn.ts +206 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/{feature → shared}/review.test.ts +4 -8
- package/src/workflows/{feature → shared}/review.ts +23 -10
- package/src/workflows/shared/skills.test.ts +2 -0
- package/src/workflows/shared/skills.ts +2 -0
- package/src/workflows/shared/standards.test.ts +90 -0
- package/src/workflows/shared/standards.ts +156 -0
- package/src/workflows/shared/steps.test.ts +64 -2
- package/src/workflows/shared/steps.ts +40 -1
- package/src/workflows/shared/tail.ts +2 -2
- package/src/workflows/shared/text.test.ts +58 -1
- package/src/workflows/shared/text.ts +39 -1
- package/src/workflows/shared/vars.ts +2 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { answered, changesSince, head, judge, numeric, read, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
|
|
2
|
+
import { removedAssertions, review } from "../workflow-kit.mjs";
|
|
3
|
+
import { A as reviewMissing, At as conflictTitle, Dt as STANDARDS, E as fixNits, F as buildReviewTriageMessage, Ft as standardKey, Mt as oneLine, Nt as parseStandards, O as fixRisks, Ot as conflictBody, Pt as renderStandards, S as awaitReview, T as fixCheck, d as guarded, dt as reviewEditNotesCapture, f as healthy, ft as reviewEditsCapture, h as QUALITY, ht as reviewNotesCapture, jt as isConflictTitle, kt as conflictFor, w as collecting, x as answerReviewQuestions, y as REVIEW, z as composedReview } from "./reserved-ty1mtlnS.mjs";
|
|
4
|
+
import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-CM3sVoAb.mjs";
|
|
5
|
+
//#region src/workflows/shared/learn.ts
|
|
6
|
+
const SUMMARY_MAX = 200;
|
|
7
|
+
const BANNER = /^_(?:The fixer left this unresolved\.|A fix turn removed this while renaming another finding — check whether it was fixed\.|No detail given\.)_\s*/;
|
|
8
|
+
const summary = (body) => {
|
|
9
|
+
const text = oneLine(body).replace(BANNER, "");
|
|
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
|
+
};
|
|
209
|
+
const should = [...a("blastRadius") === "contract" ? ["contract"] : [], ...a("newConcept") === "yes" ? ["new concept"] : []];
|
|
210
|
+
if (should.length > 0) return {
|
|
211
|
+
tier: "should",
|
|
212
|
+
reason: `should: ${should.join(", ")}`,
|
|
213
|
+
rank: rank()
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
tier: "could",
|
|
217
|
+
reason: "could",
|
|
218
|
+
rank: 2
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
const severityQuestions = (note) => [
|
|
222
|
+
{
|
|
223
|
+
id: `${note.id}.security`,
|
|
224
|
+
primitive: "noul",
|
|
225
|
+
instructions: `Does the change the human asks for in note "${note.id}" (on "${note.anchor}") touch security — authentication, authorisation, secrets, untrusted input or data exposure?`,
|
|
226
|
+
criteria: "yes: it changes or could weaken a security property. no: it does not."
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: `${note.id}.reversibility`,
|
|
230
|
+
primitive: "choice",
|
|
231
|
+
instructions: `Once the change in note "${note.id}" ships, can it be undone without breaking anyone who already depends on it?`,
|
|
232
|
+
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."
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: `${note.id}.blastRadius`,
|
|
236
|
+
primitive: "choice",
|
|
237
|
+
instructions: `How far does the change in note "${note.id}" reach?`,
|
|
238
|
+
criteria: "local: one function or file. module: a whole module or several files. contract: a public interface other code or users depend on."
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: `${note.id}.newConcept`,
|
|
242
|
+
primitive: "noul",
|
|
243
|
+
instructions: `Does the change in note "${note.id}" 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.`,
|
|
244
|
+
criteria: "yes: a genuinely new concept. no: reuses or extends an existing one."
|
|
245
|
+
}
|
|
246
|
+
];
|
|
247
|
+
/**
|
|
248
|
+
* One judge rest: a verdict per unforced note (a forced note is asked
|
|
249
|
+
* nothing) plus four severity answers. Dismissing a note is the risky
|
|
250
|
+
* direction, so only a confident non-`edit` verdict on uncut evidence counts;
|
|
251
|
+
* a cut, unanswered or below-floor note is an `edit`. Severity answers on a
|
|
252
|
+
* non-`edit` note are ignored; on an `edit` they order the collecting turn.
|
|
253
|
+
*/
|
|
254
|
+
const triage = (severities) => async (notes) => {
|
|
255
|
+
const asked = notes.filter((n) => !n.forced);
|
|
256
|
+
const judgment = asked.length === 0 ? {
|
|
257
|
+
answers: {},
|
|
258
|
+
truncated: []
|
|
259
|
+
} : await judge("review.triage", {
|
|
260
|
+
questions: asked.flatMap((n) => [verdictQuestion(n), ...severityQuestions(n)]),
|
|
261
|
+
evidence: Object.fromEntries(asked.map((n) => [n.id, `Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`])),
|
|
262
|
+
message: buildReviewTriageMessage(),
|
|
263
|
+
label: "Judging each note"
|
|
264
|
+
});
|
|
265
|
+
const { answers, truncated } = judgment;
|
|
266
|
+
const minP = numeric(vars.reviewNoteActionable, Infinity);
|
|
267
|
+
const severityMinP = numeric(vars.reviewSeverityMinP, Infinity);
|
|
268
|
+
severities.clear();
|
|
269
|
+
return new Map(notes.map((n) => {
|
|
270
|
+
const verdict = n.forced ? void 0 : VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP));
|
|
271
|
+
if (verdict !== void 0 && !truncated.includes(n.id)) return [n.id, verdict];
|
|
272
|
+
severities.set(n.id, severityOf(n, judgment, severityMinP));
|
|
273
|
+
return [n.id, "edit"];
|
|
274
|
+
}));
|
|
275
|
+
};
|
|
276
|
+
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
277
|
+
/**
|
|
278
|
+
* gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
|
|
279
|
+
* turn — and removes what the fixer left in `.gtd/QUALITY.md`. A re-review
|
|
280
|
+
* after fixed nits restores the doc as committed at `carry`, answers kept;
|
|
281
|
+
* a fresh one leaves out the notes the human already answered.
|
|
282
|
+
*/
|
|
283
|
+
const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new Set()) => run("review.composing", carry === void 0 ? writeScript(REVIEW, composedReview(base, {
|
|
284
|
+
...report,
|
|
285
|
+
weakened: removedAssertions(changesSince(base)),
|
|
286
|
+
nitFix,
|
|
287
|
+
answered,
|
|
288
|
+
standards: {
|
|
289
|
+
added: standardsAddedSince(base),
|
|
290
|
+
undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
|
|
291
|
+
}
|
|
292
|
+
}), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
|
|
293
|
+
label: "Composing the review",
|
|
294
|
+
file: REVIEW,
|
|
295
|
+
base
|
|
296
|
+
});
|
|
297
|
+
/**
|
|
298
|
+
* The quality lap's notes reach a human over everything since `base`, who
|
|
299
|
+
* answers each one — fix as suggested, won't fix, or their own answer — and
|
|
300
|
+
* may comment. A fix joins the nit batch, a won't-fix is dropped, and an own
|
|
301
|
+
* answer or comment is judged: edits end the review as `feedback` for the
|
|
302
|
+
* caller to build, questions are answered in place, nits fixed in one batch,
|
|
303
|
+
* praise dropped.
|
|
304
|
+
*/
|
|
305
|
+
const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
306
|
+
const { guard, guardRests, fixAccess, rules } = options;
|
|
307
|
+
const feedback = options.feedback ?? {
|
|
308
|
+
file: ".gtd/REQUIREMENTS.md",
|
|
309
|
+
collecting,
|
|
310
|
+
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"
|
|
311
|
+
};
|
|
312
|
+
const severities = /* @__PURE__ */ new Map();
|
|
313
|
+
let nitFix;
|
|
314
|
+
const answered = /* @__PURE__ */ new Set();
|
|
315
|
+
return review(base, {
|
|
316
|
+
file: REVIEW,
|
|
317
|
+
feedback: feedback.file,
|
|
318
|
+
isCode,
|
|
319
|
+
reviewing: (b, carry) => {
|
|
320
|
+
const fixedAt = nitFix;
|
|
321
|
+
nitFix = void 0;
|
|
322
|
+
return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt, answered);
|
|
323
|
+
},
|
|
324
|
+
fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
|
|
325
|
+
keepGreen: () => healthy(() => fixCheck(rules), {
|
|
326
|
+
escalations,
|
|
327
|
+
guard,
|
|
328
|
+
guardRests,
|
|
329
|
+
rules
|
|
330
|
+
}),
|
|
331
|
+
awaitReview: (b) => awaitReview(b),
|
|
332
|
+
reviewMissing,
|
|
333
|
+
collecting: feedback.collecting,
|
|
334
|
+
triage: triage(severities),
|
|
335
|
+
decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
|
|
336
|
+
settles: settlesStandard,
|
|
337
|
+
learn: learnStandards,
|
|
338
|
+
pending: standardsPending,
|
|
339
|
+
answerQuestions: answerReviewQuestions,
|
|
340
|
+
fixNits: async (notes) => {
|
|
341
|
+
await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
|
|
342
|
+
nitFix = head();
|
|
343
|
+
},
|
|
344
|
+
close: closeReview,
|
|
345
|
+
captures: {
|
|
346
|
+
edits: reviewEditsCapture,
|
|
347
|
+
notes: reviewNotesCapture,
|
|
348
|
+
editNotes: (round, edits, answeredAt) => reviewEditNotesCapture(round, edits.map((n, i) => ({
|
|
349
|
+
n,
|
|
350
|
+
i,
|
|
351
|
+
s: severities.get(n.id),
|
|
352
|
+
key: captureKey(severities.get(n.id))
|
|
353
|
+
})).sort((x, y) => x.key - y.key || x.i - y.i).map(({ n, s }) => ({
|
|
354
|
+
id: n.id,
|
|
355
|
+
anchor: n.anchor,
|
|
356
|
+
text: n.forced ? n.text.replace(/^!\s*/, "") : n.text,
|
|
357
|
+
...s === void 0 ? {} : { label: s.reason }
|
|
358
|
+
})), answeredAt)
|
|
359
|
+
},
|
|
360
|
+
refusals: {
|
|
361
|
+
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
|
|
362
|
+
unmatched: feedback.unmatched,
|
|
363
|
+
...feedback.dropped === void 0 ? {} : { dropped: feedback.dropped }
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
function buildTail(first, second, ...rest) {
|
|
368
|
+
const options = callForm(first, second, rest);
|
|
369
|
+
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"));
|
|
370
|
+
const [base, form] = options;
|
|
371
|
+
return tail((escalations, report) => humanReview(base, escalations, {
|
|
372
|
+
...form,
|
|
373
|
+
report
|
|
374
|
+
}), {
|
|
375
|
+
...form,
|
|
376
|
+
triage: true
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const isOptionalBoolean = (value) => value == null || typeof value === "boolean";
|
|
380
|
+
const legacyForm = (fixFirst, base, green, options) => isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options) ? [base, {
|
|
381
|
+
...options,
|
|
382
|
+
fixFirst: fixFirst ?? void 0,
|
|
383
|
+
green: green ?? void 0
|
|
384
|
+
}] : void 0;
|
|
385
|
+
const currentForm = (base, options, third, fourth) => isOptionalObject(options) && third == null && fourth == null ? [base, options ?? {}] : void 0;
|
|
386
|
+
const callForm = (first, second, [third, fourth, ...extra]) => {
|
|
387
|
+
if (extra.some((value) => value != null)) return void 0;
|
|
388
|
+
if (typeof second === "string") return legacyForm(first, second, third, fourth);
|
|
389
|
+
if (typeof first === "string") return currentForm(first, second, third, fourth);
|
|
390
|
+
};
|
|
391
|
+
/**
|
|
392
|
+
* Undo the human's review-round code edits. A path changed since the human
|
|
393
|
+
* left it is not overwritten; requireRevert names it instead.
|
|
394
|
+
*/
|
|
395
|
+
const reUnwind = async (feedback) => {
|
|
396
|
+
const { base, restoreFrom, edited } = feedback;
|
|
397
|
+
await run("re-unwind", restoreScript(base, {
|
|
398
|
+
restore: edited.filter((c) => c.status !== "added").map((c) => c.path),
|
|
399
|
+
remove: edited.filter((c) => c.status === "added").map((c) => c.path)
|
|
400
|
+
}, restoreFrom), {
|
|
401
|
+
label: "Re-unwinding your review edit",
|
|
402
|
+
file: REVIEW,
|
|
403
|
+
base
|
|
404
|
+
});
|
|
405
|
+
requireRevert(edited, base);
|
|
406
|
+
};
|
|
407
|
+
//#endregion
|
|
408
|
+
export { reUnwind as i, humanReview as n, isCode as r, buildTail as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { answered, head, judge, mark, numeric, read, refuse, removeScript, run, scope, sectionBodies, showScript, vars, writeScript } from "../flows.mjs";
|
|
2
2
|
import { fixFindings, lensEntriesOf, lensOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { A as reviewMissing, D as fixQuality, M as reviewing, O as fixRisks, S as awaitReview, T as fixCheck, Y as handOffAwaitReviewMessage, d as guarded, f as healthy, h as QUALITY, it as qualityTriageMessage, j as reviewQuality, l as escalation, rt as qualityFindingsFile, y as REVIEW } from "./reserved-ty1mtlnS.mjs";
|
|
4
4
|
//#region src/workflows/shared/lenses.ts
|
|
5
5
|
const NO_FINDINGS = {
|
|
6
6
|
notes: [],
|
|
@@ -217,7 +217,7 @@ const runTail = (finish, options) => scope("build", async () => {
|
|
|
217
217
|
return finish(escalations, NO_FINDINGS);
|
|
218
218
|
});
|
|
219
219
|
/**
|
|
220
|
-
* The
|
|
220
|
+
* The fix workflow's human review since `base`: a sign-off closes
|
|
221
221
|
* `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
|
|
222
222
|
* `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
|
|
223
223
|
*/
|
|
@@ -237,7 +237,7 @@ const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
|
|
|
237
237
|
unparsed: "gtd land: review-doc: your edit to .gtd/REVIEW.md contains no note fix can read — restore the deleted chunk, or write the note as a line under its entry.",
|
|
238
238
|
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
|
|
239
239
|
});
|
|
240
|
-
/** The tail the
|
|
240
|
+
/** The tail the fix workflow runs: ends at the hand-off review since `base`. */
|
|
241
241
|
const handOffTail = (fixFirst, base, guard) => tail((escalations) => handOffReview(base, escalations, guard), {
|
|
242
242
|
fixFirst,
|
|
243
243
|
guard
|
package/dist/workflow-kit.mjs
CHANGED
|
@@ -233,14 +233,20 @@ const record = (p, decisions) => {
|
|
|
233
233
|
p.decided?.(decisions);
|
|
234
234
|
for (const d of decisions) if (d.choice === "wontfix") mark("wontfix", oneLine(d.anchor));
|
|
235
235
|
};
|
|
236
|
+
const routed = (p, answers) => answers.filter((d) => p.settles?.(d) !== true);
|
|
237
|
+
/** Record the routed answers on the gate's commit, then hand every answer to `learn`. */
|
|
238
|
+
const recordAndLearn = async (p, decisions, answers) => {
|
|
239
|
+
record(p, decisions);
|
|
240
|
+
if (answers.length > 0 && p.learn !== void 0) await p.learn(answers);
|
|
241
|
+
};
|
|
236
242
|
/**
|
|
237
243
|
* Judge every note, then run each verdict's route: answers, then nit fixes,
|
|
238
244
|
* then the edits' lap. Of the human's `decisions`, a fix joins the nit batch
|
|
239
245
|
* unjudged, an own answer is judged like a note, and a won't-fix is dropped
|
|
240
246
|
* with a `Gtd-Mark` on the gate's commit.
|
|
241
247
|
*/
|
|
242
|
-
const routeNotes = async (p, notes, r, decisions = []) => {
|
|
243
|
-
|
|
248
|
+
const routeNotes = async (p, notes, r, decisions = [], answers = decisions) => {
|
|
249
|
+
await recordAndLearn(p, decisions, answers);
|
|
244
250
|
const of = (c) => decisions.filter((d) => d.choice === c);
|
|
245
251
|
const fixes = of("fix").map((d, i) => decisionNote(d, `fix-${i + 1}`, d.body));
|
|
246
252
|
const judged = [...notes, ...of("answer").map((d, i) => decisionNote(d, `answer-${i + 1}`, d.text))];
|
|
@@ -294,9 +300,8 @@ const settledAfter = (p, collectedAt) => ({
|
|
|
294
300
|
folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
|
|
295
301
|
settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
|
|
296
302
|
});
|
|
297
|
-
const finishNotes = async (p,
|
|
298
|
-
|
|
299
|
-
if (notes.length + decisions.length > 0) return routeNotes(p, notes, ctx, decisions);
|
|
303
|
+
const finishNotes = async (p, notes, answers, reopened, ctx) => {
|
|
304
|
+
if (notes.length + answers.length > 0) return routeNotes(p, notes, ctx, routed(p, answers), answers);
|
|
300
305
|
if (reopened) return ctx.unfolded();
|
|
301
306
|
await ctx.close();
|
|
302
307
|
return ctx.outcome(await collect(p, p.captures.notes(ctx.round), "notes"));
|
|
@@ -311,6 +316,7 @@ const decisionsSince = (p, since) => {
|
|
|
311
316
|
const after = read(p.file) ?? "";
|
|
312
317
|
return reviewDecisions(since.get(p.file)?.before ?? after, after);
|
|
313
318
|
};
|
|
319
|
+
const notesSince = (p, since) => reviewNotes(since.get(p.file)?.before ?? "", read(p.file) ?? "");
|
|
314
320
|
const finish = async (p, reviewed, collectedAt) => {
|
|
315
321
|
const round = head();
|
|
316
322
|
const since = changesSince(reviewed);
|
|
@@ -331,23 +337,29 @@ const finish = async (p, reviewed, collectedAt) => {
|
|
|
331
337
|
settled,
|
|
332
338
|
folded
|
|
333
339
|
});
|
|
334
|
-
const
|
|
335
|
-
if (settled && !reopened &&
|
|
340
|
+
const answers = decisionsSince(p, since);
|
|
341
|
+
if (settled && !reopened && answers.length === 0 || edited.length === 0 && !noted) return unfolded();
|
|
342
|
+
const notes = notesSince(p, since);
|
|
336
343
|
if (edited.length > 0) {
|
|
337
|
-
|
|
344
|
+
await recordAndLearn(p, routed(p, answers), answers);
|
|
338
345
|
await close();
|
|
339
346
|
return outcome(await collect(p, p.captures.edits(round, reopened ? reviewed : void 0), "edits", {
|
|
340
347
|
notes: [],
|
|
341
348
|
paths: edited.map((c) => c.path)
|
|
342
349
|
}));
|
|
343
350
|
}
|
|
344
|
-
return finishNotes(p,
|
|
351
|
+
return finishNotes(p, notes, answers, reopened, {
|
|
345
352
|
round,
|
|
346
353
|
close,
|
|
347
354
|
outcome,
|
|
348
355
|
unfolded
|
|
349
356
|
});
|
|
350
357
|
};
|
|
358
|
+
/** A sign-off while something waits on the human writes the review again instead. */
|
|
359
|
+
const unlessPending = (p, result) => "verdict" in result && result.verdict === "signoff" && p.pending?.() === true ? {
|
|
360
|
+
next: "rereview",
|
|
361
|
+
carry: void 0
|
|
362
|
+
} : result;
|
|
351
363
|
/** Write the review; if it marks risks, fix them, keep green, and write it again — once, so the re-review's own risks reach the human unfixed. */
|
|
352
364
|
const reviewOnce = async (p, base, carry) => {
|
|
353
365
|
await p.reviewing(base, carry);
|
|
@@ -373,7 +385,7 @@ const review = async (base, p) => {
|
|
|
373
385
|
for (;;) {
|
|
374
386
|
collectedAt = await converse(p, base, reviewed, collectedAt);
|
|
375
387
|
if (collectedAt === "missing") break;
|
|
376
|
-
const result = await finish(p, reviewed, collectedAt);
|
|
388
|
+
const result = unlessPending(p, await finish(p, reviewed, collectedAt));
|
|
377
389
|
if (!("next" in result)) return result;
|
|
378
390
|
if (result.next === "rereview") {
|
|
379
391
|
carry = result.carry;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { Bt as requireFlag, C as coder, Lt as flagReferences, Tt as envDefaults, i as skills, n as summary, o as flagShipped, r as access, t as steering, wt as defaults, zt as registryEntry } from "../chunks/reserved-ty1mtlnS.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CM3sVoAb.mjs";
|
|
4
4
|
//#region src/workflows/cleanup/cleanup.ts
|
|
5
5
|
const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
|
|
6
6
|
const cleanupFlow = async () => {
|