@shiyuphan/follow-me 0.2.0 → 0.3.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/README.md +44 -55
- package/dist/src/cli.js +566 -188
- package/dist/src/comments.js +66 -5
- package/dist/src/git.js +12 -0
- package/dist/src/host.js +19 -8
- package/dist/src/install.js +21 -0
- package/dist/src/plan.js +1 -1
- package/dist/src/prompt.js +343 -39
- package/dist/src/server.js +176 -67
- package/dist/src/session.js +13 -11
- package/dist/ui/dist/assets/abnfDiagram-WEYOIA3C-DcmzRkVZ.js +1 -0
- package/dist/ui/dist/assets/arc-T5QB0T1C.js +1 -0
- package/dist/ui/dist/assets/architecture-WOLXFQ4H-DUIkQTeE.js +1 -0
- package/dist/ui/dist/assets/architectureDiagram-ZHVYKT53-BUYi4-bA.js +36 -0
- package/dist/ui/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/ui/dist/assets/blockDiagram-RJAE7QA3-dyJxtT9e.js +140 -0
- package/dist/ui/dist/assets/c4Diagram-XDPPPE54-CrAgiaZR.js +38 -0
- package/dist/ui/dist/assets/channel-wW7ebm0i.js +1 -0
- package/dist/ui/dist/assets/chunk-2E7WHVMD-C6fAf4Vj.js +88 -0
- package/dist/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/ui/dist/assets/chunk-4EMNKR2T-BLZjwZ4Q.js +1 -0
- package/dist/ui/dist/assets/chunk-55XCU76K-DGLK2Q-L.js +1 -0
- package/dist/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/ui/dist/assets/chunk-7S6XSTFO-BablszTX.js +2 -0
- package/dist/ui/dist/assets/chunk-AQDTSKZB-Cuk3B_vb.js +72 -0
- package/dist/ui/dist/assets/chunk-DSNLSHJZ-DiC78RqE.js +127 -0
- package/dist/ui/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/dist/ui/dist/assets/chunk-HAGFOTED-X_RbvixX.js +62 -0
- package/dist/ui/dist/assets/chunk-HNE4Q36P-C14_x5ck.js +1 -0
- package/dist/ui/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/dist/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/ui/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/dist/ui/dist/assets/chunk-NQOKRVIF-Dxd0dtYR.js +213 -0
- package/dist/ui/dist/assets/chunk-UHW7PFBP-BIxePNUS.js +1 -0
- package/dist/ui/dist/assets/chunk-VQ5W62QI-CKSKLbj4.js +1 -0
- package/dist/ui/dist/assets/chunk-WQQL4CDK-CWNmZGlR.js +10 -0
- package/dist/ui/dist/assets/chunk-XXDRQBXY-D-I5QHgG.js +1 -0
- package/dist/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/ui/dist/assets/chunk-YZO67HFG-DFP68CFd.js +1 -0
- package/dist/ui/dist/assets/chunk-ZAUX2LTX-DY90d4QG.js +1 -0
- package/dist/ui/dist/assets/chunk-ZCHMQSOR-Dlpr-5UC.js +2 -0
- package/dist/ui/dist/assets/classDiagram-v2-4MYWR3UA-CH9uhoLJ.js +217 -0
- package/dist/ui/dist/assets/cose-bilkent-JH36ORCC-DtYHrp8D.js +1 -0
- package/dist/ui/dist/assets/cynefin-EF2NZ3EQ-BtslbtrY.js +1 -0
- package/dist/ui/dist/assets/cynefinDiagram-WF24UDRP-DUU1Jcc7.js +62 -0
- package/dist/ui/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/ui/dist/assets/dagre-NK4GIZVX-CDlP-dRW.js +4 -0
- package/dist/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/ui/dist/assets/diagram-CBK3MWTC-DiV2fn-r.js +24 -0
- package/dist/ui/dist/assets/diagram-I5XIIV2D-Bg5RehGi.js +24 -0
- package/dist/ui/dist/assets/diagram-ODIL2UCW-CFmazz3f.js +30 -0
- package/dist/ui/dist/assets/diagram-PFBEJUXE-uw3ZBqDp.js +3 -0
- package/dist/ui/dist/assets/diagram-RJA3I4VV-uEOYYRYa.js +41 -0
- package/dist/ui/dist/assets/diagram-TYUNFSNR-BN58LF_3.js +200 -0
- package/dist/ui/dist/assets/dist-tWS_lXDS.js +1 -0
- package/dist/ui/dist/assets/ebnfDiagram-V4P3TTPT-B68Tz-w8.js +1 -0
- package/dist/ui/dist/assets/elk-QONTBCTQ-DZZohNea.js +27 -0
- package/dist/ui/dist/assets/erDiagram-KYQIGQAA-BH_m74pv.js +99 -0
- package/dist/ui/dist/assets/eventmodeling-K75KTNOO-CTp5FHos.js +1 -0
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-CsCvexry.js +1 -0
- package/dist/ui/dist/assets/ganttDiagram-R7GPNTJY-BDau7_0f.js +292 -0
- package/dist/ui/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/ui/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/ui/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/ui/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/ui/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/ui/dist/assets/gitGraph-VSP46ZUC-Bv84IoCm.js +1 -0
- package/dist/ui/dist/assets/gitGraphDiagram-PDYL7XHY-DzXcMlm8.js +106 -0
- package/dist/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/ui/dist/assets/index-BTJoqVVN.js +133 -0
- package/dist/ui/dist/assets/index-D60U3smc.css +1 -0
- package/dist/ui/dist/assets/info-OHQRW6UA-222J0O_j.js +1 -0
- package/dist/ui/dist/assets/infoDiagram-MVDRKAX5-DE2sa3Vz.js +2 -0
- package/dist/ui/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/ui/dist/assets/ishikawaDiagram-HPEHV2GE-BJDk0Zcj.js +70 -0
- package/dist/ui/dist/assets/journeyDiagram-BOSHW7O7-JSUXX5Hb.js +139 -0
- package/dist/ui/dist/assets/kanban-definition-FNSA4HC5-Bg79b49M.js +89 -0
- package/dist/ui/dist/assets/katex-B_JcIwqj.js +257 -0
- package/dist/ui/dist/assets/line-Bk5qQSIa.js +1 -0
- package/dist/ui/dist/assets/linear-PFnVMru5.js +1 -0
- package/dist/ui/dist/assets/mermaid-parser.core-Dcxlzc-S.js +7 -0
- package/dist/ui/dist/assets/mermaid.core-TeOHQJ00.js +44 -0
- package/dist/ui/dist/assets/mindmap-definition-6OY57UG3-IaNk8NnW.js +98 -0
- package/dist/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/ui/dist/assets/packet-JDAUHWVQ-DMff5cVK.js +1 -0
- package/dist/ui/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/ui/dist/assets/pegDiagram-ECIUJNBX-66Xu4YRc.js +1 -0
- package/dist/ui/dist/assets/pie-XZMESJXO-GEtzLCIM.js +1 -0
- package/dist/ui/dist/assets/pieDiagram-GNMPY74T-7jMBi-kU.js +39 -0
- package/dist/ui/dist/assets/prism-bash-D6zCJ74D.js +1 -0
- package/dist/ui/dist/assets/prism-c-04YixN25.js +1 -0
- package/dist/ui/dist/assets/prism-clike-DapgWyxx.js +1 -0
- package/dist/ui/dist/assets/prism-core-FDHlWQ43.js +1 -0
- package/dist/ui/dist/assets/prism-cpp-C-lJbC-6.js +1 -0
- package/dist/ui/dist/assets/prism-css-CPgOzxXp.js +1 -0
- package/dist/ui/dist/assets/prism-go-C_8qAH5n.js +1 -0
- package/dist/ui/dist/assets/prism-java-6wtusvIl.js +1 -0
- package/dist/ui/dist/assets/prism-javascript-BnO-swvr.js +1 -0
- package/dist/ui/dist/assets/prism-json-Dp_-W-Hv.js +1 -0
- package/dist/ui/dist/assets/prism-jsx-D-D0NWtC.js +1 -0
- package/dist/ui/dist/assets/prism-kotlin-BUlJgC-o.js +1 -0
- package/dist/ui/dist/assets/prism-markdown-Bc9-36OC.js +1 -0
- package/dist/ui/dist/assets/prism-markup-Cp04rIa1.js +1 -0
- package/dist/ui/dist/assets/prism-markup-templating-KkSMBj6Y.js +1 -0
- package/dist/ui/dist/assets/prism-php-Co4fM40K.js +1 -0
- package/dist/ui/dist/assets/prism-python-CfiSVtM6.js +1 -0
- package/dist/ui/dist/assets/prism-ruby-DFCASkuD.js +1 -0
- package/dist/ui/dist/assets/prism-rust-Bu9N8X3N.js +1 -0
- package/dist/ui/dist/assets/prism-sql-CYhVDMEE.js +1 -0
- package/dist/ui/dist/assets/prism-swift-B0kWc-ne.js +1 -0
- package/dist/ui/dist/assets/prism-toml-DKb54GAT.js +1 -0
- package/dist/ui/dist/assets/prism-tsx-p6J2Kc_4.js +1 -0
- package/dist/ui/dist/assets/prism-typescript-BMqONzxf.js +1 -0
- package/dist/ui/dist/assets/prism-yaml-C0JHu8gr.js +1 -0
- package/dist/ui/dist/assets/quadrantDiagram-D57K4MC3-8hIWayC2.js +7 -0
- package/dist/ui/dist/assets/radar-ABXABTNO-BTdwUOOF.js +1 -0
- package/dist/ui/dist/assets/railroad-I3PHUGI6-DcMbDqBL.js +1 -0
- package/dist/ui/dist/assets/railroad-abnf-LNEFI6M7-gTzUTWeI.js +1 -0
- package/dist/ui/dist/assets/railroad-ebnf-DWYD2UWJ-CNtdUPtk.js +1 -0
- package/dist/ui/dist/assets/railroad-peg-JR7BVNR7-DOiExAtE.js +1 -0
- package/dist/ui/dist/assets/railroadDiagram-Y72NRBL7-2kE_ygvM.js +1 -0
- package/dist/ui/dist/assets/requirementDiagram-EQYSW5RA-CoIAHRq2.js +84 -0
- package/dist/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/ui/dist/assets/sankeyDiagram-SY24JHO5-7p8D3wim.js +40 -0
- package/dist/ui/dist/assets/sequenceDiagram-BLEHNHQU-C6QFnMvn.js +169 -0
- package/dist/ui/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/ui/dist/assets/src-FgijoBVs.js +1 -0
- package/dist/ui/dist/assets/stateDiagram-v2-A6IESKWZ-n9eAb_bT.js +291 -0
- package/dist/ui/dist/assets/swimlanes-B3ENRKFQ-U18mtUzX.js +1 -0
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DX07QVy9.js +8 -0
- package/dist/ui/dist/assets/timeline-definition-JXYKGLGB-CD2Z8QGP.js +120 -0
- package/dist/ui/dist/assets/treeView-D4JQ5SDB-sqUjH5gB.js +1 -0
- package/dist/ui/dist/assets/treemap-SAJKECNS-DvZ38FP7.js +1 -0
- package/dist/ui/dist/assets/usecaseDiagram-WPOLBNNX-CqOGidaZ.js +328 -0
- package/dist/ui/dist/assets/vennDiagram-Q3Y3IDCW-D1SXSBQ5.js +34 -0
- package/dist/ui/dist/assets/wardley-7MLQ67FV-Bpwio-Dx.js +1 -0
- package/dist/ui/dist/assets/wardleyDiagram-IVAIDKWA-JSUXHMnk.js +78 -0
- package/dist/ui/dist/assets/xychartDiagram-4URNHDU4-B4IFqQTN.js +7 -0
- package/dist/ui/dist/index.html +15 -0
- package/dist/ui/dist/style.css +121 -0
- package/package.json +31 -10
- package/skills/follow-me/SKILL.md +15 -89
- package/dist/src/anthropic.js +0 -133
- package/dist/ui/app.js +0 -2201
- package/dist/ui/concerns.js +0 -165
- package/dist/ui/highlight.js +0 -221
- package/dist/ui/index.html +0 -94
- package/dist/ui/live.js +0 -184
- package/dist/ui/playback.js +0 -579
- package/dist/ui/questions.js +0 -189
- package/dist/ui/style.css +0 -1331
- package/dist/ui/summary.js +0 -45
package/dist/src/prompt.js
CHANGED
|
@@ -53,7 +53,7 @@ function renderPreviousPlan(plan) {
|
|
|
53
53
|
})
|
|
54
54
|
.join("\n");
|
|
55
55
|
}
|
|
56
|
-
function describeAnchor(anchor) {
|
|
56
|
+
function describeAnchor(anchor, ctx) {
|
|
57
57
|
switch (anchor.kind) {
|
|
58
58
|
case "plan":
|
|
59
59
|
return "the whole plan";
|
|
@@ -62,7 +62,16 @@ function describeAnchor(anchor) {
|
|
|
62
62
|
case "position":
|
|
63
63
|
return `the position of commit ${anchor.commit + 1} (the reviewer thinks it belongs elsewhere)`;
|
|
64
64
|
case "hunk": {
|
|
65
|
-
const
|
|
65
|
+
const pick = ctx === null ? null : pickRows(ctx, anchor);
|
|
66
|
+
if (pick !== null) {
|
|
67
|
+
const rows = formatRanges(pick.offsets);
|
|
68
|
+
return `${pick.label} in commit ${anchor.commit + 1} (${anchor.unit} rows ${rows}; ${pick.counts})`;
|
|
69
|
+
}
|
|
70
|
+
const where = anchor.offsets !== undefined
|
|
71
|
+
? `rows ${formatRanges(anchor.offsets)}`
|
|
72
|
+
: anchor.changes === null
|
|
73
|
+
? "the whole unit"
|
|
74
|
+
: `change lines ${formatRanges(anchor.changes)}`;
|
|
66
75
|
return `${anchor.unit} (${where}) in commit ${anchor.commit + 1}`;
|
|
67
76
|
}
|
|
68
77
|
default: {
|
|
@@ -71,8 +80,92 @@ function describeAnchor(anchor) {
|
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
/** The hunk around the picked rows with the picked ones marked `>`; empty without offsets. */
|
|
84
|
+
function anchorRows(ctx, anchor) {
|
|
85
|
+
return (ctx === null ? null : pickRows(ctx, anchor))?.rows ?? [];
|
|
86
|
+
}
|
|
87
|
+
const PICK_MARGIN = 3;
|
|
88
|
+
function plural(n, noun) {
|
|
89
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolves offsets against the hunk. Line numbers are the branch tip's, or the base's when a
|
|
93
|
+
* picked row is a removal, since a removed line has no tip number. Rows are shown as the commit
|
|
94
|
+
* writes them: its own changes marked +/-, everything else as context.
|
|
95
|
+
*/
|
|
96
|
+
function pickRows(ctx, anchor) {
|
|
97
|
+
if (anchor.kind !== "hunk" || anchor.offsets === undefined)
|
|
98
|
+
return null;
|
|
99
|
+
const hunk = ctx.units.get(anchor.unit);
|
|
100
|
+
const { offsets } = anchor;
|
|
101
|
+
const first = offsets[0];
|
|
102
|
+
const last = offsets.at(-1);
|
|
103
|
+
if (hunk?.kind !== "hunk" || first === undefined || last === undefined)
|
|
104
|
+
return null;
|
|
105
|
+
if (last >= hunk.body.length)
|
|
106
|
+
return null;
|
|
107
|
+
// Without the projected commit, the hunk is quoted as git shows it.
|
|
108
|
+
const projected = anchor.commit < ctx.commits.length;
|
|
109
|
+
const owners = ownersOf(ctx.commits, hunk);
|
|
110
|
+
const picked = new Set(offsets);
|
|
111
|
+
let tip = tipStartOf(hunk, ctx.units);
|
|
112
|
+
let base = hunk.oldStart + 1;
|
|
113
|
+
const rows = [];
|
|
114
|
+
const lines = [];
|
|
115
|
+
hunk.body.forEach((line, i) => {
|
|
116
|
+
lines.push({ op: line.op, tip, base });
|
|
117
|
+
let op = line.op;
|
|
118
|
+
let shown = true;
|
|
119
|
+
if (projected && line.op !== " ") {
|
|
120
|
+
const owner = owners[line.change] ?? null;
|
|
121
|
+
const applied = owner !== null && owner < anchor.commit;
|
|
122
|
+
if (owner !== anchor.commit) {
|
|
123
|
+
shown = line.op === "+" ? applied : !applied;
|
|
124
|
+
op = " ";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (picked.has(i) && !shown) {
|
|
128
|
+
shown = true;
|
|
129
|
+
op = line.op;
|
|
130
|
+
}
|
|
131
|
+
const inWindow = i >= first - PICK_MARGIN && i <= last + PICK_MARGIN;
|
|
132
|
+
if (shown && inWindow)
|
|
133
|
+
rows.push(`${picked.has(i) ? ">" : " "} ${op}${stripEol(line.text)}`);
|
|
134
|
+
if (line.op !== "-")
|
|
135
|
+
tip += 1;
|
|
136
|
+
if (line.op !== "+")
|
|
137
|
+
base += 1;
|
|
138
|
+
});
|
|
139
|
+
const chosen = offsets.flatMap((i) => lines[i] ?? []);
|
|
140
|
+
const removal = chosen.some((l) => l.op === "-");
|
|
141
|
+
const numbers = removal
|
|
142
|
+
? chosen.filter((l) => l.op !== "+").map((l) => l.base)
|
|
143
|
+
: chosen.map((l) => l.tip);
|
|
144
|
+
const from = Math.min(...numbers);
|
|
145
|
+
const to = Math.max(...numbers);
|
|
146
|
+
const range = from === to ? `${from}` : `${from}-${to}`;
|
|
147
|
+
const count = (op) => chosen.filter((l) => l.op === op).length;
|
|
148
|
+
const counts = [
|
|
149
|
+
count(" ") === 0 ? null : plural(count(" "), "context line"),
|
|
150
|
+
count("+") === 0 ? null : `${count("+")} added`,
|
|
151
|
+
count("-") === 0 ? null : `${count("-")} removed`,
|
|
152
|
+
].filter((part) => part !== null);
|
|
153
|
+
return {
|
|
154
|
+
offsets,
|
|
155
|
+
label: `${hunk.path}:${range}${removal ? " (base side)" : ""}`,
|
|
156
|
+
range,
|
|
157
|
+
baseSide: removal,
|
|
158
|
+
line: from,
|
|
159
|
+
counts: counts.join(", "),
|
|
160
|
+
rows,
|
|
161
|
+
touchesContext: count(" ") > 0,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Said once wherever comments reach the agent, with the same words. */
|
|
165
|
+
const CONTEXT_RULE = "A comment on a context row (marked `>` with no + or -) is about code this branch leaves alone or depends on. The plan cannot move it, so do not move lines because of it. Answer a question with an explanation, or point at the commit that last touched those lines. Answer a change request as a code issue, not by reordering.";
|
|
166
|
+
function renderComment(comment, ctx) {
|
|
167
|
+
const lines = [`- ${comment.id} on ${describeAnchor(comment.anchor, ctx)}`];
|
|
168
|
+
const rows = anchorRows(ctx, comment.anchor);
|
|
76
169
|
if (comment.follows)
|
|
77
170
|
lines.push(` asks again after ${comment.follows}`);
|
|
78
171
|
if (comment.context.commitMessage) {
|
|
@@ -80,7 +173,10 @@ function renderComment(comment) {
|
|
|
80
173
|
}
|
|
81
174
|
if (comment.context.path)
|
|
82
175
|
lines.push(` path: ${comment.context.path}`);
|
|
83
|
-
if (
|
|
176
|
+
if (rows.length > 0) {
|
|
177
|
+
lines.push(" rows asked about, picked ones marked >:", ...rows.map((r) => ` ${r}`));
|
|
178
|
+
}
|
|
179
|
+
else if (comment.context.lines && comment.context.lines.length > 0) {
|
|
84
180
|
lines.push(" lines asked about:", ...comment.context.lines.map((l) => ` ${l}`));
|
|
85
181
|
}
|
|
86
182
|
if (comment.severity)
|
|
@@ -91,8 +187,8 @@ function renderComment(comment) {
|
|
|
91
187
|
}
|
|
92
188
|
return lines.join("\n");
|
|
93
189
|
}
|
|
94
|
-
const QUESTIONS = `A question is not a request to change the plan. Answer it by explaining, not by moving lines. Where the asked-about lines now live, add or rewrite the \`narration\` of that commit or unit so it answers the question, in plain
|
|
95
|
-
const CONCERNS = `You are also a careful reviewer. Add a concern only for something you would raise in a pull request: a bug or unhandled case, a change with no test where one is expected, a name that misleads, or a risky change such as a migration, concurrency or security. Pin it to the change lines with \`lines\`. Say what goes wrong and when, in one or two sentences. Severity: \`blocker\` breaks behaviour or loses data, \`major\` should be fixed before merge, \`minor\` is worth a note. Do not flag style, formatting or taste. Do not repeat the
|
|
190
|
+
const QUESTIONS = `A question is not a request to change the plan. Answer it by explaining, not by moving lines. Where the asked-about lines now live, add or rewrite the \`narration\` (the page shows it as your note) of that commit or unit so it answers the question, in plain sentences. When the answer is about flow, structure or order across files, add a \`diagram\` there too. Then write one response with \`"status": "answered"\`, a \`note\` of one to three sentences that answers the question directly, and \`"at"\` naming the commit (1-based) and unit that carry the explanation. Move lines only if answering shows the order itself was wrong. Say so in the note, and answer the question as well. If the diff cannot answer it, use \`"deferred"\` and say what is missing.`;
|
|
191
|
+
const CONCERNS = `You are also a careful reviewer. Add a concern only for something you would raise in a pull request: a bug or unhandled case, a change with no test where one is expected, a name that misleads, or a risky change such as a migration, concurrency or security. Pin it to the change lines with \`lines\`. Say what goes wrong and when, in one or two sentences. Severity: \`blocker\` breaks behaviour or loses data, \`major\` should be fixed before merge, \`minor\` is worth a note. Do not flag style, formatting or taste. Do not repeat the note. Most commits deserve no concern, and a plan with none is fine. Never write more than one concern per unit. Concerns are in your voice as a reviewer, while the note stays the author's.
|
|
96
192
|
|
|
97
193
|
A concern is \`{ "severity": "blocker" | "major" | "minor", "kind": "bug" | "missing-test" | "naming" | "risk", "text": "...", "lines": [0, 1] }\`. \`lines\` holds change indices of that unit and is allowed only on a unit ref. Write \`"concerns": [ ... ]\` on a commit, or on a unit written as an object: \`{ "unit": "h_x:0-3", "concerns": [ ... ] }\`. Never write an id; follow-me derives one.`;
|
|
98
194
|
const ORDERING = `Order the commits so the story reads naturally:
|
|
@@ -104,7 +200,8 @@ const ORDERING = `Order the commits so the story reads naturally:
|
|
|
104
200
|
- The last commit must bring the tree to exactly the tip, which follows from covering every unit.
|
|
105
201
|
|
|
106
202
|
Write each commit message as a short imperative title, then a blank line and a body that
|
|
107
|
-
says why.
|
|
203
|
+
says why. Put the why in the body first, before anything else. Keep the title to 50
|
|
204
|
+
characters or fewer; put detail in the body.`;
|
|
108
205
|
const GOALS = {
|
|
109
206
|
split: {
|
|
110
207
|
title: "plan the commits",
|
|
@@ -116,17 +213,16 @@ ${ORDERING}`,
|
|
|
116
213
|
},
|
|
117
214
|
walk: {
|
|
118
215
|
title: "tell the story of this branch",
|
|
119
|
-
goal: `The reader is a reviewer meeting this branch for the first time. They will
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
grouped and ordered, and what the author is thinking while writing them.
|
|
216
|
+
goal: `The reader is a reviewer meeting this branch for the first time. They will read it commit
|
|
217
|
+
by commit and comment on the code itself. The final tree is fixed. You decide how its change
|
|
218
|
+
lines are grouped and ordered, and why each step is there.
|
|
123
219
|
|
|
124
220
|
${ORDERING}
|
|
125
221
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
222
|
+
Add a \`narration\` to a commit, or to a unit, only when it says something the commit message
|
|
223
|
+
body does not. The page shows it as your note. A note is one plain paragraph on why, in plain
|
|
224
|
+
text with no markdown. Say why, not what the lines already show. Leave it out when the body
|
|
225
|
+
already says it.`,
|
|
130
226
|
},
|
|
131
227
|
};
|
|
132
228
|
const FORMAT = (draftPath, base, tip) => `Write the plan as JSON to ${draftPath}. Write only the file, in this format:
|
|
@@ -139,7 +235,7 @@ const FORMAT = (draftPath, base, tip) => `Write the plan as JSON to ${draftPath}
|
|
|
139
235
|
{ "message": "Add the Token type\\n\\nThe parser needs it.", "units": ["h_0123456789ab", "h_ba9876543210:0-3,7"] },
|
|
140
236
|
{
|
|
141
237
|
"message": "Parse tokens",
|
|
142
|
-
"narration": "
|
|
238
|
+
"narration": "Tokens are read in one pass, so the loop below never backtracks.",
|
|
143
239
|
"units": [{ "unit": "h_ba9876543210", "narration": "The loop is boring on purpose." }, "f_0123456789ab"]
|
|
144
240
|
}
|
|
145
241
|
],
|
|
@@ -148,19 +244,18 @@ const FORMAT = (draftPath, base, tip) => `Write the plan as JSON to ${draftPath}
|
|
|
148
244
|
]
|
|
149
245
|
}
|
|
150
246
|
|
|
151
|
-
\`narration\` is optional on a commit and on a unit
|
|
152
|
-
\`{ "unit": "<ref>", "narration": "..." }\` instead of the plain ref string.
|
|
247
|
+
\`narration\` is optional on a commit and on a unit, and the page shows it as your note. A unit
|
|
248
|
+
with a note is written as \`{ "unit": "<ref>", "narration": "..." }\` instead of the plain ref string.
|
|
153
249
|
|
|
154
250
|
\`diagram\` is optional on a commit and on a unit, written the same way:
|
|
155
251
|
\`{ "unit": "<ref>", "diagram": "..." }\`. It is Mermaid source that the review page draws beside
|
|
156
|
-
the code: a commit's diagram
|
|
157
|
-
unit. Add one only where a picture says what the code and prose cannot: data flowing between
|
|
252
|
+
the code: a commit's diagram under its commit, a unit's under its hunk. Add one only where a picture says what the code and prose cannot: data flowing between
|
|
158
253
|
modules, a state machine, a call graph, or the before and after shape of a structure. Most commits
|
|
159
254
|
need none. The first line names the diagram type, for example:
|
|
160
255
|
|
|
161
256
|
"diagram": "flowchart LR\\n parse --> validate --> apply"
|
|
162
257
|
|
|
163
|
-
|
|
258
|
+
The note stays plain text even when a diagram is present.
|
|
164
259
|
\`responses\` is [] when there are no review comments. \`status\` is one of "addressed",
|
|
165
260
|
"declined", "deferred" or, for a question, "answered", and \`note\` says what you did or why not.
|
|
166
261
|
An "answered" response also carries \`"at": { "commit": 3, "unit": "h_x" }\`: the commit, counted from 1,
|
|
@@ -188,7 +283,7 @@ export function renderPrompt(input) {
|
|
|
188
283
|
const sections = [
|
|
189
284
|
...head,
|
|
190
285
|
`## Concerns\n\n${CONCERNS}`,
|
|
191
|
-
`## Questions from the reviewer\n\n${QUESTIONS}`,
|
|
286
|
+
`## Questions from the reviewer\n\n${QUESTIONS}\n\n${CONTEXT_RULE}`,
|
|
192
287
|
units,
|
|
193
288
|
];
|
|
194
289
|
return `${sections.join("\n\n")}\n`;
|
|
@@ -198,11 +293,13 @@ export function renderPrompt(input) {
|
|
|
198
293
|
if (previousPlan) {
|
|
199
294
|
sections.push(`## Previous plan\n\nCommits are numbered from 1.\n\n${renderPreviousPlan(previousPlan)}`);
|
|
200
295
|
}
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const
|
|
296
|
+
const theirs = review?.comments.filter((c) => c.author !== "agent") ?? [];
|
|
297
|
+
const asked = theirs.filter((c) => c.kind === "question");
|
|
298
|
+
const changes = theirs.filter((c) => c.kind !== "question");
|
|
299
|
+
const ctx = { commits: input.previousCommits ?? [], units: diff.units };
|
|
300
|
+
const questions = [`## Questions from the reviewer\n`, QUESTIONS, `\n${CONTEXT_RULE}`];
|
|
204
301
|
if (review && asked.length > 0) {
|
|
205
|
-
questions.push(`\nThe reviewer asked ${asked.length === 1 ? "this question" : "these questions"} about plan v${review.planVersion}. Commits are numbered from 1. Write one response for each.\n`, asked.map(renderComment).join("\n"));
|
|
302
|
+
questions.push(`\nThe reviewer asked ${asked.length === 1 ? "this question" : "these questions"} about plan v${review.planVersion}. Commits are numbered from 1. Write one response for each.\n`, asked.map((c) => renderComment(c, ctx)).join("\n"));
|
|
206
303
|
}
|
|
207
304
|
sections.push(questions.join("\n"));
|
|
208
305
|
if (review && changes.length > 0) {
|
|
@@ -210,7 +307,7 @@ export function renderPrompt(input) {
|
|
|
210
307
|
`## Open review comments\n`,
|
|
211
308
|
`A reviewer commented on plan v${review.planVersion}. Commits are numbered from 1.`,
|
|
212
309
|
"Answer every comment below with one entry in `responses`, changing the plan where you agree.\n",
|
|
213
|
-
changes.map(renderComment).join("\n"),
|
|
310
|
+
changes.map((c) => renderComment(c, ctx)).join("\n"),
|
|
214
311
|
].join("\n"));
|
|
215
312
|
}
|
|
216
313
|
return `${sections.join("\n\n")}\n`;
|
|
@@ -299,6 +396,15 @@ function whereComment(ctx, c) {
|
|
|
299
396
|
case "position":
|
|
300
397
|
return { path: null, line: null, label: `commit ${anchor.commit + 1} position`, quote: [] };
|
|
301
398
|
case "hunk": {
|
|
399
|
+
const pick = pickRows(ctx, anchor);
|
|
400
|
+
if (pick !== null) {
|
|
401
|
+
return {
|
|
402
|
+
path: ctx.units.get(anchor.unit)?.path ?? c.context.path,
|
|
403
|
+
line: pick.line,
|
|
404
|
+
label: `${pick.range.includes("-") ? "lines" : "line"} ${pick.range}${pick.baseSide ? " (base side)" : ""}`,
|
|
405
|
+
quote: pick.rows,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
302
408
|
const quoted = quoteHunk(ctx.units, ctx.commits, anchor.unit, anchor.commit, anchor.changes);
|
|
303
409
|
const path = ctx.units.get(anchor.unit)?.path ?? c.context.path;
|
|
304
410
|
if (quoted === null) {
|
|
@@ -514,10 +620,24 @@ function renderQuestions(ctx, questions, responses) {
|
|
|
514
620
|
}
|
|
515
621
|
return out;
|
|
516
622
|
}
|
|
623
|
+
/** Threads the agent opened, with what the reviewer said back. */
|
|
624
|
+
function renderThreads(ctx, threads) {
|
|
625
|
+
const out = ["## Threads you started", ""];
|
|
626
|
+
for (const c of threads) {
|
|
627
|
+
const where = whereComment(ctx, c);
|
|
628
|
+
const place = where.path === null ? where.label : `${where.path} · ${where.label}`;
|
|
629
|
+
out.push(`### ${c.id} · ${place}`, "", ...quoteBlock(where.quote));
|
|
630
|
+
if (where.quote.length > 0)
|
|
631
|
+
out.push("");
|
|
632
|
+
out.push(`Agent: ${c.body}`, ...renderReplies(c), "");
|
|
633
|
+
}
|
|
634
|
+
return out;
|
|
635
|
+
}
|
|
517
636
|
/** Plan feedback for a split review: what the planner answers through `responses`. */
|
|
518
|
-
function renderPlanFeedback(
|
|
637
|
+
function renderPlanFeedback(ctx, comments) {
|
|
638
|
+
const { commits } = ctx;
|
|
519
639
|
const out = [];
|
|
520
|
-
const line = (c) => [`- ${c.id} on ${describeAnchor(c.anchor)}`];
|
|
640
|
+
const line = (c) => [`- ${c.id} on ${describeAnchor(c.anchor, ctx)}`];
|
|
521
641
|
const planLevel = comments.filter((c) => c.anchor.kind === "plan");
|
|
522
642
|
if (planLevel.length > 0) {
|
|
523
643
|
out.push("## Whole plan", "");
|
|
@@ -535,6 +655,9 @@ function renderPlanFeedback(commits, comments) {
|
|
|
535
655
|
out.push(...line(c));
|
|
536
656
|
if (c.context.path)
|
|
537
657
|
out.push(` - file: ${c.context.path}`);
|
|
658
|
+
const rows = anchorRows(ctx, c.anchor);
|
|
659
|
+
if (rows.length > 0)
|
|
660
|
+
out.push(" - picked rows are marked >:", ...rows.map((r) => ` ${r}`));
|
|
538
661
|
out.push(` - ${c.body.split("\n").join("\n ")}`);
|
|
539
662
|
for (const reply of renderReplies(c))
|
|
540
663
|
out.push(` - ${reply}`);
|
|
@@ -549,6 +672,155 @@ function renderPlanFeedback(commits, comments) {
|
|
|
549
672
|
}
|
|
550
673
|
return out;
|
|
551
674
|
}
|
|
675
|
+
/** The rows `want` picks, numbered as the quoting code numbers them. */
|
|
676
|
+
function numberedRows(hunk, units, want) {
|
|
677
|
+
let tip = tipStartOf(hunk, units);
|
|
678
|
+
let base = hunk.oldStart + 1;
|
|
679
|
+
const rows = [];
|
|
680
|
+
hunk.body.forEach((line, i) => {
|
|
681
|
+
if (want(i, line.op === " " ? null : line.change))
|
|
682
|
+
rows.push({ op: line.op, tip, base });
|
|
683
|
+
if (line.op !== "-")
|
|
684
|
+
tip += 1;
|
|
685
|
+
if (line.op !== "+")
|
|
686
|
+
base += 1;
|
|
687
|
+
});
|
|
688
|
+
return rows;
|
|
689
|
+
}
|
|
690
|
+
const NO_SPAN = { newLine: null, oldLine: null, startNewLine: null, startOldLine: null };
|
|
691
|
+
/** The last row names the line: both sides for context, the tip for an addition, the base for a removal. */
|
|
692
|
+
function spanOf(rows) {
|
|
693
|
+
const first = rows[0];
|
|
694
|
+
const last = rows.at(-1);
|
|
695
|
+
if (first === undefined || last === undefined)
|
|
696
|
+
return NO_SPAN;
|
|
697
|
+
const range = rows.length > 1;
|
|
698
|
+
return {
|
|
699
|
+
newLine: last.op === "-" ? null : last.tip,
|
|
700
|
+
oldLine: last.op === "+" ? null : last.base,
|
|
701
|
+
startNewLine: range && first.op !== "-" ? first.tip : null,
|
|
702
|
+
startOldLine: range && first.op !== "+" ? first.base : null,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function commentSpan(ctx, c) {
|
|
706
|
+
const { anchor } = c;
|
|
707
|
+
if (anchor.kind !== "hunk")
|
|
708
|
+
return NO_SPAN;
|
|
709
|
+
const hunk = ctx.units.get(anchor.unit);
|
|
710
|
+
if (hunk?.kind !== "hunk")
|
|
711
|
+
return NO_SPAN;
|
|
712
|
+
const { offsets, changes } = anchor;
|
|
713
|
+
if (offsets !== undefined)
|
|
714
|
+
return spanOf(numberedRows(hunk, ctx.units, (i) => offsets.includes(i)));
|
|
715
|
+
return spanOf(numberedRows(hunk, ctx.units, (_, n) => n !== null && (changes === null || changes.includes(n))));
|
|
716
|
+
}
|
|
717
|
+
function concernSpan(ctx, concern) {
|
|
718
|
+
const hunk = concern.unit === undefined ? undefined : ctx.units.get(concern.unit);
|
|
719
|
+
if (hunk?.kind !== "hunk")
|
|
720
|
+
return NO_SPAN;
|
|
721
|
+
const lines = concern.lines;
|
|
722
|
+
return spanOf(numberedRows(hunk, ctx.units, (_, n) => n !== null && (lines === undefined || lines.includes(n))));
|
|
723
|
+
}
|
|
724
|
+
function threadOf(c) {
|
|
725
|
+
return (c.replies ?? []).map((r) => ({ author: r.author, body: r.body }));
|
|
726
|
+
}
|
|
727
|
+
function withSeverity(severity, text) {
|
|
728
|
+
return severity === null ? text : `**${severity}**\n\n${text}`;
|
|
729
|
+
}
|
|
730
|
+
function collectItems(review, commits, units, responses) {
|
|
731
|
+
const ctx = { commits, units };
|
|
732
|
+
const fromReviewer = review.comments.filter((c) => c.author !== "agent");
|
|
733
|
+
const walk = review.mode === "walk";
|
|
734
|
+
const rest = fromReviewer.filter((c) => c.kind !== "question");
|
|
735
|
+
const issues = walk ? rest : rest.filter((c) => c.severity !== undefined);
|
|
736
|
+
const noComments = new Set(review.excluded?.comments ?? []);
|
|
737
|
+
const noConcerns = new Set(review.excluded?.concerns ?? []);
|
|
738
|
+
const resolved = new Set(review.resolved ?? []);
|
|
739
|
+
const out = buildItems(ctx, keptConcerns(review, commits), issues).map((it) => {
|
|
740
|
+
const { kept } = it;
|
|
741
|
+
const first = it.comments[0];
|
|
742
|
+
if (kept !== null) {
|
|
743
|
+
const note = kept.note === null ? "" : `\n\nReviewer: ${kept.note}`;
|
|
744
|
+
const thread = it.comments.flatMap((c) => [
|
|
745
|
+
{ author: "reviewer", body: c.body },
|
|
746
|
+
...threadOf(c),
|
|
747
|
+
]);
|
|
748
|
+
return {
|
|
749
|
+
where: it.where,
|
|
750
|
+
item: {
|
|
751
|
+
...concernSpan(ctx, kept.concern),
|
|
752
|
+
path: it.where.path,
|
|
753
|
+
oldPath: it.where.path,
|
|
754
|
+
severity: it.severity,
|
|
755
|
+
kind: "concern",
|
|
756
|
+
body: withSeverity(it.severity, `${kept.concern.text}${note}`),
|
|
757
|
+
thread,
|
|
758
|
+
resolved: false,
|
|
759
|
+
excluded: noConcerns.has(kept.concern.id),
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
if (first === undefined)
|
|
764
|
+
throw new Error("an item with neither a concern nor a comment");
|
|
765
|
+
return {
|
|
766
|
+
where: it.where,
|
|
767
|
+
item: {
|
|
768
|
+
...commentSpan(ctx, first),
|
|
769
|
+
path: it.where.path,
|
|
770
|
+
oldPath: it.where.path,
|
|
771
|
+
severity: it.severity,
|
|
772
|
+
kind: "flag",
|
|
773
|
+
body: withSeverity(it.severity, first.body),
|
|
774
|
+
thread: threadOf(first),
|
|
775
|
+
resolved: resolved.has(first.id),
|
|
776
|
+
excluded: noComments.has(first.id),
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
});
|
|
780
|
+
for (const q of fromReviewer.filter((c) => c.kind === "question")) {
|
|
781
|
+
const where = whereComment(ctx, q);
|
|
782
|
+
const answer = responses.find((r) => r.comment === q.id)?.note;
|
|
783
|
+
const lastAgent = (q.replies ?? []).findLast((r) => r.author === "agent")?.body;
|
|
784
|
+
const said = answer ?? lastAgent;
|
|
785
|
+
const details = said === undefined ? "" : `\n\n<details><summary>Answer</summary>\n\n${said}\n\n</details>`;
|
|
786
|
+
out.push({
|
|
787
|
+
where,
|
|
788
|
+
item: {
|
|
789
|
+
...commentSpan(ctx, q),
|
|
790
|
+
path: where.path,
|
|
791
|
+
oldPath: where.path,
|
|
792
|
+
severity: q.severity ?? null,
|
|
793
|
+
kind: "question",
|
|
794
|
+
body: `${q.body}${details}`,
|
|
795
|
+
thread: threadOf(q),
|
|
796
|
+
resolved: resolved.has(q.id),
|
|
797
|
+
excluded: noComments.has(q.id),
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
return sortItems(out, units);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* The items of a review for the merge request, in file order then line order, general items last.
|
|
805
|
+
* Excluded ones are present and marked; whoever posts them skips those.
|
|
806
|
+
*/
|
|
807
|
+
export function reportItems(review, commits, units, opts) {
|
|
808
|
+
return collectItems(review, commits, units, opts.responses).map((c) => c.item);
|
|
809
|
+
}
|
|
810
|
+
/** A short list of what the reviewer left out, so nothing vanishes without a trace. */
|
|
811
|
+
function renderExcluded(collected) {
|
|
812
|
+
const left = collected.filter((c) => c.item.excluded);
|
|
813
|
+
if (left.length === 0)
|
|
814
|
+
return [];
|
|
815
|
+
const out = ["## Excluded", "", "The reviewer left these out of the hand-off:", ""];
|
|
816
|
+
for (const { item, where } of left) {
|
|
817
|
+
const place = where.path === null ? where.label : `${where.path} · ${where.label}`;
|
|
818
|
+
const first = item.body.split("\n").find((l) => l !== "" && !l.startsWith("**")) ?? "";
|
|
819
|
+
out.push(`- ${place} · ${item.kind}${item.severity === null ? "" : ` · ${item.severity}`}: ${first}`);
|
|
820
|
+
}
|
|
821
|
+
out.push("");
|
|
822
|
+
return out;
|
|
823
|
+
}
|
|
552
824
|
/**
|
|
553
825
|
* The consolidated report of a review, in the voices of both the reviewer and the agent: the
|
|
554
826
|
* reviewer's comments and the agent's concerns the reviewer kept, grouped by file and ordered by
|
|
@@ -556,11 +828,29 @@ function renderPlanFeedback(commits, comments) {
|
|
|
556
828
|
* split review is plan feedback for the planner, followed by the same code issues and questions.
|
|
557
829
|
* Concerns the reviewer has not decided are left out, as are those dismissed.
|
|
558
830
|
*/
|
|
559
|
-
export function renderReport(
|
|
831
|
+
export function renderReport(full, commits, units, opts) {
|
|
832
|
+
const excluded = full.excluded;
|
|
833
|
+
if (excluded === undefined)
|
|
834
|
+
return renderIncluded(full, commits, units, opts);
|
|
835
|
+
const noComments = new Set(excluded.comments);
|
|
836
|
+
const noConcerns = new Set(excluded.concerns);
|
|
837
|
+
const review = {
|
|
838
|
+
...full,
|
|
839
|
+
comments: full.comments.filter((c) => !noComments.has(c.id)),
|
|
840
|
+
concerns: (full.concerns ?? []).filter((d) => !noConcerns.has(d.concern)),
|
|
841
|
+
};
|
|
842
|
+
const tail = renderExcluded(collectItems(full, commits, units, opts.responses));
|
|
843
|
+
const main = renderIncluded(review, commits, units, opts);
|
|
844
|
+
return tail.length === 0 ? main : `${main}${main.endsWith("\n") ? "" : "\n"}${tail.join("\n")}`;
|
|
845
|
+
}
|
|
846
|
+
function renderIncluded(review, commits, units, opts) {
|
|
560
847
|
const ctx = { commits, units };
|
|
561
848
|
const walk = review.mode === "walk";
|
|
562
|
-
const
|
|
563
|
-
|
|
849
|
+
const fromReviewer = review.comments.filter((c) => c.author !== "agent");
|
|
850
|
+
// A thread the agent started matters to it only once the reviewer has answered.
|
|
851
|
+
const started = review.comments.filter((c) => c.author === "agent" && (c.replies ?? []).some((r) => r.author === "reviewer"));
|
|
852
|
+
const questions = fromReviewer.filter((c) => c.kind === "question");
|
|
853
|
+
const rest = fromReviewer.filter((c) => c.kind !== "question");
|
|
564
854
|
const issues = walk ? rest : rest.filter((c) => c.severity !== undefined);
|
|
565
855
|
const feedback = walk ? [] : rest.filter((c) => c.severity === undefined);
|
|
566
856
|
const items = buildItems(ctx, keptConcerns(review, commits), issues);
|
|
@@ -573,36 +863,47 @@ export function renderReport(review, commits, units, opts) {
|
|
|
573
863
|
}
|
|
574
864
|
else {
|
|
575
865
|
out.push(`# Review of plan v${review.planVersion}`, "");
|
|
576
|
-
if (feedback.length === 0 &&
|
|
866
|
+
if (feedback.length === 0 &&
|
|
867
|
+
items.length === 0 &&
|
|
868
|
+
questions.length === 0 &&
|
|
869
|
+
started.length === 0) {
|
|
577
870
|
out.push("No comments.", "");
|
|
578
871
|
return out.join("\n");
|
|
579
872
|
}
|
|
580
|
-
out.push(...renderPlanFeedback(
|
|
873
|
+
out.push(...renderPlanFeedback(ctx, feedback));
|
|
581
874
|
if (items.length > 0) {
|
|
582
875
|
out.push(`## Code issues (${summarizeIssues(items)})`, "", ...renderItems(items, units, 3));
|
|
583
876
|
}
|
|
584
877
|
}
|
|
585
878
|
if (questions.length > 0)
|
|
586
879
|
out.push(...renderQuestions(ctx, questions, opts.responses));
|
|
880
|
+
if (started.length > 0)
|
|
881
|
+
out.push(...renderThreads(ctx, started));
|
|
587
882
|
if (walk && items.length > 0) {
|
|
588
883
|
out.push("Address each issue above in the code on this branch, then tell the reviewer what you changed and what you chose not to change and why.", "");
|
|
589
884
|
}
|
|
590
885
|
if (!walk && (feedback.length > 0 || questions.length > 0)) {
|
|
591
886
|
out.push("Answer every comment id above in `responses` of the next draft, then run `follow-me check` and `follow-me apply`.", "");
|
|
592
887
|
}
|
|
888
|
+
const onContext = review.comments.some((c) => pickRows(ctx, c.anchor)?.touchesContext);
|
|
889
|
+
if (onContext)
|
|
890
|
+
out.push(CONTEXT_RULE, "");
|
|
593
891
|
if (!walk && items.length > 0) {
|
|
594
892
|
out.push("Fix the code issues on the original branch, then run `follow-me extract` again.", "");
|
|
595
893
|
}
|
|
596
894
|
return out.join("\n");
|
|
597
895
|
}
|
|
598
|
-
function renderEvent(event) {
|
|
896
|
+
function renderEvent(event, ctx) {
|
|
599
897
|
switch (event.type) {
|
|
600
898
|
case "comment":
|
|
601
899
|
case "comment-updated": {
|
|
602
900
|
const { comment: c } = event;
|
|
603
901
|
const what = c.kind === "question" ? "question" : "comment";
|
|
604
902
|
const verb = event.type === "comment" ? "new" : "edited";
|
|
605
|
-
const lines = [`- ${verb} ${what} ${c.id} on ${describeAnchor(c.anchor)}`];
|
|
903
|
+
const lines = [`- ${verb} ${what} ${c.id} on ${describeAnchor(c.anchor, ctx)}`];
|
|
904
|
+
const rows = anchorRows(ctx, c.anchor);
|
|
905
|
+
if (rows.length > 0)
|
|
906
|
+
lines.push(...rows.map((r) => ` ${r}`));
|
|
606
907
|
if (c.context.path)
|
|
607
908
|
lines.push(` - file: ${c.context.path}`);
|
|
608
909
|
if (c.severity)
|
|
@@ -625,6 +926,8 @@ function renderEvent(event) {
|
|
|
625
926
|
return [`- question ${event.commentId} marked ${event.resolved ? "resolved" : "open again"}`];
|
|
626
927
|
case "version":
|
|
627
928
|
return [`- plan v${event.version} is now open`];
|
|
929
|
+
case "submitted":
|
|
930
|
+
return ["- the reviewer submitted the review; the held flags follow as new comments"];
|
|
628
931
|
case "ended":
|
|
629
932
|
return [`- the reviewer ended the review; the report is next to ${event.review}`];
|
|
630
933
|
default: {
|
|
@@ -634,9 +937,10 @@ function renderEvent(event) {
|
|
|
634
937
|
}
|
|
635
938
|
}
|
|
636
939
|
/** A markdown digest of what happened on the review page, for `follow-me watch`. */
|
|
637
|
-
export function renderEvents(events) {
|
|
940
|
+
export function renderEvents(events, ctx = null) {
|
|
638
941
|
const first = events[0]?.seq;
|
|
639
942
|
const last = events.at(-1)?.seq;
|
|
640
943
|
const range = first === last ? `event ${first}` : `events ${first}-${last}`;
|
|
641
|
-
|
|
944
|
+
const body = events.flatMap((event) => renderEvent(event, ctx));
|
|
945
|
+
return [`# Review activity (${range})`, "", ...body, ""].join("\n");
|
|
642
946
|
}
|