@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/dist/steering.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
export { ANSWER_FIX, ANSWER_LEAD, ANSWER_WONT_FIX, BUILT_IN_MODE_NAMES, FOOTNOTE_ACTION_TITLE, FREE_TEXT_PLACEHOLDER, THREAD_REPLY_ACTION_TITLE, applySteeringEdits, formatFinding, freeFormFormat, getParseCount, headingSectionBodies, headingSections, isAnswered, openThreadFindings, openThreads, parseCodeThreads, parseFootnotes, parseThreads, parseThreadsWithFindings, reviewChunks, reviewDecisions, reviewNotes, reviewRisks, steeringFormatFor, steeringFormatOrFreeForm, stripCodeThreads, unansweredNotes, unansweredQuestions, viewOf };
|
|
1
|
+
import { A as headingSections, C as THREAD_REPLY_ACTION_TITLE, D as parseThreadsWithFindings, E as parseThreads, O as getParseCount, S as FOOTNOTE_ACTION_TITLE, T as parseFootnotes, _ as reviewRisks, a as unansweredQuestions, b as FREE_TEXT_PLACEHOLDER, c as stripCodeThreads, d as ANSWER_FIX, f as ANSWER_LEAD, g as reviewNotes, h as reviewDecisions, i as steeringFormatOrFreeForm, j as formatFinding, k as headingSectionBodies, l as freeFormFormat, m as reviewChunks, n as openThreads, o as viewOf, p as ANSWER_WONT_FIX, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES, u as applySteeringEdits, v as severityLine, w as openThreadFindings, x as isAnswered, y as unansweredNotes } from "./chunks/steering-DtUVUz9l.mjs";
|
|
2
|
+
export { ANSWER_FIX, ANSWER_LEAD, ANSWER_WONT_FIX, BUILT_IN_MODE_NAMES, FOOTNOTE_ACTION_TITLE, FREE_TEXT_PLACEHOLDER, THREAD_REPLY_ACTION_TITLE, applySteeringEdits, formatFinding, freeFormFormat, getParseCount, headingSectionBodies, headingSections, isAnswered, openThreadFindings, openThreads, parseCodeThreads, parseFootnotes, parseThreads, parseThreadsWithFindings, reviewChunks, reviewDecisions, reviewNotes, reviewRisks, severityLine, steeringFormatFor, steeringFormatOrFreeForm, stripCodeThreads, unansweredNotes, unansweredQuestions, viewOf };
|
package/dist/testkit.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { answered, applyScript, checkScript, configProblems, configure, installContext, isFactoryWorkflow, moveScript, numeric, protocolMismatch, removeScript, restoreScript, revertScript, showScript, t as flows_exports, writeScript } from "./flows.mjs";
|
|
2
2
|
import { A as beatKindOf, B as ACCESS_REFUSAL, C as renderLandPlain, D as renderBeatJson, E as beatDocument, O as statusOf, S as renderLandJson, T as renderBeatPlain, U as renderFormat, _ as selectPath, a as contextLimitFault, g as resolveSession, h as globMatches, j as demandOf, l as parseContextWindow, m as slicesDocument, o as contextShapeFault, p as renderSlicesJson, v as judgeJson, x as noopText, y as landFields } from "./chunks/wire-BJuZWR3C.mjs";
|
|
3
|
-
import { A as
|
|
3
|
+
import { A as headingSections, D as parseThreadsWithFindings, a as unansweredQuestions, j as formatFinding, k as headingSectionBodies, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES } from "./chunks/steering-DtUVUz9l.mjs";
|
|
4
4
|
import { $ as logWarning, $i as BaseProto, $n as map$5, $r as make$19, $t as provide$1, A as catchIf, Ai as add$1, An as scopeMake, Ar as flatMap$3, At as suspend$5, B as flatMap$4, Bi as getOrElse, Bn as get$3, Br as sync$1, Bt as unsafeMakeLatch, C as all$1, Ci as toArray, Cn as isInterrupted, Cr as exitFail, Ct as sandbox, D as async, Di as GenericTag, Dn as squash, Dr as exitZipRight, Dt as scopedWith$1, E as asVoid$1, Ei as unsafeHead, En as reduceWithContext, Er as exitVoid, Et as scoped$4, F as ensuring$2, Fi as fromIterable, Fn as join$3, Fr as map$3, Ft as tryMap, G as forkIn, Gi as match$4, Gn as TypeId$10, Gr as whenEffect, Gt as zipLeft, H as fnUntraced, Hi as isNone, Hn as modify$1, Hr as uninterruptible, Ht as when, I as exit, Ii as splitAt, In as sequential, Ir as onInterrupt, It as tryPromise, J as ignore, Ji as isLeft, Jn as all, Jr as DeferredTypeId$1, Jt as spaced$1, K as forkScoped, Ki as none, Kn as Class, Kr as withFiberRuntime, Kt as zipRight$3, L as fail$5, Li as filter$1, Ln as bind, Lr as scopeClose, Lt as try_, M as die$2, Mi as getOption, Mn as _await$2, Mr as interruptAsFiber, Mt as tap, N as dieMessage, Ni as omit, Nn as inheritAll$1, Nr as interruptFiber, Nt as tapErrorCause, O as catchAll$1, Oi as Reference, On as ensuring$3, Or as fiberId, Ot as serviceOption, P as either, Pi as empty$5, Pn as interruptAllAs$1, Pr as interruptible$1, Pt as timeout, Q as isEffect, Qi as pipeArguments, Qn as isFailure, Qr as ids, Qt as mergeAll$2, R as failCause$4, Ri as flatMap$5, Rn as bindTo, Rr as succeed$4, Rt as uninterruptible$1, S as addFinalizer$1, Si as takeRight, Sn as isDieType, Sr as exitAs, St as runtime, T as as, Ti as unsafeGet, Tn as map$6, Tr as exitSucceed, Tt as scopeWith, U as forEach$2, Ui as isSome, Un as set$1, Ur as uninterruptibleMask$1, Ut as withFiberRuntime$1, V as flatten$4, Vi as getOrUndefined, Vn as make$16, Vr as tap$1, Vt as unsafeMakeSemaphore$1, W as forkDaemon, Wi as map$8, Wn as update$1, Wr as void_$2, Wt as zip$1, X as interruptible, Xi as match$2, Xn as fail$4, Xr as combine, Xt as tagged, Y as interrupt$4, Yi as left, Yn as die, Yr as empty$6, Yt as TaggedError$1, Z as intoDeferred, Zi as right, Zn as failCause$3, Zr as combineAll, Zt as fromEffect$5, _ as decodeSync, _i as of, _n as fail$3, _r as deferredIsDone, _t as raceWith, a as Literal, aa as constFalse, ai as size$2, an as updateContext$1, ar as zipRight$2, at as matchCause, b as acquireRelease, bi as reverse, bn as interrupt$3, br as deferredUnsafeDone, bt as runPromise, c as String$, ca as constVoid, ci as append$1, cn as Scope, cr as asyncInterrupt, ct as negate, d as Union, da as pipe, di as empty$4, dn as close, dr as deferredAwait, dt as orElse, ea as NodeInspectSymbol, ei as none$1, en as provideMerge$1, er as mapBoth, et as makeSemaphore, f as between, fi as filter, fn as extend, fr as deferredComplete, ft as orElseSucceed, gi as map$7, gn as die$1, gr as deferredInterruptWith, gt as race, hi as isEmpty$2, hn as NoSuchElementException, hr as deferredInterrupt, ht as provideService, i as Defect, ia as hasProperty, ii as has, in as unsafeRunPromiseExit, ir as zip$2, it as match$5, j as catchTag, ji as get$5, jn as scopeTag, jr as interrupt$2, jt as sync$2, k as catchAllCause, ki as Tag, kn as forEachConcurrentDiscard, kr as fiberRefLocally, kt as succeed$6, l as Struct, la as dual, li as appendAll, ln as addFinalizer, lr as catchAll, lt as onError, m as optional$1, mi as isChunk, mn as make$18, mr as deferredFailCause, mt as provide$2, n as Boolean$, na as toJSON, ni as make$17, nn as succeed$3, nr as succeed$5, nt as mapBoth$1, o as Never, oa as constTrue, oi as decode, on as forkIn$1, or as as$1, ot as matchCauseEffect, p as decodeUnknown, pi as fromIterable$1, pn as fork, pr as deferredFail, pt as promise, q as gen, qi as some, qn as spanTag, qr as zipRight$1, qt as zipWith, r as Cause, ra as equals, ri as set$2, rn as unsafeFork, rr as void_$3, rt as mapError, s as Number$, sa as constUndefined, si as isFinite, sn as unsafeMakeSemaphore, sr as asVoid, st as matchEffect, t as Array$, ta as format, ti as get$4, tn as scoped$3, tr as match$3, tt as map$4, u as TaggedError, ua as identity, ui as drop, un as addFinalizerExit, ur as currentContext$1, ut as orDie, v as encodeSync, vi as prepend, vn as failureOrCause, vr as deferredMake, vt as repeat, w as annotateLogs, wi as unsafeFromArray, wn as isInterruptedOnly, wr as exitFailCause, wt as scope, x as acquireUseRelease$2, xi as take$2, xn as interruptors, xr as deferredUnsafeMake, xt as runPromiseExit$1, y as _void, yi as reduce, yn as flipCauseOption, yr as deferredSucceed, yt as retry, z as fiberIdWith, zi as fromNullable, zn as sleep, zr as suspend$4, zt as uninterruptibleMask } from "./chunks/Schema-C6fLlQzx.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import * as Crypto from "node:crypto";
|
package/dist/web/main.js
CHANGED
|
@@ -31565,6 +31565,8 @@ const qaDescriptor = {
|
|
|
31565
31565
|
unansweredQuestions,
|
|
31566
31566
|
openThreads: openThreadsOf
|
|
31567
31567
|
};
|
|
31568
|
+
/** A chunk's leading `_critical: security_` paragraph: the tier gtd ranked it with, and why. */
|
|
31569
|
+
const SEVERITY_LINE_RE = /^_((?:critical|should|could)(?::[^_\n]*)?)_$/;
|
|
31568
31570
|
/**
|
|
31569
31571
|
* The canonical sample. Every detail here is load-bearing for the formatter
|
|
31570
31572
|
* round-trip: both footnote bodies exceed 80 characters so a reflow actually
|
|
@@ -31855,12 +31857,26 @@ const answerListOf = (content, body) => {
|
|
|
31855
31857
|
})
|
|
31856
31858
|
};
|
|
31857
31859
|
};
|
|
31860
|
+
/** A leading severity line is the chunk's tier, never its prose. */
|
|
31861
|
+
const splitSeverity = (content, run) => {
|
|
31862
|
+
const first = run[0];
|
|
31863
|
+
const label = first?.type === "paragraph" ? SEVERITY_LINE_RE.exec(sourceText(content, first).trim())?.[1] : void 0;
|
|
31864
|
+
if (label === void 0) return { leadingRun: run };
|
|
31865
|
+
return {
|
|
31866
|
+
severity: {
|
|
31867
|
+
label,
|
|
31868
|
+
line: toLspPosition(first.position.start).line
|
|
31869
|
+
},
|
|
31870
|
+
leadingRun: run.slice(1)
|
|
31871
|
+
};
|
|
31872
|
+
};
|
|
31858
31873
|
/** 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). */
|
|
31859
31874
|
const parseChunkBody = (content, lines, title, body) => {
|
|
31860
31875
|
const answer = answerListOf(content, body);
|
|
31861
31876
|
const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
|
|
31862
31877
|
const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)));
|
|
31863
|
-
const
|
|
31878
|
+
const run = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
|
|
31879
|
+
const { severity, leadingRun } = splitSeverity(content, run);
|
|
31864
31880
|
const descriptionNodes = blockNodesOfRun(content, leadingRun);
|
|
31865
31881
|
const descriptionInline = collapseImages(blockRunInline(content, leadingRun));
|
|
31866
31882
|
const description = leadingRun.filter((n) => n.type !== "footnoteDefinition").map((n) => sourceText(content, n)).join(" ").trim();
|
|
@@ -31879,6 +31895,7 @@ const parseChunkBody = (content, lines, title, body) => {
|
|
|
31879
31895
|
files,
|
|
31880
31896
|
errors,
|
|
31881
31897
|
...answer === void 0 ? {} : { answers: answer.options },
|
|
31898
|
+
...severity === void 0 ? {} : { severity },
|
|
31882
31899
|
boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0)
|
|
31883
31900
|
};
|
|
31884
31901
|
};
|
|
@@ -31915,7 +31932,7 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
31915
31932
|
const errors = [];
|
|
31916
31933
|
for (const { heading, headingLine, body } of splitChunks(tree)) {
|
|
31917
31934
|
const title = headingText$1(content, heading);
|
|
31918
|
-
const { description, descriptionNodes, descriptionInline, files, answers, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
31935
|
+
const { description, descriptionNodes, descriptionInline, files, answers, severity, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
31919
31936
|
errors.push(...bodyErrors);
|
|
31920
31937
|
if (files.length === 0 && boxed) errors.push({
|
|
31921
31938
|
message: `Chunk "${title}" has no file pointers`,
|
|
@@ -31929,7 +31946,8 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
31929
31946
|
descriptionInline,
|
|
31930
31947
|
files,
|
|
31931
31948
|
headingLine,
|
|
31932
|
-
...answers === void 0 ? {} : { answers }
|
|
31949
|
+
...answers === void 0 ? {} : { answers },
|
|
31950
|
+
...severity === void 0 ? {} : { severity }
|
|
31933
31951
|
});
|
|
31934
31952
|
}
|
|
31935
31953
|
if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" });
|
|
@@ -32230,6 +32248,7 @@ const reviewView = (content) => {
|
|
|
32230
32248
|
detail: chunk.description,
|
|
32231
32249
|
detailInline: chunk.descriptionInline,
|
|
32232
32250
|
body: chunk.descriptionNodes,
|
|
32251
|
+
...chunk.severity === void 0 ? {} : { status: chunk.severity.label },
|
|
32233
32252
|
anchor: {
|
|
32234
32253
|
kind: "chunk",
|
|
32235
32254
|
index: chunkIndex
|
|
@@ -34779,6 +34798,11 @@ const ChunkRow = ({ chunk, chunkIndex, state }) => {
|
|
|
34779
34798
|
disabled: hunks.length === 0,
|
|
34780
34799
|
className: "flex-1 text-left",
|
|
34781
34800
|
children: [
|
|
34801
|
+
chunk.status !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
34802
|
+
"data-testid": `chunk-severity-${chunkIndex}`,
|
|
34803
|
+
className: cn("text-small font-semibold", chunk.status.startsWith("critical") ? "text-warning" : "text-muted"),
|
|
34804
|
+
children: chunk.status
|
|
34805
|
+
}),
|
|
34782
34806
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
34783
34807
|
className: "font-semibold",
|
|
34784
34808
|
children: chunk.title
|
package/dist/workflow-kit.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { changes, changesSince, hasThreadFor, head, mark, read, refuse, requireReplies, requireThreadsClosed, scope } from "./flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { E as parseThreads, _ as reviewRisks, c as stripCodeThreads, g as reviewNotes, h as reviewDecisions, y as unansweredNotes } from "./chunks/steering-DtUVUz9l.mjs";
|
|
3
3
|
//#region src/workflow-kit/health.ts
|
|
4
4
|
/** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
|
|
5
5
|
const escalation = async (count, steps) => {
|
|
@@ -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 { At as defaults, Gt as registryEntry, Kt as requireFlag, S as coder, Ut as flagReferences, i as skills, jt as envDefaults, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-CdOzKFVX.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-DRM2eC1z.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 () => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
|
|
2
2
|
import { lensEntriesOf } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as
|
|
4
|
-
import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-
|
|
5
|
-
import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-
|
|
3
|
+
import { $ as naiveApplyFailure, At as defaults, B as coverageMessage, C as collecting, Ct as uncoveredMessage, Et as unwindFailure, F as builderSystem, G as designSystem, H as coveringPrompt, K as designTriagePrompt, L as clearCriteriaMessage, M as agentWithSkills, Mt as testFileGlobs, N as buildReviewCollectingPrompt, O as planner, Q as implementFixPrompt, R as clearCriteriaQuestion, S as coder, T as fixNits, Tt as unwindFailedMessage, U as criterionLine, V as coverageQuestion, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REQUIREMENTS, _t as reviewerSystem, a as flagModeOn, b as answerReviewQuestions, bt as slicingPrompt, ct as refactorFixPrompt, d as healthy, et as naivePassPrompt, f as FEEDBACK, g as REFACTOR_NOTE, h as REFACTOR, i as skills, jt as envDefaults, l as gate, lt as refactorNotePrompt, n as summary, nt as oversizedMessage, ot as refactorBuildingPrompt, p as HANDOFF, r as access, s as shipped, st as refactorDroppedRefusal, t as steering, tt as normaliseCriteriaScript, ut as refactorRules, vt as seamByHandMessage, xt as startGateBlockedMessage, y as SLICES, yt as seamSteerMessage } from "../chunks/reserved-CdOzKFVX.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-DRM2eC1z.mjs";
|
|
5
|
+
import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-BChXEl2Y.mjs";
|
|
6
6
|
import { posix } from "node:path";
|
|
7
7
|
//#region src/workflows/shared/fences.ts
|
|
8
8
|
const markFenced = (text) => {
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
|
-
import { _ as reviewRisks, g as reviewNotes } from "../chunks/steering-
|
|
3
|
-
import {
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
2
|
+
import { _ as reviewRisks, g as reviewNotes } from "../chunks/steering-DtUVUz9l.mjs";
|
|
3
|
+
import { At as defaults, J as fixReviewNotesPrompt, M as agentWithSkills, S as coder, c as baseline, i as skills, jt as envDefaults, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, v as REVIEW } from "../chunks/reserved-CdOzKFVX.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-DRM2eC1z.mjs";
|
|
5
5
|
//#region src/workflows/fix/fix.ts
|
|
6
6
|
/**
|
|
7
7
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, start } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff } from "../chunks/tail-
|
|
4
|
-
import { i as reUnwind, t as buildTail } from "../chunks/review-
|
|
2
|
+
import { At as defaults, M as agentWithSkills, S as coder, _ as REQUIREMENTS, i as skills, jt as envDefaults, l as gate, mt as reviewGateBlockedMessage, n as summary, pt as reviewFeedbackFixPrompt, q as finisherSystem, r as access, s as shipped, t as steering } from "../chunks/reserved-CdOzKFVX.mjs";
|
|
3
|
+
import { a as refusePendingHandOff } from "../chunks/tail-DRM2eC1z.mjs";
|
|
4
|
+
import { i as reUnwind, t as buildTail } from "../chunks/review-BChXEl2Y.mjs";
|
|
5
5
|
//#region src/workflows/review/review.ts
|
|
6
6
|
const fixFeedback = async () => {
|
|
7
7
|
await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { At as defaults, Ht as flagMentions, Kt as requireFlag, S as coder, Wt as flipProblems, f as FEEDBACK, i as skills, jt as envDefaults, l as gate, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-CdOzKFVX.mjs";
|
|
3
3
|
//#region src/workflows/rollout/rollout.ts
|
|
4
4
|
const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
|
|
5
5
|
const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
|
package/dist/workflows.mjs
CHANGED
|
@@ -15648,6 +15648,9 @@ const qaDescriptor = {
|
|
|
15648
15648
|
const ANSWER_LEAD = "Tick exactly one:";
|
|
15649
15649
|
const ANSWER_FIX = "fix as suggested";
|
|
15650
15650
|
const ANSWER_WONT_FIX = "won't fix";
|
|
15651
|
+
/** A chunk's leading `_critical: security_` paragraph: the tier gtd ranked it with, and why. */
|
|
15652
|
+
const SEVERITY_LINE_RE = /^_((?:critical|should|could)(?::[^_\n]*)?)_$/;
|
|
15653
|
+
const severityLine = (label) => `_${label}_`;
|
|
15651
15654
|
/**
|
|
15652
15655
|
* The canonical sample. Every detail here is load-bearing for the formatter
|
|
15653
15656
|
* round-trip: both footnote bodies exceed 80 characters so a reflow actually
|
|
@@ -15938,12 +15941,26 @@ const answerListOf = (content, body) => {
|
|
|
15938
15941
|
})
|
|
15939
15942
|
};
|
|
15940
15943
|
};
|
|
15944
|
+
/** A leading severity line is the chunk's tier, never its prose. */
|
|
15945
|
+
const splitSeverity = (content, run) => {
|
|
15946
|
+
const first = run[0];
|
|
15947
|
+
const label = first?.type === "paragraph" ? SEVERITY_LINE_RE.exec(sourceText(content, first).trim())?.[1] : void 0;
|
|
15948
|
+
if (label === void 0) return { leadingRun: run };
|
|
15949
|
+
return {
|
|
15950
|
+
severity: {
|
|
15951
|
+
label,
|
|
15952
|
+
line: toLspPosition(first.position.start).line
|
|
15953
|
+
},
|
|
15954
|
+
leadingRun: run.slice(1)
|
|
15955
|
+
};
|
|
15956
|
+
};
|
|
15941
15957
|
/** 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). */
|
|
15942
15958
|
const parseChunkBody = (content, lines, title, body) => {
|
|
15943
15959
|
const answer = answerListOf(content, body);
|
|
15944
15960
|
const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
|
|
15945
15961
|
const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)));
|
|
15946
|
-
const
|
|
15962
|
+
const run = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
|
|
15963
|
+
const { severity, leadingRun } = splitSeverity(content, run);
|
|
15947
15964
|
const descriptionNodes = blockNodesOfRun(content, leadingRun);
|
|
15948
15965
|
const descriptionInline = collapseImages(blockRunInline(content, leadingRun));
|
|
15949
15966
|
const description = leadingRun.filter((n) => n.type !== "footnoteDefinition").map((n) => sourceText(content, n)).join(" ").trim();
|
|
@@ -15962,6 +15979,7 @@ const parseChunkBody = (content, lines, title, body) => {
|
|
|
15962
15979
|
files,
|
|
15963
15980
|
errors,
|
|
15964
15981
|
...answer === void 0 ? {} : { answers: answer.options },
|
|
15982
|
+
...severity === void 0 ? {} : { severity },
|
|
15965
15983
|
boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0)
|
|
15966
15984
|
};
|
|
15967
15985
|
};
|
|
@@ -15998,7 +16016,7 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
15998
16016
|
const errors = [];
|
|
15999
16017
|
for (const { heading, headingLine, body } of splitChunks(tree)) {
|
|
16000
16018
|
const title = headingText$1(content, heading);
|
|
16001
|
-
const { description, descriptionNodes, descriptionInline, files, answers, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
16019
|
+
const { description, descriptionNodes, descriptionInline, files, answers, severity, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
16002
16020
|
errors.push(...bodyErrors);
|
|
16003
16021
|
if (files.length === 0 && boxed) errors.push({
|
|
16004
16022
|
message: `Chunk "${title}" has no file pointers`,
|
|
@@ -16012,7 +16030,8 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
16012
16030
|
descriptionInline,
|
|
16013
16031
|
files,
|
|
16014
16032
|
headingLine,
|
|
16015
|
-
...answers === void 0 ? {} : { answers }
|
|
16033
|
+
...answers === void 0 ? {} : { answers },
|
|
16034
|
+
...severity === void 0 ? {} : { severity }
|
|
16016
16035
|
});
|
|
16017
16036
|
}
|
|
16018
16037
|
if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" });
|
|
@@ -16313,6 +16332,7 @@ const reviewView = (content) => {
|
|
|
16313
16332
|
detail: chunk.description,
|
|
16314
16333
|
detailInline: chunk.descriptionInline,
|
|
16315
16334
|
body: chunk.descriptionNodes,
|
|
16335
|
+
...chunk.severity === void 0 ? {} : { status: chunk.severity.label },
|
|
16316
16336
|
anchor: {
|
|
16317
16337
|
kind: "chunk",
|
|
16318
16338
|
index: chunkIndex
|
|
@@ -16460,15 +16480,19 @@ const reviewDescriptor = {
|
|
|
16460
16480
|
openThreads: openThreadsOf
|
|
16461
16481
|
};
|
|
16462
16482
|
const pointerLabel = (f) => `${f.path}${f.line === void 0 ? "" : `#${f.line}${f.rangeEnd === void 0 ? "" : `-${f.rangeEnd}`}`}`;
|
|
16463
|
-
const
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
|
|
16468
|
-
|
|
16469
|
-
|
|
16470
|
-
|
|
16471
|
-
|
|
16483
|
+
const chunkProse = (chunk, retiered) => [chunk?.description ?? "", retiered && chunk?.severity !== void 0 ? severityLine(chunk.severity.label) : ""].filter((p) => p !== "").join("\n\n");
|
|
16484
|
+
const chunkNote = (chunk, prior) => {
|
|
16485
|
+
const retiered = chunk.severity !== void 0 && chunk.severity.label !== prior?.severity?.label;
|
|
16486
|
+
return chunkProse(chunk, retiered) === chunkProse(prior, retiered) || prior === void 0 && chunk.answers !== void 0 ? [] : [{
|
|
16487
|
+
line: chunk.headingLine,
|
|
16488
|
+
note: {
|
|
16489
|
+
kind: "chunk",
|
|
16490
|
+
anchor: chunk.title,
|
|
16491
|
+
before: chunkProse(prior, retiered),
|
|
16492
|
+
text: chunkProse(chunk, retiered)
|
|
16493
|
+
}
|
|
16494
|
+
}];
|
|
16495
|
+
};
|
|
16472
16496
|
const pointerNotes = (chunk, prior) => chunk.files.flatMap((file) => {
|
|
16473
16497
|
const label = pointerLabel(file);
|
|
16474
16498
|
const was = prior?.files.find((f) => pointerLabel(f) === label);
|
|
@@ -16582,7 +16606,13 @@ const reviewDecisions = (before, after) => {
|
|
|
16582
16606
|
const reviewChunks = (content) => parseReviewDoc(content).changesets.map((c) => ({
|
|
16583
16607
|
title: c.title,
|
|
16584
16608
|
line: c.headingLine,
|
|
16585
|
-
pointers: c.files.length
|
|
16609
|
+
pointers: c.files.length,
|
|
16610
|
+
...c.severity === void 0 ? {} : { severityLine: c.severity.line },
|
|
16611
|
+
files: c.files.map((f) => ({
|
|
16612
|
+
path: f.path,
|
|
16613
|
+
...f.line === void 0 ? {} : { line: f.line },
|
|
16614
|
+
...f.rangeEnd === void 0 ? {} : { rangeEnd: f.rangeEnd }
|
|
16615
|
+
}))
|
|
16586
16616
|
}));
|
|
16587
16617
|
//#endregion
|
|
16588
16618
|
//#region src/steering/CodeThreads.ts
|
|
@@ -17609,6 +17639,56 @@ const withWalkthrough = (review, report) => {
|
|
|
17609
17639
|
const body = lines.join("\n").trimEnd();
|
|
17610
17640
|
return summary.length === 0 ? `${body}\n` : `${body}\n\n${summary.join("\n\n")}\n`;
|
|
17611
17641
|
};
|
|
17642
|
+
const chunkBlocks = (review) => {
|
|
17643
|
+
const lines = review.split("\n");
|
|
17644
|
+
const chunks = reviewChunks(review);
|
|
17645
|
+
return {
|
|
17646
|
+
head: lines.slice(0, chunks[0]?.line ?? lines.length),
|
|
17647
|
+
blocks: chunks.map((c, i) => ({
|
|
17648
|
+
title: c.title,
|
|
17649
|
+
lines: lines.slice(c.line, chunks[i + 1]?.line ?? lines.length)
|
|
17650
|
+
}))
|
|
17651
|
+
};
|
|
17652
|
+
};
|
|
17653
|
+
const isBlank = (line) => line !== void 0 && line.trim() === "";
|
|
17654
|
+
/** \`review\` with every chunk's severity line removed, so a re-review is ranked afresh. */
|
|
17655
|
+
const unranked = (review) => {
|
|
17656
|
+
const drop = new Set(reviewChunks(review).flatMap((c) => c.severityLine ?? []));
|
|
17657
|
+
if (drop.size === 0) return review;
|
|
17658
|
+
const lines = review.split("\n");
|
|
17659
|
+
return lines.filter((_, i) => !drop.has(i) && !(drop.has(i - 1) && isBlank(lines[i]))).join("\n");
|
|
17660
|
+
};
|
|
17661
|
+
const chunkTexts = (review) => new Map(chunkBlocks(review).blocks.map((b) => [b.title, b.lines.join("\n").trim()]));
|
|
17662
|
+
const trimBlankLines = (lines) => {
|
|
17663
|
+
let start = 0;
|
|
17664
|
+
let end = lines.length;
|
|
17665
|
+
while (start < end && isBlank(lines[start])) start++;
|
|
17666
|
+
while (end > start && isBlank(lines[end - 1])) end--;
|
|
17667
|
+
return lines.slice(start, end);
|
|
17668
|
+
};
|
|
17669
|
+
/**
|
|
17670
|
+
* \`review\` with the \`ranked\` chunks first, in their order, each led by its
|
|
17671
|
+
* severity line; every other chunk follows in place.
|
|
17672
|
+
*/
|
|
17673
|
+
const rankedReview = (review, ranked) => {
|
|
17674
|
+
const { head, blocks } = chunkBlocks(review);
|
|
17675
|
+
const place = new Map(ranked.map((r, k) => [r.title, k]));
|
|
17676
|
+
const labels = new Map(ranked.map((r) => [r.title, r.label]));
|
|
17677
|
+
const order = (i) => place.get(blocks[i].title) ?? ranked.length;
|
|
17678
|
+
const chunk = (i) => {
|
|
17679
|
+
const { title, lines } = blocks[i];
|
|
17680
|
+
const label = labels.get(title);
|
|
17681
|
+
const heading = /^ {0,3}-+\s*$/.test(lines[1] ?? "") ? 2 : 1;
|
|
17682
|
+
const body = trimBlankLines(lines.slice(heading)).join("\n");
|
|
17683
|
+
return [
|
|
17684
|
+
lines.slice(0, heading).join("\n"),
|
|
17685
|
+
...label === void 0 ? [] : [severityLine(label)],
|
|
17686
|
+
...body === "" ? [] : [body]
|
|
17687
|
+
].join("\n\n");
|
|
17688
|
+
};
|
|
17689
|
+
const sorted = blocks.map((_, i) => i).sort((x, y) => order(x) - order(y) || x - y);
|
|
17690
|
+
return `${[head.join("\n").trim(), ...sorted.map(chunk)].filter((p) => p !== "").join("\n\n")}\n`;
|
|
17691
|
+
};
|
|
17612
17692
|
/**
|
|
17613
17693
|
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
|
|
17614
17694
|
* answer boxes, then the fixed and noise lists. Chunk titles are made unique —
|
|
@@ -18163,6 +18243,13 @@ const wrapItem = (item) => {
|
|
|
18163
18243
|
} else line = line === "" ? word : `${line} ${word}`;
|
|
18164
18244
|
return [...out, line];
|
|
18165
18245
|
};
|
|
18246
|
+
const reviewRankingMessage = () => `Ranking each chunk of the review before the human reads it. Run
|
|
18247
|
+
\`gtd judge answer\` and pipe, per chunk \`C<n>\`: \`C<n>.security\`
|
|
18248
|
+
(yes/no), \`C<n>.reversibility\` (one-way/two-way),
|
|
18249
|
+
\`C<n>.blastRadius\` (local/module/contract) and \`C<n>.newConcept\`
|
|
18250
|
+
(yes/no) — or land without a verdict, which lists every chunk as
|
|
18251
|
+
critical.
|
|
18252
|
+
`;
|
|
18166
18253
|
const buildReviewTriageMessage = () => `Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
|
|
18167
18254
|
footnote, or prose under a chunk) with one verdict: \`edit\` (a change
|
|
18168
18255
|
request), \`question\`, \`nit\` (a small fix needing no re-plan) or
|
|
@@ -39052,7 +39139,8 @@ const severityOf = (note, judgment, minP) => {
|
|
|
39052
39139
|
if (critical.length > 0) return {
|
|
39053
39140
|
tier: "critical",
|
|
39054
39141
|
reason: `critical: ${critical.join(", ")}`,
|
|
39055
|
-
rank: rank()
|
|
39142
|
+
rank: rank(),
|
|
39143
|
+
...a("security") === "yes" ? { security: true } : {}
|
|
39056
39144
|
};
|
|
39057
39145
|
const should = [...a("blastRadius") === "contract" ? ["contract"] : [], ...a("newConcept") === "yes" ? ["new concept"] : []];
|
|
39058
39146
|
if (should.length > 0) return {
|
|
@@ -39066,29 +39154,30 @@ const severityOf = (note, judgment, minP) => {
|
|
|
39066
39154
|
rank: 2
|
|
39067
39155
|
};
|
|
39068
39156
|
};
|
|
39069
|
-
|
|
39157
|
+
/** `asks` names the change in full, `change` refers back to it. */
|
|
39158
|
+
const severityQuestions = (id, asks, change) => [
|
|
39070
39159
|
{
|
|
39071
|
-
id: `${
|
|
39160
|
+
id: `${id}.security`,
|
|
39072
39161
|
primitive: "noul",
|
|
39073
|
-
instructions: `Does
|
|
39162
|
+
instructions: `Does ${asks} touch security — authentication, authorisation, secrets, untrusted input or data exposure?`,
|
|
39074
39163
|
criteria: "yes: it changes or could weaken a security property. no: it does not."
|
|
39075
39164
|
},
|
|
39076
39165
|
{
|
|
39077
|
-
id: `${
|
|
39166
|
+
id: `${id}.reversibility`,
|
|
39078
39167
|
primitive: "choice",
|
|
39079
|
-
instructions: `Once
|
|
39168
|
+
instructions: `Once ${change} ships, can it be undone without breaking anyone who already depends on it?`,
|
|
39080
39169
|
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."
|
|
39081
39170
|
},
|
|
39082
39171
|
{
|
|
39083
|
-
id: `${
|
|
39172
|
+
id: `${id}.blastRadius`,
|
|
39084
39173
|
primitive: "choice",
|
|
39085
|
-
instructions: `How far does
|
|
39174
|
+
instructions: `How far does ${change} reach?`,
|
|
39086
39175
|
criteria: "local: one function or file. module: a whole module or several files. contract: a public interface other code or users depend on."
|
|
39087
39176
|
},
|
|
39088
39177
|
{
|
|
39089
|
-
id: `${
|
|
39178
|
+
id: `${id}.newConcept`,
|
|
39090
39179
|
primitive: "noul",
|
|
39091
|
-
instructions: `Does
|
|
39180
|
+
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.`,
|
|
39092
39181
|
criteria: "yes: a genuinely new concept. no: reuses or extends an existing one."
|
|
39093
39182
|
}
|
|
39094
39183
|
];
|
|
@@ -39105,7 +39194,7 @@ const triage$1 = (severities) => async (notes) => {
|
|
|
39105
39194
|
answers: {},
|
|
39106
39195
|
truncated: []
|
|
39107
39196
|
} : await judge("review.triage", {
|
|
39108
|
-
questions: asked.flatMap((n) => [verdictQuestion(n), ...severityQuestions(n)]),
|
|
39197
|
+
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}"`)]),
|
|
39109
39198
|
evidence: Object.fromEntries(asked.map((n) => [n.id, `Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`])),
|
|
39110
39199
|
message: buildReviewTriageMessage(),
|
|
39111
39200
|
label: "Judging each note"
|
|
@@ -39121,6 +39210,67 @@ const triage$1 = (severities) => async (notes) => {
|
|
|
39121
39210
|
return [n.id, "edit"];
|
|
39122
39211
|
}));
|
|
39123
39212
|
};
|
|
39213
|
+
/** Review order: security, one-way doors, the judge unsure, then should, then could. */
|
|
39214
|
+
const chunkKey = (s) => s.tier === "could" ? 4 : s.tier === "should" ? 3 : s.security === true ? 0 : s.rank === 0 ? 1 : 2;
|
|
39215
|
+
/** A chunk's text, then the code each of its pointers covers: a bare path the whole file. */
|
|
39216
|
+
const chunkEvidence = (text, files) => [text, ...files.map(({ path, line, rangeEnd }) => {
|
|
39217
|
+
const where = `${path}${line === void 0 ? "" : `#${line}${rangeEnd === void 0 ? "" : `-${rangeEnd}`}`}`;
|
|
39218
|
+
const lines = read(path.replace(/^\.\//, ""))?.split("\n");
|
|
39219
|
+
if (lines === void 0) return `${where}: no longer exists`;
|
|
39220
|
+
return `${where}:\n${(line === void 0 ? lines : lines.slice(line - 1, rangeEnd ?? line)).join("\n")}`;
|
|
39221
|
+
})].join("\n\n");
|
|
39222
|
+
const bytes = (text) => new TextEncoder().encode(text).length;
|
|
39223
|
+
/** Fills each batch while the budget's even share still holds its widest evidence, so nothing is cut that fits alone. */
|
|
39224
|
+
const batched = (items, budget) => {
|
|
39225
|
+
const batches = [];
|
|
39226
|
+
let batch = [];
|
|
39227
|
+
let widest = 0;
|
|
39228
|
+
for (const item of items) {
|
|
39229
|
+
const size = bytes(item.evidence);
|
|
39230
|
+
if (batch.length > 0 && Math.floor(budget / (batch.length + 1)) < Math.max(widest, size)) {
|
|
39231
|
+
batches.push(batch);
|
|
39232
|
+
batch = [];
|
|
39233
|
+
widest = 0;
|
|
39234
|
+
}
|
|
39235
|
+
batch.push(item);
|
|
39236
|
+
widest = Math.max(widest, size);
|
|
39237
|
+
}
|
|
39238
|
+
return batch.length === 0 ? batches : [...batches, batch];
|
|
39239
|
+
};
|
|
39240
|
+
/**
|
|
39241
|
+
* Rates every chunk the reviewer wrote — a walkthrough chunk or a carried
|
|
39242
|
+
* finding — and sorts \`full\` by it, each chunk led by its tier and reason.
|
|
39243
|
+
* gtd's summary chunks are not rated and stay last.
|
|
39244
|
+
*/
|
|
39245
|
+
const ranked = async (review, full) => {
|
|
39246
|
+
const texts = chunkTexts(full);
|
|
39247
|
+
const chunks = reviewChunks(review).map((c, i) => ({
|
|
39248
|
+
id: `C${i + 1}`,
|
|
39249
|
+
title: c.title,
|
|
39250
|
+
evidence: chunkEvidence(texts.get(c.title) ?? "", c.files)
|
|
39251
|
+
}));
|
|
39252
|
+
if (chunks.length === 0) return full;
|
|
39253
|
+
const minP = numeric(vars.reviewSeverityMinP, Infinity);
|
|
39254
|
+
const batches = batched(chunks, numeric(vars.judgeBudgetBytes, 32768));
|
|
39255
|
+
const severities = /* @__PURE__ */ new Map();
|
|
39256
|
+
for (const [i, batch] of batches.entries()) {
|
|
39257
|
+
const judgment = await judge("review.ranking", {
|
|
39258
|
+
questions: batch.flatMap((c) => severityQuestions(c.id, `the change review chunk "${c.id}" ("${c.title}") walks through`, `the change in review chunk "${c.id}"`)),
|
|
39259
|
+
evidence: Object.fromEntries(batch.map((c) => [c.id, c.evidence])),
|
|
39260
|
+
message: reviewRankingMessage(),
|
|
39261
|
+
label: batches.length === 1 ? "Ranking the review" : `Ranking the review (${i + 1}/${batches.length})`
|
|
39262
|
+
});
|
|
39263
|
+
for (const c of batch) severities.set(c.id, severityOf(c, judgment, minP));
|
|
39264
|
+
}
|
|
39265
|
+
return rankedReview(full, chunks.map((c, i) => ({
|
|
39266
|
+
c,
|
|
39267
|
+
i,
|
|
39268
|
+
s: severities.get(c.id)
|
|
39269
|
+
})).sort((x, y) => chunkKey(x.s) - chunkKey(y.s) || x.i - y.i).map(({ c, s }) => ({
|
|
39270
|
+
title: c.title,
|
|
39271
|
+
label: s.reason
|
|
39272
|
+
})));
|
|
39273
|
+
};
|
|
39124
39274
|
const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
39125
39275
|
const composedOf = (base, report, nitFix, answered) => ({
|
|
39126
39276
|
...report,
|
|
@@ -39172,12 +39322,13 @@ const requirePointers = (review, notes, codeChanged) => {
|
|
|
39172
39322
|
const walkthrough = async (base, report, lap, carry) => {
|
|
39173
39323
|
const notes = titledNotes(report);
|
|
39174
39324
|
await reviewing(base, carry, lap, walkthroughBlock(notes, report.nitFix));
|
|
39175
|
-
const
|
|
39176
|
-
if (
|
|
39177
|
-
|
|
39178
|
-
|
|
39179
|
-
|
|
39180
|
-
|
|
39325
|
+
const written = read(REVIEW);
|
|
39326
|
+
if (written === void 0) return;
|
|
39327
|
+
const review = unranked(written);
|
|
39328
|
+
requirePointers(written, notes, changesSince(base).some((c) => isCode$1(c.path)));
|
|
39329
|
+
const full = await ranked(review, withWalkthrough(review, report));
|
|
39330
|
+
if (full !== written) await run("review.carrying", writeScript(REVIEW, full), {
|
|
39331
|
+
label: "Ordering the review and adding the quality lap's findings",
|
|
39181
39332
|
file: REVIEW,
|
|
39182
39333
|
base
|
|
39183
39334
|
});
|
package/package.json
CHANGED
package/src/steering/index.ts
CHANGED
|
@@ -1276,6 +1276,33 @@ describe("review.view", () => {
|
|
|
1276
1276
|
expect(view.nodes[1]!.detail).toBe("")
|
|
1277
1277
|
})
|
|
1278
1278
|
|
|
1279
|
+
it("shows a chunk's leading severity line as its status, not its prose", () => {
|
|
1280
|
+
const view = review.view(
|
|
1281
|
+
[
|
|
1282
|
+
"# Review: abc1234",
|
|
1283
|
+
"",
|
|
1284
|
+
"## Ranked",
|
|
1285
|
+
"",
|
|
1286
|
+
"_critical: security, one-way door_",
|
|
1287
|
+
"",
|
|
1288
|
+
"The new export.",
|
|
1289
|
+
"",
|
|
1290
|
+
"- [ ] ./a.ts#1-1",
|
|
1291
|
+
"",
|
|
1292
|
+
"## Unranked",
|
|
1293
|
+
"",
|
|
1294
|
+
"_emphasis, not a tier_",
|
|
1295
|
+
"",
|
|
1296
|
+
"- [ ] ./a.ts#2-2",
|
|
1297
|
+
"",
|
|
1298
|
+
].join("\n"),
|
|
1299
|
+
)
|
|
1300
|
+
expect(view.nodes.map((c) => [c.status, c.detail])).toEqual([
|
|
1301
|
+
["critical: security, one-way door", "The new export."],
|
|
1302
|
+
[undefined, "_emphasis, not a tier_"],
|
|
1303
|
+
])
|
|
1304
|
+
})
|
|
1305
|
+
|
|
1279
1306
|
it("is built from one parse of the document, not one per element", () => {
|
|
1280
1307
|
const uniqueContent = [
|
|
1281
1308
|
"# Review: def4567",
|
|
@@ -1706,6 +1733,29 @@ describe("reviewNotes", async () => {
|
|
|
1706
1733
|
expect(reviewNotes(base, base)).toEqual([])
|
|
1707
1734
|
})
|
|
1708
1735
|
|
|
1736
|
+
it("an edited tier line is a note", () => {
|
|
1737
|
+
const was = doc("_could_\n\nProse.")
|
|
1738
|
+
const after = doc("_critical: this leaks the token_\n\nProse.")
|
|
1739
|
+
expect(reviewNotes(was, after)).toMatchObject([
|
|
1740
|
+
{
|
|
1741
|
+
kind: "chunk",
|
|
1742
|
+
before: "Prose.\n\n_could_",
|
|
1743
|
+
text: "Prose.\n\n_critical: this leaks the token_",
|
|
1744
|
+
},
|
|
1745
|
+
])
|
|
1746
|
+
})
|
|
1747
|
+
|
|
1748
|
+
it("prose added under a tier is the human's alone, and a leading ! still forces", () => {
|
|
1749
|
+
const was = doc("_could_\n\nProse.")
|
|
1750
|
+
expect(reviewNotes(was, doc("_could_\n\n! Not here.\n\nProse."))).toMatchObject([
|
|
1751
|
+
{ kind: "chunk", text: "! Not here. Prose.", forced: true },
|
|
1752
|
+
])
|
|
1753
|
+
})
|
|
1754
|
+
|
|
1755
|
+
it("a deleted tier line asks nothing", () => {
|
|
1756
|
+
expect(reviewNotes(doc("_could_\n\nProse."), base)).toEqual([])
|
|
1757
|
+
})
|
|
1758
|
+
|
|
1709
1759
|
it("a pointer note added", () => {
|
|
1710
1760
|
const after = doc("Prose.").replace("#1-3", "#1-3 rename this")
|
|
1711
1761
|
expect(reviewNotes(base, after)).toEqual([
|
|
@@ -1863,8 +1913,16 @@ describe("reviewChunks", async () => {
|
|
|
1863
1913
|
"- [ ] _your answer_",
|
|
1864
1914
|
].join("\n")
|
|
1865
1915
|
expect(reviewChunks(content)).toEqual([
|
|
1866
|
-
{
|
|
1867
|
-
|
|
1916
|
+
{
|
|
1917
|
+
title: "Walkthrough",
|
|
1918
|
+
line: 4,
|
|
1919
|
+
pointers: 2,
|
|
1920
|
+
files: [
|
|
1921
|
+
{ path: "./src/a.ts", line: 1, rangeEnd: 3 },
|
|
1922
|
+
{ path: "./src/b.ts", line: 4, rangeEnd: 6 },
|
|
1923
|
+
],
|
|
1924
|
+
},
|
|
1925
|
+
{ title: "Fixed automatically", line: 9, pointers: 0, files: [] },
|
|
1868
1926
|
])
|
|
1869
1927
|
})
|
|
1870
1928
|
})
|