@pmelab/gtd 49.0.0 → 50.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 +31 -26
- package/dist/chunks/{reserved-CYZ1bSBy.mjs → reserved-CdOzKFVX.mjs} +59 -2
- package/dist/chunks/{review-BewyJflS.mjs → review-BChXEl2Y.mjs} +84 -20
- package/dist/chunks/{steering-BXwjmocB.mjs → steering-DtUVUz9l.mjs} +44 -14
- package/dist/chunks/{tail-C1Oi2YEy.mjs → tail-DRM2eC1z.mjs} +1 -1
- package/dist/gtd.bundle.mjs +22 -5
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +1 -1
- package/dist/web/main.js +27 -3
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +3 -3
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +181 -30
- package/package.json +1 -1
- package/src/steering/index.ts +1 -0
- package/src/steering/review.test.ts +60 -2
- package/src/steering/review.ts +66 -9
- package/src/workflows/feature/lens-review.feature +16 -4
- package/src/workflows/feature/review-answers.feature +8 -1
- package/src/workflows/feature/review-gate.feature +20 -3
- package/src/workflows/feature/reviewer-walkthrough.feature +14 -3
- package/src/workflows/feature/severity-budget.feature +111 -0
- package/src/workflows/feature/severity-sorted-review.feature +197 -0
- package/src/workflows/feature/slice-findings.feature +14 -2
- package/src/workflows/feature/standards.feature +4 -0
- package/src/workflows/shared/review.ts +124 -18
- package/src/workflows/shared/text.test.ts +90 -0
- package/src/workflows/shared/text.ts +77 -1
package/README.md
CHANGED
|
@@ -538,32 +538,37 @@ noted in steps 3 and 4 below.
|
|
|
538
538
|
commit) and what was **dropped as noise**. A review with a chunk pointing at
|
|
539
539
|
no code, or a note left out, is refused (`review-pointers`). A pointer note
|
|
540
540
|
the reviewer opens with `Risk:` is fixed before you see the review
|
|
541
|
-
(`build.review.fix.risks.fixing`), then the review is written again.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
541
|
+
(`build.review.fix.risks.fixing`), then the review is written again. **What
|
|
542
|
+
matters most comes first:** a judge (`build.review.ranking`) rates every
|
|
543
|
+
chunk on the four severity questions below, and the review lists `critical`
|
|
544
|
+
chunks first (security, then one-way doors), then `should`, then `could`,
|
|
545
|
+
each led by its tier and reason (`_critical: security, one-way door_`) — the
|
|
546
|
+
phone shows it on the chunk's card. A chunk the judge is unsure of, or whose
|
|
547
|
+
evidence the budget cut, is `critical`. On `refactor-first` the refactor
|
|
548
|
+
slice gets a review of its own first, composed by gtd from its lap's notes,
|
|
549
|
+
before the feature is built; see [the naive pass](#the-naive-pass). **Every
|
|
550
|
+
note needs exactly one tick before `gtd land` accepts the review**
|
|
551
|
+
(`answer-completeness`; an open `- H:` thread waiting on the agent is the one
|
|
552
|
+
exception). Fix as suggested joins the nit batch below, unjudged; won't fix
|
|
553
|
+
drops the note and records it as `Gtd-Mark: wontfix=<title>` on the gate's
|
|
554
|
+
commit; your own answer is judged like a note. A note you answered is left
|
|
555
|
+
out of the fresh review after a nit fix. Land with every note answered and
|
|
556
|
+
nothing else changed to approve, or write what is wrong — disagreeing with a
|
|
557
|
+
fix or a drop means writing a note of your own. Approving ends the process
|
|
558
|
+
(at the refactor review, it moves on to the feature); feedback is judged note
|
|
559
|
+
by note, each as `edit`, `question`, `nit` or `praise`. An `edit` sends the
|
|
560
|
+
process back to step 2 for re-derived requirements — the built criteria keep
|
|
561
|
+
their IDs (reworded in place when their behaviour changes) and new behaviour
|
|
562
|
+
gets new ones — then the implementation is amended to meet them, with no new
|
|
563
|
+
naive pass or seam judge, every criterion's coverage is judged again, the
|
|
564
|
+
quality lap runs, and you review again. At the refactor review an `edit`
|
|
565
|
+
re-derives the refactor instead. A `question` is answered inline under your
|
|
566
|
+
note and the process stops at the review again — no new plan. A `nit` is
|
|
567
|
+
fixed in one batch, the suite must go green (a red one gets fix turns first),
|
|
568
|
+
then a fresh review of the change stops at the review again. `praise` is
|
|
569
|
+
dropped; a round of only praise signs off. When a round mixes them, questions
|
|
570
|
+
are answered and nits fixed first, then the edits are planned — risk: the
|
|
571
|
+
planning lap may redo nit fixes it touches.
|
|
567
572
|
|
|
568
573
|
Each `edit` note also gets a severity tier from four judge questions
|
|
569
574
|
(security, reversibility, blast radius, new concept). Every tier is
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, displayPath, env, head, human, judge, mark, numeric, quote, read, refuse, scope, skillsFor, start, vars } from "../flows.mjs";
|
|
2
|
-
import { d as ANSWER_FIX, f as ANSWER_LEAD, m as reviewChunks, p as ANSWER_WONT_FIX,
|
|
2
|
+
import { b as FREE_TEXT_PLACEHOLDER, d as ANSWER_FIX, f as ANSWER_LEAD, m as reviewChunks, p as ANSWER_WONT_FIX, v as severityLine } from "./steering-DtUVUz9l.mjs";
|
|
3
3
|
import { gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
|
|
4
4
|
import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct, n as Boolean$, s as Number$ } from "./Schema-C6fLlQzx.mjs";
|
|
5
5
|
//#region src/workflows/shared/registry.ts
|
|
@@ -641,6 +641,56 @@ const withWalkthrough = (review, report) => {
|
|
|
641
641
|
const body = lines.join("\n").trimEnd();
|
|
642
642
|
return summary.length === 0 ? `${body}\n` : `${body}\n\n${summary.join("\n\n")}\n`;
|
|
643
643
|
};
|
|
644
|
+
const chunkBlocks = (review) => {
|
|
645
|
+
const lines = review.split("\n");
|
|
646
|
+
const chunks = reviewChunks(review);
|
|
647
|
+
return {
|
|
648
|
+
head: lines.slice(0, chunks[0]?.line ?? lines.length),
|
|
649
|
+
blocks: chunks.map((c, i) => ({
|
|
650
|
+
title: c.title,
|
|
651
|
+
lines: lines.slice(c.line, chunks[i + 1]?.line ?? lines.length)
|
|
652
|
+
}))
|
|
653
|
+
};
|
|
654
|
+
};
|
|
655
|
+
const isBlank = (line) => line !== void 0 && line.trim() === "";
|
|
656
|
+
/** \`review\` with every chunk's severity line removed, so a re-review is ranked afresh. */
|
|
657
|
+
const unranked = (review) => {
|
|
658
|
+
const drop = new Set(reviewChunks(review).flatMap((c) => c.severityLine ?? []));
|
|
659
|
+
if (drop.size === 0) return review;
|
|
660
|
+
const lines = review.split("\n");
|
|
661
|
+
return lines.filter((_, i) => !drop.has(i) && !(drop.has(i - 1) && isBlank(lines[i]))).join("\n");
|
|
662
|
+
};
|
|
663
|
+
const chunkTexts = (review) => new Map(chunkBlocks(review).blocks.map((b) => [b.title, b.lines.join("\n").trim()]));
|
|
664
|
+
const trimBlankLines = (lines) => {
|
|
665
|
+
let start = 0;
|
|
666
|
+
let end = lines.length;
|
|
667
|
+
while (start < end && isBlank(lines[start])) start++;
|
|
668
|
+
while (end > start && isBlank(lines[end - 1])) end--;
|
|
669
|
+
return lines.slice(start, end);
|
|
670
|
+
};
|
|
671
|
+
/**
|
|
672
|
+
* \`review\` with the \`ranked\` chunks first, in their order, each led by its
|
|
673
|
+
* severity line; every other chunk follows in place.
|
|
674
|
+
*/
|
|
675
|
+
const rankedReview = (review, ranked) => {
|
|
676
|
+
const { head, blocks } = chunkBlocks(review);
|
|
677
|
+
const place = new Map(ranked.map((r, k) => [r.title, k]));
|
|
678
|
+
const labels = new Map(ranked.map((r) => [r.title, r.label]));
|
|
679
|
+
const order = (i) => place.get(blocks[i].title) ?? ranked.length;
|
|
680
|
+
const chunk = (i) => {
|
|
681
|
+
const { title, lines } = blocks[i];
|
|
682
|
+
const label = labels.get(title);
|
|
683
|
+
const heading = /^ {0,3}-+\s*$/.test(lines[1] ?? "") ? 2 : 1;
|
|
684
|
+
const body = trimBlankLines(lines.slice(heading)).join("\n");
|
|
685
|
+
return [
|
|
686
|
+
lines.slice(0, heading).join("\n"),
|
|
687
|
+
...label === void 0 ? [] : [severityLine(label)],
|
|
688
|
+
...body === "" ? [] : [body]
|
|
689
|
+
].join("\n\n");
|
|
690
|
+
};
|
|
691
|
+
const sorted = blocks.map((_, i) => i).sort((x, y) => order(x) - order(y) || x - y);
|
|
692
|
+
return `${[head.join("\n").trim(), ...sorted.map(chunk)].filter((p) => p !== "").join("\n\n")}\n`;
|
|
693
|
+
};
|
|
644
694
|
/**
|
|
645
695
|
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
|
|
646
696
|
* answer boxes, then the fixed and noise lists. Chunk titles are made unique —
|
|
@@ -1195,6 +1245,13 @@ const wrapItem = (item) => {
|
|
|
1195
1245
|
} else line = line === "" ? word : `${line} ${word}`;
|
|
1196
1246
|
return [...out, line];
|
|
1197
1247
|
};
|
|
1248
|
+
const reviewRankingMessage = () => `Ranking each chunk of the review before the human reads it. Run
|
|
1249
|
+
\`gtd judge answer\` and pipe, per chunk \`C<n>\`: \`C<n>.security\`
|
|
1250
|
+
(yes/no), \`C<n>.reversibility\` (one-way/two-way),
|
|
1251
|
+
\`C<n>.blastRadius\` (local/module/contract) and \`C<n>.newConcept\`
|
|
1252
|
+
(yes/no) — or land without a verdict, which lists every chunk as
|
|
1253
|
+
critical.
|
|
1254
|
+
`;
|
|
1198
1255
|
const buildReviewTriageMessage = () => `Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
|
|
1199
1256
|
footnote, or prose under a chunk) with one verdict: \`edit\` (a change
|
|
1200
1257
|
request), \`question\`, \`nit\` (a small fix needing no re-plan) or
|
|
@@ -2074,4 +2131,4 @@ const steering = {
|
|
|
2074
2131
|
[REVIEW]: "review"
|
|
2075
2132
|
};
|
|
2076
2133
|
//#endregion
|
|
2077
|
-
export {
|
|
2134
|
+
export { naiveApplyFailure as $, reviewQuality as A, defaults as At, coverageMessage as B, renderStandards as Bt, collecting as C, uncoveredMessage as Ct, fixRisks as D, walkthroughBlock as Dt, fixQuality as E, unwindFailure as Et, builderSystem as F, conflictFor as Ft, designSystem as G, registryEntry as Gt, coveringPrompt as H, flagMentions as Ht, chunkTexts as I, conflictTitle as It, fixReviewNotesPrompt as J, designTriagePrompt as K, requireFlag as Kt, clearCriteriaMessage as L, isConflictTitle as Lt, agentWithSkills as M, testFileGlobs as Mt, buildReviewCollectingPrompt as N, STANDARDS as Nt, planner as O, withLapFindings as Ot, buildReviewTriageMessage as P, conflictBody as Pt, implementFixPrompt as Q, clearCriteriaQuestion as R, oneLine as Rt, coder as S, titledNotes as St, fixNits as T, unwindFailedMessage as Tt, criterionLine as U, flagReferences as Ut, coverageQuestion as V, standardKey as Vt, designGateAnswerMessage as W, flipProblems as Wt, idleMessage as X, handOffAwaitReviewMessage as Y, implementBuildingPrompt as Z, REQUIREMENTS as _, reviewerSystem as _t, flagModeOn as a, rankedReview as at, answerReviewQuestions as b, slicingPrompt as bt, baseline as c, refactorFixPrompt as ct, healthy as d, reviewEditNotesCapture as dt, naivePassPrompt as et, FEEDBACK as f, reviewEditsCapture as ft, REFACTOR_NOTE as g, reviewRankingMessage as gt, REFACTOR as h, reviewNotesCapture as ht, skills as i, qualityTriageMessage as it, reviewing as j, envDefaults as jt, reviewMissing as k, withWalkthrough as kt, gate as l, refactorNotePrompt as lt, QUALITY as m, reviewGateBlockedMessage as mt, summary as n, oversizedMessage as nt, flagShipped as o, refactorBuildingPrompt as ot, HANDOFF as p, reviewFeedbackFixPrompt as pt, finisherSystem as q, access as r, qualityFindingsFile as rt, shipped as s, refactorDroppedRefusal as st, steering as t, normaliseCriteriaScript as tt, guarded as u, refactorRules as ut, REVIEW as v, seamByHandMessage as vt, fixCheck as w, unranked as wt, awaitReview as x, startGateBlockedMessage as xt, SLICES as y, seamSteerMessage as yt, composedReview as z, parseStandards as zt };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { answered, changesSince, head, judge, numeric, read, refuse, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
|
|
2
|
-
import { _ as reviewRisks, m as reviewChunks } from "./steering-
|
|
2
|
+
import { _ as reviewRisks, m as reviewChunks } from "./steering-DtUVUz9l.mjs";
|
|
3
3
|
import { removedAssertions, review } from "../workflow-kit.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-
|
|
4
|
+
import { Bt as renderStandards, C as collecting, D as fixRisks, Dt as walkthroughBlock, Ft as conflictFor, I as chunkTexts, It as conflictTitle, Lt as isConflictTitle, Nt as STANDARDS, P as buildReviewTriageMessage, Pt as conflictBody, Rt as oneLine, St as titledNotes, T as fixNits, Vt as standardKey, at as rankedReview, b as answerReviewQuestions, d as healthy, dt as reviewEditNotesCapture, ft as reviewEditsCapture, gt as reviewRankingMessage, ht as reviewNotesCapture, j as reviewing, k as reviewMissing, kt as withWalkthrough, u as guarded, v as REVIEW, w as fixCheck, wt as unranked, x as awaitReview, z as composedReview, zt as parseStandards } from "./reserved-CdOzKFVX.mjs";
|
|
5
|
+
import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-DRM2eC1z.mjs";
|
|
6
6
|
//#region src/workflows/shared/learn.ts
|
|
7
7
|
const SUMMARY_MAX = 200;
|
|
8
8
|
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*/;
|
|
@@ -205,7 +205,8 @@ const severityOf = (note, judgment, minP) => {
|
|
|
205
205
|
if (critical.length > 0) return {
|
|
206
206
|
tier: "critical",
|
|
207
207
|
reason: `critical: ${critical.join(", ")}`,
|
|
208
|
-
rank: rank()
|
|
208
|
+
rank: rank(),
|
|
209
|
+
...a("security") === "yes" ? { security: true } : {}
|
|
209
210
|
};
|
|
210
211
|
const should = [...a("blastRadius") === "contract" ? ["contract"] : [], ...a("newConcept") === "yes" ? ["new concept"] : []];
|
|
211
212
|
if (should.length > 0) return {
|
|
@@ -219,29 +220,30 @@ const severityOf = (note, judgment, minP) => {
|
|
|
219
220
|
rank: 2
|
|
220
221
|
};
|
|
221
222
|
};
|
|
222
|
-
|
|
223
|
+
/** `asks` names the change in full, `change` refers back to it. */
|
|
224
|
+
const severityQuestions = (id, asks, change) => [
|
|
223
225
|
{
|
|
224
|
-
id: `${
|
|
226
|
+
id: `${id}.security`,
|
|
225
227
|
primitive: "noul",
|
|
226
|
-
instructions: `Does
|
|
228
|
+
instructions: `Does ${asks} touch security — authentication, authorisation, secrets, untrusted input or data exposure?`,
|
|
227
229
|
criteria: "yes: it changes or could weaken a security property. no: it does not."
|
|
228
230
|
},
|
|
229
231
|
{
|
|
230
|
-
id: `${
|
|
232
|
+
id: `${id}.reversibility`,
|
|
231
233
|
primitive: "choice",
|
|
232
|
-
instructions: `Once
|
|
234
|
+
instructions: `Once ${change} ships, can it be undone without breaking anyone who already depends on it?`,
|
|
233
235
|
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."
|
|
234
236
|
},
|
|
235
237
|
{
|
|
236
|
-
id: `${
|
|
238
|
+
id: `${id}.blastRadius`,
|
|
237
239
|
primitive: "choice",
|
|
238
|
-
instructions: `How far does
|
|
240
|
+
instructions: `How far does ${change} reach?`,
|
|
239
241
|
criteria: "local: one function or file. module: a whole module or several files. contract: a public interface other code or users depend on."
|
|
240
242
|
},
|
|
241
243
|
{
|
|
242
|
-
id: `${
|
|
244
|
+
id: `${id}.newConcept`,
|
|
243
245
|
primitive: "noul",
|
|
244
|
-
instructions: `Does
|
|
246
|
+
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.`,
|
|
245
247
|
criteria: "yes: a genuinely new concept. no: reuses or extends an existing one."
|
|
246
248
|
}
|
|
247
249
|
];
|
|
@@ -258,7 +260,7 @@ const triage = (severities) => async (notes) => {
|
|
|
258
260
|
answers: {},
|
|
259
261
|
truncated: []
|
|
260
262
|
} : await judge("review.triage", {
|
|
261
|
-
questions: asked.flatMap((n) => [verdictQuestion(n), ...severityQuestions(n)]),
|
|
263
|
+
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}"`)]),
|
|
262
264
|
evidence: Object.fromEntries(asked.map((n) => [n.id, `Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`])),
|
|
263
265
|
message: buildReviewTriageMessage(),
|
|
264
266
|
label: "Judging each note"
|
|
@@ -274,6 +276,67 @@ const triage = (severities) => async (notes) => {
|
|
|
274
276
|
return [n.id, "edit"];
|
|
275
277
|
}));
|
|
276
278
|
};
|
|
279
|
+
/** Review order: security, one-way doors, the judge unsure, then should, then could. */
|
|
280
|
+
const chunkKey = (s) => s.tier === "could" ? 4 : s.tier === "should" ? 3 : s.security === true ? 0 : s.rank === 0 ? 1 : 2;
|
|
281
|
+
/** A chunk's text, then the code each of its pointers covers: a bare path the whole file. */
|
|
282
|
+
const chunkEvidence = (text, files) => [text, ...files.map(({ path, line, rangeEnd }) => {
|
|
283
|
+
const where = `${path}${line === void 0 ? "" : `#${line}${rangeEnd === void 0 ? "" : `-${rangeEnd}`}`}`;
|
|
284
|
+
const lines = read(path.replace(/^\.\//, ""))?.split("\n");
|
|
285
|
+
if (lines === void 0) return `${where}: no longer exists`;
|
|
286
|
+
return `${where}:\n${(line === void 0 ? lines : lines.slice(line - 1, rangeEnd ?? line)).join("\n")}`;
|
|
287
|
+
})].join("\n\n");
|
|
288
|
+
const bytes = (text) => new TextEncoder().encode(text).length;
|
|
289
|
+
/** Fills each batch while the budget's even share still holds its widest evidence, so nothing is cut that fits alone. */
|
|
290
|
+
const batched = (items, budget) => {
|
|
291
|
+
const batches = [];
|
|
292
|
+
let batch = [];
|
|
293
|
+
let widest = 0;
|
|
294
|
+
for (const item of items) {
|
|
295
|
+
const size = bytes(item.evidence);
|
|
296
|
+
if (batch.length > 0 && Math.floor(budget / (batch.length + 1)) < Math.max(widest, size)) {
|
|
297
|
+
batches.push(batch);
|
|
298
|
+
batch = [];
|
|
299
|
+
widest = 0;
|
|
300
|
+
}
|
|
301
|
+
batch.push(item);
|
|
302
|
+
widest = Math.max(widest, size);
|
|
303
|
+
}
|
|
304
|
+
return batch.length === 0 ? batches : [...batches, batch];
|
|
305
|
+
};
|
|
306
|
+
/**
|
|
307
|
+
* Rates every chunk the reviewer wrote — a walkthrough chunk or a carried
|
|
308
|
+
* finding — and sorts \`full\` by it, each chunk led by its tier and reason.
|
|
309
|
+
* gtd's summary chunks are not rated and stay last.
|
|
310
|
+
*/
|
|
311
|
+
const ranked = async (review, full) => {
|
|
312
|
+
const texts = chunkTexts(full);
|
|
313
|
+
const chunks = reviewChunks(review).map((c, i) => ({
|
|
314
|
+
id: `C${i + 1}`,
|
|
315
|
+
title: c.title,
|
|
316
|
+
evidence: chunkEvidence(texts.get(c.title) ?? "", c.files)
|
|
317
|
+
}));
|
|
318
|
+
if (chunks.length === 0) return full;
|
|
319
|
+
const minP = numeric(vars.reviewSeverityMinP, Infinity);
|
|
320
|
+
const batches = batched(chunks, numeric(vars.judgeBudgetBytes, 32768));
|
|
321
|
+
const severities = /* @__PURE__ */ new Map();
|
|
322
|
+
for (const [i, batch] of batches.entries()) {
|
|
323
|
+
const judgment = await judge("review.ranking", {
|
|
324
|
+
questions: batch.flatMap((c) => severityQuestions(c.id, `the change review chunk "${c.id}" ("${c.title}") walks through`, `the change in review chunk "${c.id}"`)),
|
|
325
|
+
evidence: Object.fromEntries(batch.map((c) => [c.id, c.evidence])),
|
|
326
|
+
message: reviewRankingMessage(),
|
|
327
|
+
label: batches.length === 1 ? "Ranking the review" : `Ranking the review (${i + 1}/${batches.length})`
|
|
328
|
+
});
|
|
329
|
+
for (const c of batch) severities.set(c.id, severityOf(c, judgment, minP));
|
|
330
|
+
}
|
|
331
|
+
return rankedReview(full, chunks.map((c, i) => ({
|
|
332
|
+
c,
|
|
333
|
+
i,
|
|
334
|
+
s: severities.get(c.id)
|
|
335
|
+
})).sort((x, y) => chunkKey(x.s) - chunkKey(y.s) || x.i - y.i).map(({ c, s }) => ({
|
|
336
|
+
title: c.title,
|
|
337
|
+
label: s.reason
|
|
338
|
+
})));
|
|
339
|
+
};
|
|
277
340
|
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
278
341
|
const composedOf = (base, report, nitFix, answered) => ({
|
|
279
342
|
...report,
|
|
@@ -325,12 +388,13 @@ const requirePointers = (review, notes, codeChanged) => {
|
|
|
325
388
|
const walkthrough = async (base, report, lap, carry) => {
|
|
326
389
|
const notes = titledNotes(report);
|
|
327
390
|
await reviewing(base, carry, lap, walkthroughBlock(notes, report.nitFix));
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
391
|
+
const written = read(REVIEW);
|
|
392
|
+
if (written === void 0) return;
|
|
393
|
+
const review = unranked(written);
|
|
394
|
+
requirePointers(written, notes, changesSince(base).some((c) => isCode(c.path)));
|
|
395
|
+
const full = await ranked(review, withWalkthrough(review, report));
|
|
396
|
+
if (full !== written) await run("review.carrying", writeScript(REVIEW, full), {
|
|
397
|
+
label: "Ordering the review and adding the quality lap's findings",
|
|
334
398
|
file: REVIEW,
|
|
335
399
|
base
|
|
336
400
|
});
|
|
@@ -15089,6 +15089,9 @@ const qaDescriptor = {
|
|
|
15089
15089
|
const ANSWER_LEAD = "Tick exactly one:";
|
|
15090
15090
|
const ANSWER_FIX = "fix as suggested";
|
|
15091
15091
|
const ANSWER_WONT_FIX = "won't fix";
|
|
15092
|
+
/** A chunk's leading `_critical: security_` paragraph: the tier gtd ranked it with, and why. */
|
|
15093
|
+
const SEVERITY_LINE_RE = /^_((?:critical|should|could)(?::[^_\n]*)?)_$/;
|
|
15094
|
+
const severityLine = (label) => `_${label}_`;
|
|
15092
15095
|
/**
|
|
15093
15096
|
* The canonical sample. Every detail here is load-bearing for the formatter
|
|
15094
15097
|
* round-trip: both footnote bodies exceed 80 characters so a reflow actually
|
|
@@ -15379,12 +15382,26 @@ const answerListOf = (content, body) => {
|
|
|
15379
15382
|
})
|
|
15380
15383
|
};
|
|
15381
15384
|
};
|
|
15385
|
+
/** A leading severity line is the chunk's tier, never its prose. */
|
|
15386
|
+
const splitSeverity = (content, run) => {
|
|
15387
|
+
const first = run[0];
|
|
15388
|
+
const label = first?.type === "paragraph" ? SEVERITY_LINE_RE.exec(sourceText(content, first).trim())?.[1] : void 0;
|
|
15389
|
+
if (label === void 0) return { leadingRun: run };
|
|
15390
|
+
return {
|
|
15391
|
+
severity: {
|
|
15392
|
+
label,
|
|
15393
|
+
line: toLspPosition(first.position.start).line
|
|
15394
|
+
},
|
|
15395
|
+
leadingRun: run.slice(1)
|
|
15396
|
+
};
|
|
15397
|
+
};
|
|
15382
15398
|
/** Splits one chunk's body nodes into its file pointers (hunk pointers are task items, collected recursively at ANY nesting depth via `taskItems` — a nested hunk is the same kind of hunk as a top-level one) and description prose. The description is the chunk's own leading run and never a node that contains a hunk pointer: it stops at the first node whose `taskItems` include a real pointer (at any depth — a blockquote wrapping a pointer list stops it just as a top-level list would). Every node kind in what's left before that point contributes — a `footnoteDefinition` is the one exception, excluded by `blockNodesOfRun` itself (it's a note, never document content). */
|
|
15383
15399
|
const parseChunkBody = (content, lines, title, body) => {
|
|
15384
15400
|
const answer = answerListOf(content, body);
|
|
15385
15401
|
const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
|
|
15386
15402
|
const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)));
|
|
15387
|
-
const
|
|
15403
|
+
const run = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
|
|
15404
|
+
const { severity, leadingRun } = splitSeverity(content, run);
|
|
15388
15405
|
const descriptionNodes = blockNodesOfRun(content, leadingRun);
|
|
15389
15406
|
const descriptionInline = collapseImages(blockRunInline(content, leadingRun));
|
|
15390
15407
|
const description = leadingRun.filter((n) => n.type !== "footnoteDefinition").map((n) => sourceText(content, n)).join(" ").trim();
|
|
@@ -15403,6 +15420,7 @@ const parseChunkBody = (content, lines, title, body) => {
|
|
|
15403
15420
|
files,
|
|
15404
15421
|
errors,
|
|
15405
15422
|
...answer === void 0 ? {} : { answers: answer.options },
|
|
15423
|
+
...severity === void 0 ? {} : { severity },
|
|
15406
15424
|
boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0)
|
|
15407
15425
|
};
|
|
15408
15426
|
};
|
|
@@ -15439,7 +15457,7 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
15439
15457
|
const errors = [];
|
|
15440
15458
|
for (const { heading, headingLine, body } of splitChunks(tree)) {
|
|
15441
15459
|
const title = headingText$1(content, heading);
|
|
15442
|
-
const { description, descriptionNodes, descriptionInline, files, answers, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
15460
|
+
const { description, descriptionNodes, descriptionInline, files, answers, severity, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
15443
15461
|
errors.push(...bodyErrors);
|
|
15444
15462
|
if (files.length === 0 && boxed) errors.push({
|
|
15445
15463
|
message: `Chunk "${title}" has no file pointers`,
|
|
@@ -15453,7 +15471,8 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
15453
15471
|
descriptionInline,
|
|
15454
15472
|
files,
|
|
15455
15473
|
headingLine,
|
|
15456
|
-
...answers === void 0 ? {} : { answers }
|
|
15474
|
+
...answers === void 0 ? {} : { answers },
|
|
15475
|
+
...severity === void 0 ? {} : { severity }
|
|
15457
15476
|
});
|
|
15458
15477
|
}
|
|
15459
15478
|
if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" });
|
|
@@ -15754,6 +15773,7 @@ const reviewView = (content) => {
|
|
|
15754
15773
|
detail: chunk.description,
|
|
15755
15774
|
detailInline: chunk.descriptionInline,
|
|
15756
15775
|
body: chunk.descriptionNodes,
|
|
15776
|
+
...chunk.severity === void 0 ? {} : { status: chunk.severity.label },
|
|
15757
15777
|
anchor: {
|
|
15758
15778
|
kind: "chunk",
|
|
15759
15779
|
index: chunkIndex
|
|
@@ -15901,15 +15921,19 @@ const reviewDescriptor = {
|
|
|
15901
15921
|
openThreads: openThreadsOf
|
|
15902
15922
|
};
|
|
15903
15923
|
const pointerLabel = (f) => `${f.path}${f.line === void 0 ? "" : `#${f.line}${f.rangeEnd === void 0 ? "" : `-${f.rangeEnd}`}`}`;
|
|
15904
|
-
const
|
|
15905
|
-
|
|
15906
|
-
|
|
15907
|
-
|
|
15908
|
-
|
|
15909
|
-
|
|
15910
|
-
|
|
15911
|
-
|
|
15912
|
-
|
|
15924
|
+
const chunkProse = (chunk, retiered) => [chunk?.description ?? "", retiered && chunk?.severity !== void 0 ? severityLine(chunk.severity.label) : ""].filter((p) => p !== "").join("\n\n");
|
|
15925
|
+
const chunkNote = (chunk, prior) => {
|
|
15926
|
+
const retiered = chunk.severity !== void 0 && chunk.severity.label !== prior?.severity?.label;
|
|
15927
|
+
return chunkProse(chunk, retiered) === chunkProse(prior, retiered) || prior === void 0 && chunk.answers !== void 0 ? [] : [{
|
|
15928
|
+
line: chunk.headingLine,
|
|
15929
|
+
note: {
|
|
15930
|
+
kind: "chunk",
|
|
15931
|
+
anchor: chunk.title,
|
|
15932
|
+
before: chunkProse(prior, retiered),
|
|
15933
|
+
text: chunkProse(chunk, retiered)
|
|
15934
|
+
}
|
|
15935
|
+
}];
|
|
15936
|
+
};
|
|
15913
15937
|
const pointerNotes = (chunk, prior) => chunk.files.flatMap((file) => {
|
|
15914
15938
|
const label = pointerLabel(file);
|
|
15915
15939
|
const was = prior?.files.find((f) => pointerLabel(f) === label);
|
|
@@ -16023,7 +16047,13 @@ const reviewDecisions = (before, after) => {
|
|
|
16023
16047
|
const reviewChunks = (content) => parseReviewDoc(content).changesets.map((c) => ({
|
|
16024
16048
|
title: c.title,
|
|
16025
16049
|
line: c.headingLine,
|
|
16026
|
-
pointers: c.files.length
|
|
16050
|
+
pointers: c.files.length,
|
|
16051
|
+
...c.severity === void 0 ? {} : { severityLine: c.severity.line },
|
|
16052
|
+
files: c.files.map((f) => ({
|
|
16053
|
+
path: f.path,
|
|
16054
|
+
...f.line === void 0 ? {} : { line: f.line },
|
|
16055
|
+
...f.rangeEnd === void 0 ? {} : { rangeEnd: f.rangeEnd }
|
|
16056
|
+
}))
|
|
16027
16057
|
}));
|
|
16028
16058
|
//#endregion
|
|
16029
16059
|
//#region src/steering/Edits.ts
|
|
@@ -16393,4 +16423,4 @@ const viewOf = (format, content) => ({
|
|
|
16393
16423
|
/** Every OPEN thread — last entry is the agent's — as `{ name, line, firstMe }` (0-based line). `[]` for a format with no thread concept. */
|
|
16394
16424
|
const openThreads = (format, content) => format.openThreads?.(content) ?? [];
|
|
16395
16425
|
//#endregion
|
|
16396
|
-
export {
|
|
16426
|
+
export { headingSections as A, THREAD_REPLY_ACTION_TITLE as C, parseThreadsWithFindings as D, parseThreads as E, getParseCount as O, FOOTNOTE_ACTION_TITLE as S, parseFootnotes as T, reviewRisks as _, unansweredQuestions as a, FREE_TEXT_PLACEHOLDER as b, stripCodeThreads as c, ANSWER_FIX as d, ANSWER_LEAD as f, reviewNotes as g, reviewDecisions as h, steeringFormatOrFreeForm as i, formatFinding as j, headingSectionBodies as k, freeFormFormat as l, reviewChunks as m, openThreads as n, viewOf as o, ANSWER_WONT_FIX as p, steeringFormatFor as r, parseCodeThreads as s, BUILT_IN_MODE_NAMES as t, applySteeringEdits as u, severityLine as v, openThreadFindings as w, isAnswered as x, unansweredNotes as y };
|
|
@@ -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 { lensEntriesOf, lensOf, qualityLap, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
-
import { A as reviewQuality, D as fixRisks, E as fixQuality,
|
|
3
|
+
import { A as reviewQuality, D as fixRisks, E as fixQuality, Ot as withLapFindings, Y as handOffAwaitReviewMessage, d as healthy, it as qualityTriageMessage, j as reviewing, k as reviewMissing, m as QUALITY, rt as qualityFindingsFile, u as guarded, v as REVIEW, w as fixCheck, x as awaitReview } from "./reserved-CdOzKFVX.mjs";
|
|
4
4
|
//#region src/workflows/shared/lenses.ts
|
|
5
5
|
const NO_FINDINGS = {
|
|
6
6
|
notes: [],
|