@shiyuphan/follow-me 0.9.0 → 0.9.1
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/dist/src/cli.js +150 -34
- package/dist/src/comments.js +25 -4
- package/dist/src/coverage.js +26 -15
- package/dist/src/host.js +1 -0
- package/dist/src/plan.js +44 -22
- package/dist/src/prompt.js +58 -17
- package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-DztIEwNE.js → abnfDiagram-WEYOIA3C-BWgk01ve.js} +1 -1
- package/dist/ui/dist/assets/{arc-DUYh-EqZ.js → arc-Cg_Qb5c4.js} +1 -1
- package/dist/ui/dist/assets/{architecture-WOLXFQ4H-BQYTB92_.js → architecture-WOLXFQ4H-EdfV1etv.js} +1 -1
- package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-Dl7095A5.js → architectureDiagram-ZHVYKT53-Dz-gytQU.js} +1 -1
- package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-QDZ5kE1J.js → blockDiagram-RJAE7QA3-B7Q5KhYC.js} +1 -1
- package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-CKJZ1xOw.js → c4Diagram-XDPPPE54-Cq9IvGR1.js} +1 -1
- package/dist/ui/dist/assets/channel-Da0YvuQF.js +1 -0
- package/dist/ui/dist/assets/{chunk-2E7WHVMD-BXBjaqd1.js → chunk-2E7WHVMD-BTJB-4C8.js} +1 -1
- package/dist/ui/dist/assets/{chunk-4EMNKR2T-DPzfVuke.js → chunk-4EMNKR2T-BILthc2j.js} +1 -1
- package/dist/ui/dist/assets/{chunk-55XCU76K-B33Sj3w8.js → chunk-55XCU76K-DH3fYrJH.js} +1 -1
- package/dist/ui/dist/assets/{chunk-7S6XSTFO-B_u7dS03.js → chunk-7S6XSTFO-DDIiBvc2.js} +1 -1
- package/dist/ui/dist/assets/{chunk-AQDTSKZB-B4EzH5gx.js → chunk-AQDTSKZB-b1EeJ3gb.js} +1 -1
- package/dist/ui/dist/assets/{chunk-DSNLSHJZ-CfWeHABr.js → chunk-DSNLSHJZ-DZZ635s5.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HAGFOTED-B8G8PjLZ.js → chunk-HAGFOTED-DhhrHTIh.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HNE4Q36P-BA-PqoTF.js → chunk-HNE4Q36P-B8wvhc2e.js} +1 -1
- package/dist/ui/dist/assets/{chunk-NQOKRVIF-JvhuFcnR.js → chunk-NQOKRVIF-CLtGMwDx.js} +1 -1
- package/dist/ui/dist/assets/{chunk-UHW7PFBP-BrfL5CGY.js → chunk-UHW7PFBP-B9fVLGl0.js} +1 -1
- package/dist/ui/dist/assets/{chunk-VQ5W62QI-dBJqgdDy.js → chunk-VQ5W62QI-CgPvvMAv.js} +1 -1
- package/dist/ui/dist/assets/{chunk-WQQL4CDK-dfqd3k_P.js → chunk-WQQL4CDK-DdqKu6R9.js} +1 -1
- package/dist/ui/dist/assets/{chunk-XXDRQBXY-kViWdB9M.js → chunk-XXDRQBXY-GlM07vbp.js} +1 -1
- package/dist/ui/dist/assets/{chunk-YZO67HFG-ukfs4wT3.js → chunk-YZO67HFG-YqT6M0W9.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZAUX2LTX-BxZHki81.js → chunk-ZAUX2LTX-DqHK51Gc.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZCHMQSOR-PjluhR_E.js → chunk-ZCHMQSOR-DEeXkjWz.js} +1 -1
- package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-Ct89LJec.js → classDiagram-v2-4MYWR3UA-BcKsX4up.js} +1 -1
- package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-BnQceGmB.js → cose-bilkent-JH36ORCC-fu9djgYc.js} +1 -1
- package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-D6WkCVE8.js → cynefin-EF2NZ3EQ-CFJASBsJ.js} +1 -1
- package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-dn6DUPAb.js → cynefinDiagram-WF24UDRP-BVJD_n0g.js} +1 -1
- package/dist/ui/dist/assets/{dagre-NK4GIZVX-BtZSlLk7.js → dagre-NK4GIZVX-Cx8pjz9O.js} +1 -1
- package/dist/ui/dist/assets/{diagram-CBK3MWTC-Ds4_tv6X.js → diagram-CBK3MWTC-CVIkEZ1X.js} +1 -1
- package/dist/ui/dist/assets/{diagram-I5XIIV2D-DcoVkg45.js → diagram-I5XIIV2D-2rxpF8BZ.js} +1 -1
- package/dist/ui/dist/assets/{diagram-ODIL2UCW-T47FAA3k.js → diagram-ODIL2UCW-NJfGHnLk.js} +1 -1
- package/dist/ui/dist/assets/{diagram-PFBEJUXE-CRZcaVKP.js → diagram-PFBEJUXE-DZHVJSeM.js} +1 -1
- package/dist/ui/dist/assets/{diagram-RJA3I4VV-D6KXeUPg.js → diagram-RJA3I4VV-CQAfOObI.js} +1 -1
- package/dist/ui/dist/assets/{diagram-TYUNFSNR-BHEIVyNN.js → diagram-TYUNFSNR-DGGaMOl6.js} +1 -1
- package/dist/ui/dist/assets/{dist-DLlKIWBp.js → dist-CW-4NrUq.js} +1 -1
- package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-nUDvYRBV.js → ebnfDiagram-V4P3TTPT-92pb2Fcg.js} +1 -1
- package/dist/ui/dist/assets/{elk-QONTBCTQ-D21BcRzc.js → elk-QONTBCTQ-vu0Z4rYG.js} +1 -1
- package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-CTpHuftU.js → erDiagram-KYQIGQAA-BXnsRzCQ.js} +1 -1
- package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-B6JaKuDY.js → eventmodeling-K75KTNOO-bSr88MT1.js} +1 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-CUH45H9n.js +1 -0
- package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-D2-pMcnC.js → ganttDiagram-R7GPNTJY-BQzVkGbj.js} +1 -1
- package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-B2yxwhEo.js → gitGraph-VSP46ZUC-BegblPQi.js} +1 -1
- package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-DnrhBMH1.js → gitGraphDiagram-PDYL7XHY-BaGlGpEX.js} +1 -1
- package/dist/ui/dist/assets/{index-Dnte9tPd.js → index-D8ErIacA.js} +4 -4
- package/dist/ui/dist/assets/{info-OHQRW6UA-D8oh6gvs.js → info-OHQRW6UA-D3HVGJDc.js} +1 -1
- package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-BLSZjjgB.js → infoDiagram-MVDRKAX5-Ckcp-D0C.js} +1 -1
- package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-BXiznLSn.js → ishikawaDiagram-HPEHV2GE-CWhd0CTS.js} +1 -1
- package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-BSYmVcO_.js → journeyDiagram-BOSHW7O7-CrH5cFXY.js} +1 -1
- package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-COthkkzK.js → kanban-definition-FNSA4HC5-CpORMTDZ.js} +1 -1
- package/dist/ui/dist/assets/{line-6ND8OaOL.js → line-B3El9R0Z.js} +1 -1
- package/dist/ui/dist/assets/{linear-DbWEPFJX.js → linear-DLfthkbB.js} +1 -1
- package/dist/ui/dist/assets/{mermaid-parser.core-Btsk294z.js → mermaid-parser.core-QtQjLaoK.js} +3 -3
- package/dist/ui/dist/assets/{mermaid.core-B4llDSFO.js → mermaid.core-C2DGgMSc.js} +4 -4
- package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-hv2PfX-x.js → mindmap-definition-6OY57UG3-CW1lSXnX.js} +1 -1
- package/dist/ui/dist/assets/{packet-JDAUHWVQ-Dr4vBgjt.js → packet-JDAUHWVQ-Dhu_m_17.js} +1 -1
- package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-DvlFov3N.js → pegDiagram-ECIUJNBX-BGOhbzN4.js} +1 -1
- package/dist/ui/dist/assets/{pie-XZMESJXO-C9QsqC4n.js → pie-XZMESJXO-Citt3cMX.js} +1 -1
- package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-CrrHOj67.js → pieDiagram-GNMPY74T-DPVZikvz.js} +1 -1
- package/dist/ui/dist/assets/{prism-core-Cc8H7LPG.js → prism-core-YgwsYh9d.js} +1 -1
- package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-DQ7J6-ti.js → quadrantDiagram-D57K4MC3-Bwye1B9e.js} +1 -1
- package/dist/ui/dist/assets/{radar-ABXABTNO-BBBPaujh.js → radar-ABXABTNO-DRUb4G6V.js} +1 -1
- package/dist/ui/dist/assets/{railroad-I3PHUGI6-C-P6Igaz.js → railroad-I3PHUGI6-x3Ug2mxX.js} +1 -1
- package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-CAkorRLs.js → railroad-abnf-LNEFI6M7-Dw14XhkZ.js} +1 -1
- package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-D8Hz6gCf.js → railroad-ebnf-DWYD2UWJ-AwoWTWgL.js} +1 -1
- package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-D-XNMkOs.js → railroad-peg-JR7BVNR7-U7MXfciE.js} +1 -1
- package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-DE9LwxJQ.js → railroadDiagram-Y72NRBL7-BxWlCZYs.js} +1 -1
- package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-FrKFY6NJ.js → requirementDiagram-EQYSW5RA-TPmnZguV.js} +1 -1
- package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-BDxI6RDX.js → sankeyDiagram-SY24JHO5-nPGftpl5.js} +1 -1
- package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-pV1XcKX7.js → sequenceDiagram-BLEHNHQU-BdctgaWa.js} +1 -1
- package/dist/ui/dist/assets/{src-snNJTXXq.js → src-zq235FjH.js} +1 -1
- package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-BSc_5aAf.js → stateDiagram-v2-A6IESKWZ-BK_E6kPg.js} +1 -1
- package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-DC6qUXLh.js → swimlanes-B3ENRKFQ-BY50Ne4l.js} +1 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-Ctw23I3i.js +8 -0
- package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-C8L0Dxtu.js → timeline-definition-JXYKGLGB-Bg8AXJ2P.js} +1 -1
- package/dist/ui/dist/assets/{treeView-D4JQ5SDB-DDyZIG5T.js → treeView-D4JQ5SDB-B4hz6M4l.js} +1 -1
- package/dist/ui/dist/assets/{treemap-SAJKECNS-CUaLgge0.js → treemap-SAJKECNS-e_Vc97Ox.js} +1 -1
- package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-BHRBd5pV.js → usecaseDiagram-WPOLBNNX-Butu_LCX.js} +1 -1
- package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-Du4iwwkh.js → vennDiagram-Q3Y3IDCW-BGIyxlbf.js} +1 -1
- package/dist/ui/dist/assets/{wardley-7MLQ67FV-DUgENI0s.js → wardley-7MLQ67FV-Btv95mqq.js} +1 -1
- package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-B86zOdke.js → wardleyDiagram-IVAIDKWA-DwTKExJ4.js} +1 -1
- package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-twq9irqO.js → xychartDiagram-4URNHDU4-UmlA5uxc.js} +1 -1
- package/dist/ui/dist/index.html +1 -1
- package/package.json +1 -1
- package/skills/follow-me/SKILL.md +7 -7
- package/dist/ui/dist/assets/channel-DF08Hd8f.js +0 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-BTeOUR-p.js +0 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-F1HykV3o.js +0 -8
package/dist/src/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { mapOffTip, parseStateMap } from "./comments.js";
|
|
7
|
-
import { changedLines, LcovError, PREVIEW_LINES, parseLcov, parseLocArg, recordRun, runOf, tipPlacer, totalOf, } from "./coverage.js";
|
|
7
|
+
import { changedBranchesTaken, changedLines, LcovError, PREVIEW_LINES, parseLcov, parseLocArg, recordRun, runOf, tipPlacer, totalOf, } from "./coverage.js";
|
|
8
8
|
import { definitionIndex, isIdentifier } from "./definition.js";
|
|
9
9
|
import { DiffParseError, formatUnitRef, isRepoPath, isUnitId, splitLines } from "./diff.js";
|
|
10
10
|
import { BranchRefusedError, blobsAt, branchHead, GitError, isOwnWork, pathsAt, pathsChanged, signing, treeOf, writeBranch, } from "./git.js";
|
|
@@ -20,6 +20,10 @@ import { checkSyntax } from "./syntax.js";
|
|
|
20
20
|
class UsageError extends Error {
|
|
21
21
|
name = "UsageError";
|
|
22
22
|
}
|
|
23
|
+
/** `--help` or `-h` anywhere a command reads its options; main prints that command's usage. */
|
|
24
|
+
class HelpRequested extends Error {
|
|
25
|
+
name = "HelpRequested";
|
|
26
|
+
}
|
|
23
27
|
/** A test run follow-me will not record, with the reason the agent can act on. */
|
|
24
28
|
class CoverageError extends Error {
|
|
25
29
|
name = "CoverageError";
|
|
@@ -47,7 +51,8 @@ Other commands:
|
|
|
47
51
|
the items to post to a merge request
|
|
48
52
|
coverage <lcov> [--test <id> [--at <path:line>]] [--reset]
|
|
49
53
|
record a test run taken at the tip for the page;
|
|
50
|
-
--
|
|
54
|
+
--at is where the test is declared; --reset alone
|
|
55
|
+
clears it before per-test runs
|
|
51
56
|
|
|
52
57
|
help --all lists every command.`;
|
|
53
58
|
const USAGE = `usage: follow-me <command> [options]
|
|
@@ -117,7 +122,8 @@ Interactive loop (while the review page is open):
|
|
|
117
122
|
record the lcov of a test run at the branch tip;
|
|
118
123
|
refuses when a covered file in the working tree
|
|
119
124
|
differs from the tip. A plain run replaces the last;
|
|
120
|
-
--test runs add one test each, --at is
|
|
125
|
+
--test runs add one test each, --at is the line
|
|
126
|
+
where that test is declared.
|
|
121
127
|
--reset starts fresh; alone it clears the last run
|
|
122
128
|
apply, while the page is open, swaps it to the new plan version in place, same URL.
|
|
123
129
|
|
|
@@ -157,6 +163,8 @@ function parseArgs(argv, spec) {
|
|
|
157
163
|
const flags = new Set();
|
|
158
164
|
for (let i = 0; i < argv.length; i++) {
|
|
159
165
|
const arg = argv[i] ?? "";
|
|
166
|
+
if (arg === "--help" || arg === "-h")
|
|
167
|
+
throw new HelpRequested();
|
|
160
168
|
if (!arg.startsWith("--")) {
|
|
161
169
|
positionals.push(arg);
|
|
162
170
|
continue;
|
|
@@ -176,6 +184,43 @@ function parseArgs(argv, spec) {
|
|
|
176
184
|
}
|
|
177
185
|
return { positionals, values, flags };
|
|
178
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* The usage of one command, read from its entries in USAGE: the synopsis, then what it does.
|
|
189
|
+
* Null for a command USAGE does not list.
|
|
190
|
+
*/
|
|
191
|
+
function usageOf(command) {
|
|
192
|
+
const blocks = [];
|
|
193
|
+
let block = null;
|
|
194
|
+
for (const line of USAGE.split("\n")) {
|
|
195
|
+
if (line.startsWith(` ${command} `) || line === ` ${command}`) {
|
|
196
|
+
block = [line];
|
|
197
|
+
blocks.push(block);
|
|
198
|
+
}
|
|
199
|
+
else if (block !== null && line.startsWith(" "))
|
|
200
|
+
block.push(line);
|
|
201
|
+
else
|
|
202
|
+
block = null;
|
|
203
|
+
}
|
|
204
|
+
if (blocks.length === 0)
|
|
205
|
+
return null;
|
|
206
|
+
return blocks
|
|
207
|
+
.map((lines) => {
|
|
208
|
+
const synopsis = [];
|
|
209
|
+
const what = [];
|
|
210
|
+
for (const line of lines) {
|
|
211
|
+
const [left = "", right = ""] = line.trim().split(/\s{3,}/, 2);
|
|
212
|
+
if (/^\s{20,}/.test(line))
|
|
213
|
+
what.push(left);
|
|
214
|
+
else {
|
|
215
|
+
synopsis.push(left);
|
|
216
|
+
if (right !== "")
|
|
217
|
+
what.push(right);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return [`usage: follow-me ${synopsis.join(" ")}`, ...what.map((w) => ` ${w}`)].join("\n");
|
|
221
|
+
})
|
|
222
|
+
.join("\n");
|
|
223
|
+
}
|
|
179
224
|
function say(line = "") {
|
|
180
225
|
process.stdout.write(`${line}\n`);
|
|
181
226
|
}
|
|
@@ -265,45 +310,59 @@ function summarize(session, cwd) {
|
|
|
265
310
|
}
|
|
266
311
|
say(`session ${relative(cwd, session.dir) || "."}`);
|
|
267
312
|
}
|
|
313
|
+
/** Commits are numbered from 1 in everything the agent reads; issues carry 0-based indices. */
|
|
314
|
+
function commitName(k) {
|
|
315
|
+
return `commit ${k + 1}`;
|
|
316
|
+
}
|
|
317
|
+
/** A JSON path into the draft with its commit named from 1: `$.commits[2].units[0]` reads `commit 3, units[0]`. */
|
|
318
|
+
function jsonPlace(at) {
|
|
319
|
+
const m = /^\$\.commits\[(\d+)\]\.?(.*)$/.exec(at);
|
|
320
|
+
if (m === null)
|
|
321
|
+
return at;
|
|
322
|
+
const [, k = "0", rest = ""] = m;
|
|
323
|
+
return rest === "" ? commitName(Number(k)) : `${commitName(Number(k))}, ${rest}`;
|
|
324
|
+
}
|
|
268
325
|
function formatIssue(issue) {
|
|
269
326
|
switch (issue.kind) {
|
|
270
327
|
case "malformed":
|
|
271
|
-
return `malformed ${issue.at}: ${issue.reason}`;
|
|
328
|
+
return `malformed ${jsonPlace(issue.at)}: ${issue.reason}`;
|
|
272
329
|
case "stale":
|
|
273
330
|
return `stale: the plan is for tip ${short(issue.planTip)} but the session tip is ${short(issue.sessionTip)}`;
|
|
274
331
|
case "empty-message":
|
|
275
|
-
return
|
|
332
|
+
return `${commitName(issue.commit)}: empty message`;
|
|
276
333
|
case "empty-commit":
|
|
277
|
-
return
|
|
334
|
+
return `${commitName(issue.commit)}: no units`;
|
|
278
335
|
case "unknown-unit":
|
|
279
|
-
return
|
|
336
|
+
return `${commitName(issue.commit)}: unknown unit ${issue.ref}`;
|
|
280
337
|
case "bad-selection":
|
|
281
|
-
return
|
|
338
|
+
return `${commitName(issue.commit)}: ${issue.unit}: ${issue.reason}`;
|
|
282
339
|
case "duplicate": {
|
|
283
340
|
const [a, b] = issue.commits;
|
|
284
341
|
const what = issue.changes.length > 0 ? lines(issue.unit, issue.changes) : issue.unit;
|
|
285
|
-
return `${what} is claimed by commits ${a} and ${b}`;
|
|
342
|
+
return `${what} is claimed by commits ${a + 1} and ${b + 1}`;
|
|
286
343
|
}
|
|
287
344
|
case "unassigned": {
|
|
288
345
|
const what = issue.changes.length > 0 ? lines(issue.unit, issue.changes) : issue.unit;
|
|
289
346
|
return `${what} (${show(issue.path)}) is in no commit`;
|
|
290
347
|
}
|
|
291
348
|
case "path-conflict":
|
|
292
|
-
return
|
|
349
|
+
return `${commitName(issue.commit)}: ${show(issue.file)} is a file while ${show(issue.under)} exists`;
|
|
293
350
|
case "unknown-response":
|
|
294
351
|
return `response to unknown comment ${issue.comment}`;
|
|
295
352
|
case "bad-answer":
|
|
296
353
|
return `response to ${issue.comment}: ${issue.reason}`;
|
|
354
|
+
case "section-order":
|
|
355
|
+
return `${commitName(issue.commit)}: section ${issue.section} comes after ${commitName(issue.before)} in ${issue.beforeSection}; sections read story, steps, details, wiring, so move ${commitName(issue.commit)} before ${commitName(issue.before)}, or change its section`;
|
|
297
356
|
case "bad-concern":
|
|
298
|
-
return
|
|
357
|
+
return `${commitName(issue.commit)}: concern: ${issue.reason}`;
|
|
299
358
|
case "too-many-concerns":
|
|
300
|
-
return `${issue.unit} has
|
|
359
|
+
return `${commitName(issue.commit)}: ${issue.unit} has ${issue.count} concerns; a unit takes one per commit (each slice of a split hunk may carry its own), so keep the one that matters`;
|
|
301
360
|
case "bad-offer":
|
|
302
|
-
return
|
|
361
|
+
return `${commitName(issue.commit)}: offer: ${issue.reason}`;
|
|
303
362
|
case "too-many-offers":
|
|
304
363
|
return issue.commit === null
|
|
305
364
|
? `the plan has ${issue.count} offers; at most ${OFFERS_PER_PLAN}, and most plans need none`
|
|
306
|
-
:
|
|
365
|
+
: `${commitName(issue.commit)} has ${issue.count} offers; at most ${OFFERS_PER_COMMIT} per commit`;
|
|
307
366
|
case "final-mismatch": {
|
|
308
367
|
const tip = issue.expected ? `${issue.expected.mode} ${issue.expected.blob}` : "absent";
|
|
309
368
|
return `${show(issue.path)}: the last commit has ${issue.got}, the tip has ${tip}`;
|
|
@@ -316,10 +375,7 @@ function formatIssue(issue) {
|
|
|
316
375
|
}
|
|
317
376
|
function formatWarning(w) {
|
|
318
377
|
if (w.kind === "long-title") {
|
|
319
|
-
return `warning:
|
|
320
|
-
}
|
|
321
|
-
if (w.kind === "section-order") {
|
|
322
|
-
return `warning: $.commits[${w.commit}]: section ${w.section} comes after ${w.after}; sections read story, steps, details, wiring`;
|
|
378
|
+
return `warning: ${commitName(w.commit)}: title is ${w.length} characters; keep it to 50`;
|
|
323
379
|
}
|
|
324
380
|
if (w.kind === "question-addressed") {
|
|
325
381
|
return `warning: question ${w.comment} is marked addressed; explain it with "answered" and "at" unless answering it changed the plan`;
|
|
@@ -403,7 +459,7 @@ function syntaxOverride(args) {
|
|
|
403
459
|
return { override };
|
|
404
460
|
}
|
|
405
461
|
function formatSyntax(report, mark = "") {
|
|
406
|
-
const out = report.issues.map((i) => `${mark}
|
|
462
|
+
const out = report.issues.map((i) => `${mark}${commitName(i.commit)}: ${show(i.path)}: ${i.message}`);
|
|
407
463
|
for (const i of report.tip) {
|
|
408
464
|
out.push(`${mark}${show(i.path)}: ${i.message} (the tip itself does not parse here, so earlier commits are not checked for it)`);
|
|
409
465
|
}
|
|
@@ -559,7 +615,7 @@ function projectedCommits(session, version) {
|
|
|
559
615
|
const plan = session.plans().find((p) => p.version === version);
|
|
560
616
|
if (plan === undefined)
|
|
561
617
|
return [];
|
|
562
|
-
const verdict = validatePlan(session.diff, serializePlanFile(plan.file));
|
|
618
|
+
const verdict = validatePlan(session.diff, serializePlanFile(plan.file), { frozen: true });
|
|
563
619
|
return verdict.ok ? verdict.plan.commits : [];
|
|
564
620
|
}
|
|
565
621
|
/** Writes the one-commit-per-file draft with no agent. */
|
|
@@ -930,7 +986,8 @@ function reviewedPlan(session) {
|
|
|
930
986
|
const reviews = reviewsOf(session);
|
|
931
987
|
for (const stored of reviews.toReversed()) {
|
|
932
988
|
const plan = session.plans().find((p) => p.version === stored.version);
|
|
933
|
-
if (plan !== undefined &&
|
|
989
|
+
if (plan !== undefined &&
|
|
990
|
+
validatePlan(session.diff, serializePlanFile(plan.file), { frozen: true }).ok) {
|
|
934
991
|
return stored;
|
|
935
992
|
}
|
|
936
993
|
}
|
|
@@ -1076,11 +1133,42 @@ async function offerStep(port, summary) {
|
|
|
1076
1133
|
return null;
|
|
1077
1134
|
return `${STILL_WORKING}; offer ${first.offer} waits for your diagram: follow-me reply ${first.comment} --diagram <file> [--map <file>], then run follow-me again`;
|
|
1078
1135
|
}
|
|
1136
|
+
/** The per-test step while a state map waits for it, said as a `next:` of the listening loop. */
|
|
1137
|
+
async function testsStep(session, open, cwd) {
|
|
1138
|
+
if (!(await mapWantsTests(session, open)))
|
|
1139
|
+
return null;
|
|
1140
|
+
return `${STILL_WORKING}; ${perTestStep(session, cwd)}; then run follow-me again`;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Where the agent writes lcov files, relative to cwd: inside the session, which `.follow-me/`'s
|
|
1144
|
+
* own .gitignore hides and which goes when the session goes, so no run leaves a file in the repo.
|
|
1145
|
+
*/
|
|
1146
|
+
function lcovDir(session, cwd) {
|
|
1147
|
+
const dir = join(session.dir, "lcov");
|
|
1148
|
+
mkdirSync(dir, { recursive: true });
|
|
1149
|
+
return relative(cwd, dir);
|
|
1150
|
+
}
|
|
1151
|
+
/** The aggregate run to take when a review opens. */
|
|
1152
|
+
function coverageStep(session, cwd) {
|
|
1153
|
+
return `if the project has tests, run them with coverage at the tip, lcov to ${lcovDir(session, cwd)}/all.info, and pass it to follow-me coverage`;
|
|
1154
|
+
}
|
|
1155
|
+
/** The per-test runs a state map needs for its badges and its "Not driven" list. */
|
|
1156
|
+
function perTestStep(session, cwd) {
|
|
1157
|
+
const dir = lcovDir(session, cwd);
|
|
1158
|
+
return `the state machine needs one coverage run per test: follow-me coverage --reset, then for each test run it alone with lcov to ${dir}/<n>.info and follow-me coverage ${dir}/<n>.info --test "<test name>" --at <path:line where it is declared>`;
|
|
1159
|
+
}
|
|
1160
|
+
/** Whether a reply of the hosted review carries a state map while no run has per-test detail. */
|
|
1161
|
+
async function mapWantsTests(session, open) {
|
|
1162
|
+
if ((session.coverage()?.tests.size ?? 0) > 0)
|
|
1163
|
+
return false;
|
|
1164
|
+
const said = await fetchComments(open.port, open.summary);
|
|
1165
|
+
return said.some(({ comments }) => comments.some((c) => (c.replies ?? []).some((r) => r.map !== undefined)));
|
|
1166
|
+
}
|
|
1079
1167
|
/**
|
|
1080
1168
|
* Listens to the hosted review for up to --wait seconds and prints what the reviewer did. It
|
|
1081
1169
|
* returns at once with news, and with the report when the reviewer submitted or ended the review.
|
|
1082
1170
|
*/
|
|
1083
|
-
async function listen(session, open, args) {
|
|
1171
|
+
async function listen(session, open, args, cwd) {
|
|
1084
1172
|
const { port, summary } = open;
|
|
1085
1173
|
const waitMs = waitOf(args) * 1000;
|
|
1086
1174
|
const deadline = Date.now() + waitMs;
|
|
@@ -1136,11 +1224,13 @@ async function listen(session, open, args) {
|
|
|
1136
1224
|
return 0;
|
|
1137
1225
|
}
|
|
1138
1226
|
next((await offerStep(port, summary)) ??
|
|
1227
|
+
(await testsStep(session, open, cwd)) ??
|
|
1139
1228
|
`${STILL_WORKING}; answer with follow-me reply <id> --body "<markdown>" [--diagram file], then run follow-me again`);
|
|
1140
1229
|
return 0;
|
|
1141
1230
|
}
|
|
1142
1231
|
if (left <= 0) {
|
|
1143
1232
|
next((await offerStep(port, summary)) ??
|
|
1233
|
+
(await testsStep(session, open, cwd)) ??
|
|
1144
1234
|
`${STILL_WORKING}; run follow-me again to keep listening`);
|
|
1145
1235
|
return 0;
|
|
1146
1236
|
}
|
|
@@ -1165,7 +1255,7 @@ async function entry(cwd, args) {
|
|
|
1165
1255
|
return swapOpenPage(session, hosted, args);
|
|
1166
1256
|
}
|
|
1167
1257
|
if (hosted !== null)
|
|
1168
|
-
return listen(session, hosted, args);
|
|
1258
|
+
return listen(session, hosted, args, cwd);
|
|
1169
1259
|
const state = draftState(session);
|
|
1170
1260
|
if (state.kind === "invalid") {
|
|
1171
1261
|
next("fix the draft, then run follow-me again");
|
|
@@ -1210,11 +1300,10 @@ async function entry(cwd, args) {
|
|
|
1210
1300
|
const result = await hostReview(session, mode, detached, async () => prep, reportSaved);
|
|
1211
1301
|
if (result.kind !== "elsewhere")
|
|
1212
1302
|
return exitCode(result);
|
|
1213
|
-
if (session.coverage() === null)
|
|
1214
|
-
next(
|
|
1215
|
-
}
|
|
1303
|
+
if (session.coverage() === null)
|
|
1304
|
+
next(coverageStep(session, cwd));
|
|
1216
1305
|
say("follow-me is interactive: keep running it until it prints the report");
|
|
1217
|
-
return listen(session, result, args);
|
|
1306
|
+
return listen(session, result, args, cwd);
|
|
1218
1307
|
}
|
|
1219
1308
|
/** Opens the session, re-extracting instead of failing when the branch moved or --base changed. */
|
|
1220
1309
|
async function openFresh(cwd, args) {
|
|
@@ -1237,7 +1326,10 @@ function walkablePlan(session) {
|
|
|
1237
1326
|
const lastReview = session.lastReview();
|
|
1238
1327
|
const stored = session.plans().at(-1);
|
|
1239
1328
|
if (stored !== undefined) {
|
|
1240
|
-
const verdict = validatePlan(session.diff, serializePlanFile(stored.file), {
|
|
1329
|
+
const verdict = validatePlan(session.diff, serializePlanFile(stored.file), {
|
|
1330
|
+
lastReview,
|
|
1331
|
+
frozen: true,
|
|
1332
|
+
});
|
|
1241
1333
|
if (verdict.ok)
|
|
1242
1334
|
return { version: stored.version, plan: verdict.plan };
|
|
1243
1335
|
}
|
|
@@ -1276,7 +1368,7 @@ async function renderStored(session, stored, format = "markdown") {
|
|
|
1276
1368
|
console.error(`follow-me: review v${stored.version} refers to a plan that is missing`);
|
|
1277
1369
|
return null;
|
|
1278
1370
|
}
|
|
1279
|
-
const verdict = validatePlan(session.diff, serializePlanFile(plan.file));
|
|
1371
|
+
const verdict = validatePlan(session.diff, serializePlanFile(plan.file), { frozen: true });
|
|
1280
1372
|
if (!verdict.ok) {
|
|
1281
1373
|
console.error(`follow-me: plan v${plan.version} no longer fits ${session.anchor.branch}`);
|
|
1282
1374
|
return null;
|
|
@@ -1340,7 +1432,7 @@ async function report(cwd, args, walkOnly = false) {
|
|
|
1340
1432
|
process.stdout.write(text);
|
|
1341
1433
|
return 0;
|
|
1342
1434
|
}
|
|
1343
|
-
const COVERAGE_USAGE = "usage: follow-me coverage <lcov> [--test <id> [--at <path:line>]] [--reset]\n follow-me coverage --reset";
|
|
1435
|
+
const COVERAGE_USAGE = "usage: follow-me coverage <lcov> [--test <id> [--at <path:line>]] [--reset]\n follow-me coverage --reset\n--at is the path:line where the test is declared";
|
|
1344
1436
|
/**
|
|
1345
1437
|
* Records the lcov of a test run the agent made at the tip in the session's coverage.json and
|
|
1346
1438
|
* prints how much of the branch's changed code it ran. follow-me never runs the tests itself.
|
|
@@ -1404,7 +1496,19 @@ async function coverage(cwd, args) {
|
|
|
1404
1496
|
if (open !== null)
|
|
1405
1497
|
await postCoverage(open.port, open.summary);
|
|
1406
1498
|
const { covered, executable } = totalOf(changedLines(session.diff.units, recorded.files).values());
|
|
1407
|
-
|
|
1499
|
+
const total = `coverage for ${plural(recorded.files.size, "file")} at ${short(tip)}; ${covered} of ${executable} changed lines covered`;
|
|
1500
|
+
if (test === undefined)
|
|
1501
|
+
say(total);
|
|
1502
|
+
else {
|
|
1503
|
+
// A per-test run adds to what is on record, so its own share is said apart from the total.
|
|
1504
|
+
const own = totalOf(changedLines(session.diff.units, run.files).values()).covered;
|
|
1505
|
+
const branches = changedBranchesTaken(session.diff.units, run.files);
|
|
1506
|
+
say(`recorded test ${JSON.stringify(test.id)}: ${plural(own, "changed line")}, ${branches === 1 ? "1 branch" : `${branches} branches`}`);
|
|
1507
|
+
say(`running total of ${plural(recorded.tests.size, "test")}: ${total}`);
|
|
1508
|
+
}
|
|
1509
|
+
if (test === undefined && open !== null && (await mapWantsTests(session, open))) {
|
|
1510
|
+
next(`${perTestStep(session, cwd)}; then run follow-me again`);
|
|
1511
|
+
}
|
|
1408
1512
|
return 0;
|
|
1409
1513
|
}
|
|
1410
1514
|
/**
|
|
@@ -1540,7 +1644,11 @@ async function reply(cwd, args) {
|
|
|
1540
1644
|
...(refs.length === 0 ? {} : { refs }),
|
|
1541
1645
|
});
|
|
1542
1646
|
say(`replied ${id} to ${commentId}`);
|
|
1543
|
-
|
|
1647
|
+
if (map !== undefined && (session.coverage()?.tests.size ?? 0) === 0) {
|
|
1648
|
+
next(`${perTestStep(session, cwd)}; then run follow-me again; ${STILL_WORKING}`);
|
|
1649
|
+
}
|
|
1650
|
+
else
|
|
1651
|
+
next(`run follow-me again; ${STILL_WORKING}`);
|
|
1544
1652
|
return 0;
|
|
1545
1653
|
}
|
|
1546
1654
|
const REPLY_USAGE = `usage: follow-me reply <commentId> --body <text> [--diagram <file> [--map <file>]]
|
|
@@ -1824,7 +1932,7 @@ async function showRef(cwd, args) {
|
|
|
1824
1932
|
throw new UsageError(`${ref} is not a unit of the branch now; its hunk changed`);
|
|
1825
1933
|
}
|
|
1826
1934
|
const commits = latest === undefined ? [] : projectedCommits(session, latest.version);
|
|
1827
|
-
process.stdout.write(renderShownUnit(unit, commits));
|
|
1935
|
+
process.stdout.write(renderShownUnit(unit, units, commits));
|
|
1828
1936
|
return 0;
|
|
1829
1937
|
}
|
|
1830
1938
|
if (/^c_[a-z2-7]{6}$/.test(ref) && isCommentId(ref)) {
|
|
@@ -1915,6 +2023,9 @@ async function serverCommand(argv) {
|
|
|
1915
2023
|
await started.host.closed;
|
|
1916
2024
|
return 0;
|
|
1917
2025
|
}
|
|
2026
|
+
case "--help":
|
|
2027
|
+
case "-h":
|
|
2028
|
+
throw new HelpRequested();
|
|
1918
2029
|
default:
|
|
1919
2030
|
throw new UsageError("usage: follow-me server status|stop");
|
|
1920
2031
|
}
|
|
@@ -2136,6 +2247,11 @@ export async function main(argv, cwd) {
|
|
|
2136
2247
|
}
|
|
2137
2248
|
}
|
|
2138
2249
|
catch (error) {
|
|
2250
|
+
if (error instanceof HelpRequested) {
|
|
2251
|
+
const asked = command === undefined || command === "help" ? null : usageOf(command);
|
|
2252
|
+
say(asked ?? SHORT_USAGE);
|
|
2253
|
+
return 0;
|
|
2254
|
+
}
|
|
2139
2255
|
const known = [
|
|
2140
2256
|
UsageError,
|
|
2141
2257
|
CoverageError,
|
package/dist/src/comments.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseLoc } from "./coverage.js";
|
|
2
2
|
import { isRepoPath, isSha, isUnitId } from "./diff.js";
|
|
3
3
|
import { diagramProblem, isArray, isCommentId, isConcernId, isFields, isOfferId, isPlanVersion, isSeverity, parsePlacedConcern, } from "./plan.js";
|
|
4
|
+
import { parseRefText } from "./refs.js";
|
|
4
5
|
/**
|
|
5
6
|
* The wire formats of review state, parsed once at the boundary: comments, replies, concern
|
|
6
7
|
* decisions, the review file, its autosaved draft and the events a hosted session logs. Every
|
|
@@ -49,26 +50,42 @@ function parseAnchor(v) {
|
|
|
49
50
|
}
|
|
50
51
|
return { kind: "hunk", unit: v.unit, commit, changes, offsets };
|
|
51
52
|
}
|
|
53
|
+
/** A place written as a ref string (`path:a-b`, tip lines) or as { path, start, end }. */
|
|
54
|
+
function parsePlace(raw) {
|
|
55
|
+
if (typeof raw !== "string")
|
|
56
|
+
return parseLoc(raw);
|
|
57
|
+
const spec = parseRefText(raw);
|
|
58
|
+
if (spec === null)
|
|
59
|
+
return null;
|
|
60
|
+
if (spec.at !== null)
|
|
61
|
+
return `${JSON.stringify(raw)}: a map is in tip lines; drop the @`;
|
|
62
|
+
return parseLoc({ path: spec.path, start: spec.start, end: spec.end });
|
|
63
|
+
}
|
|
52
64
|
function parseLocs(v, at, needed) {
|
|
53
|
-
const what = `${at}: expected a list of { path, start, end }${needed ? " with at least one place" : ""}`;
|
|
65
|
+
const what = `${at}: expected a list of "path:a-b" or { path, start, end }${needed ? " with at least one place" : ""}`;
|
|
54
66
|
if (!isArray(v) || (needed && v.length === 0))
|
|
55
67
|
return what;
|
|
56
68
|
const out = [];
|
|
57
69
|
for (const [i, raw] of v.entries()) {
|
|
58
|
-
const loc =
|
|
70
|
+
const loc = parsePlace(raw);
|
|
59
71
|
if (loc === null)
|
|
60
|
-
return `${at}[${i}]: expected { path, start, end } with 1 <= start <= end`;
|
|
72
|
+
return `${at}[${i}]: expected "path:a-b" or { path, start, end } with 1 <= start <= end`;
|
|
73
|
+
if (typeof loc === "string")
|
|
74
|
+
return `${at}[${i}]: ${loc}`;
|
|
61
75
|
out.push(loc);
|
|
62
76
|
}
|
|
63
77
|
return out;
|
|
64
78
|
}
|
|
65
79
|
const TRANSITION_ENDS = ["from", "to"];
|
|
80
|
+
/** A stateDiagram's start and end marker. */
|
|
81
|
+
const START_END = "[*]";
|
|
66
82
|
function escapeRegExp(text) {
|
|
67
83
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
68
84
|
}
|
|
69
85
|
/**
|
|
70
86
|
* Boundary parse of a reply's state map. Every state has at least one place, every transition
|
|
71
|
-
* names states of the map, and with a stateDiagram every state name appears in it.
|
|
87
|
+
* names states of the map, and with a stateDiagram every state name appears in it. A transition
|
|
88
|
+
* to or from `[*]` is dropped: the diagram marks the start and end. The reason
|
|
72
89
|
* names the bad entry, as map.states.<name>[i] or map.transitions[i].<field>.
|
|
73
90
|
*/
|
|
74
91
|
export function parseStateMap(v, diagram) {
|
|
@@ -95,6 +112,10 @@ export function parseStateMap(v, diagram) {
|
|
|
95
112
|
const at = `map.transitions[${i}]`;
|
|
96
113
|
if (!isFields(raw))
|
|
97
114
|
return `${at}: expected { from, to, event, at }`;
|
|
115
|
+
// The diagram's [*] already marks where the machine starts and ends; a move to or from it
|
|
116
|
+
// has no code of its own, so it is dropped rather than refused.
|
|
117
|
+
if (raw.from === START_END || raw.to === START_END)
|
|
118
|
+
continue;
|
|
98
119
|
for (const end of TRANSITION_ENDS) {
|
|
99
120
|
const state = raw[end];
|
|
100
121
|
if (typeof state !== "string" || !named.has(state)) {
|
package/dist/src/coverage.js
CHANGED
|
@@ -272,12 +272,8 @@ export function recordRun(prior, run, at) {
|
|
|
272
272
|
function testEntry(ran) {
|
|
273
273
|
return { lines: ran.lines, ...(ran.branches.size === 0 ? {} : { branches: ran.branches }) };
|
|
274
274
|
}
|
|
275
|
-
/**
|
|
276
|
-
|
|
277
|
-
* row is a tip line, which is where the run was taken. Files without such rows are left out.
|
|
278
|
-
*/
|
|
279
|
-
export function changedLines(units, files) {
|
|
280
|
-
const out = new Map();
|
|
275
|
+
/** Calls visit with each added row's tip line, per hunk, for files the run covers. */
|
|
276
|
+
function eachAddedLine(units, files, visit) {
|
|
281
277
|
for (const unit of units.values()) {
|
|
282
278
|
if (unit.kind !== "hunk")
|
|
283
279
|
continue;
|
|
@@ -285,22 +281,37 @@ export function changedLines(units, files) {
|
|
|
285
281
|
if (file === undefined)
|
|
286
282
|
continue;
|
|
287
283
|
let line = tipStartOf(unit, units);
|
|
288
|
-
let { covered, executable } = out.get(unit.path) ?? { covered: 0, executable: 0 };
|
|
289
284
|
for (const row of unit.body) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
executable += 1;
|
|
293
|
-
if (hits > 0)
|
|
294
|
-
covered += 1;
|
|
295
|
-
}
|
|
285
|
+
if (row.op === "+")
|
|
286
|
+
visit(unit.path, file, line);
|
|
296
287
|
if (row.op !== "-")
|
|
297
288
|
line += 1;
|
|
298
289
|
}
|
|
299
|
-
if (executable > 0)
|
|
300
|
-
out.set(unit.path, { covered, executable });
|
|
301
290
|
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Per file, the branch's added rows that lcov calls executable, and how many ran. Every added
|
|
294
|
+
* row is a tip line, which is where the run was taken. Files without such rows are left out.
|
|
295
|
+
*/
|
|
296
|
+
export function changedLines(units, files) {
|
|
297
|
+
const out = new Map();
|
|
298
|
+
eachAddedLine(units, files, (path, file, line) => {
|
|
299
|
+
const hits = file.lines.get(line);
|
|
300
|
+
if (hits === undefined)
|
|
301
|
+
return;
|
|
302
|
+
const { covered, executable } = out.get(path) ?? { covered: 0, executable: 0 };
|
|
303
|
+
out.set(path, { covered: covered + (hits > 0 ? 1 : 0), executable: executable + 1 });
|
|
304
|
+
});
|
|
302
305
|
return out;
|
|
303
306
|
}
|
|
307
|
+
/** How many branches the run took on the branch's added rows, summed over every decision. */
|
|
308
|
+
export function changedBranchesTaken(units, files) {
|
|
309
|
+
let taken = 0;
|
|
310
|
+
eachAddedLine(units, files, (_, file, line) => {
|
|
311
|
+
taken += file.branches.get(line)?.[0] ?? 0;
|
|
312
|
+
});
|
|
313
|
+
return taken;
|
|
314
|
+
}
|
|
304
315
|
export function totalOf(tallies) {
|
|
305
316
|
let covered = 0;
|
|
306
317
|
let executable = 0;
|
package/dist/src/host.js
CHANGED
|
@@ -122,6 +122,7 @@ async function loadHosted(registration, held) {
|
|
|
122
122
|
}
|
|
123
123
|
const verdict = validatePlan(session.diff, serializePlanFile(stored.file), {
|
|
124
124
|
lastReview: session.lastReview(),
|
|
125
|
+
frozen: true,
|
|
125
126
|
});
|
|
126
127
|
if (!verdict.ok) {
|
|
127
128
|
throw new RegistrationError(`plan v${version} no longer fits ${branch}; check it again`);
|
package/dist/src/plan.js
CHANGED
|
@@ -556,12 +556,13 @@ function deriveConcernId(unit, c) {
|
|
|
556
556
|
}
|
|
557
557
|
/**
|
|
558
558
|
* Gives every concern its id and checks where it sits: lines must be change lines the commit
|
|
559
|
-
* takes, and a unit carries at most one concern
|
|
559
|
+
* takes, and a unit carries at most one concern in each commit, so each slice of a hunk split
|
|
560
|
+
* across commits may carry its own.
|
|
560
561
|
*/
|
|
561
562
|
function placeConcerns(diff, file, claims, issues) {
|
|
562
563
|
const ids = new Set();
|
|
563
|
-
const holders = new Map();
|
|
564
564
|
const placedPerCommit = file.commits.map((commit, k) => {
|
|
565
|
+
const held = new Map();
|
|
565
566
|
const placed = [];
|
|
566
567
|
const add = (c, unit) => {
|
|
567
568
|
const id = deriveConcernId(unit, c);
|
|
@@ -578,7 +579,7 @@ function placeConcerns(diff, file, claims, issues) {
|
|
|
578
579
|
const concerns = ref.concerns ?? [];
|
|
579
580
|
if (concerns.length === 0)
|
|
580
581
|
return;
|
|
581
|
-
|
|
582
|
+
held.set(ref.unit, (held.get(ref.unit) ?? 0) + concerns.length);
|
|
582
583
|
const taken = claims.resolved[k]?.[j]?.select;
|
|
583
584
|
const unit = diff.units.get(ref.unit);
|
|
584
585
|
for (const c of concerns) {
|
|
@@ -591,25 +592,25 @@ function placeConcerns(diff, file, claims, issues) {
|
|
|
591
592
|
});
|
|
592
593
|
continue;
|
|
593
594
|
}
|
|
594
|
-
const
|
|
595
|
-
|
|
595
|
+
const takes = taken?.kind === "lines" ? taken : null;
|
|
596
|
+
const outside = takes === null ? [] : lines.filter((n) => !takes.changes.includes(n));
|
|
597
|
+
if (takes !== null && outside.length > 0) {
|
|
596
598
|
issues.push({
|
|
597
599
|
kind: "bad-concern",
|
|
598
600
|
commit: k,
|
|
599
|
-
reason: `${ref.unit}: concern lines ${outside.join(", ")} are not change lines this commit takes`,
|
|
601
|
+
reason: `${ref.unit}: concern lines ${outside.join(", ")} are not change lines this commit takes; \`lines\` are the hunk's [n] as prompt.md shows them, on a slice too, and this commit takes ${formatUnitRef({ unit: ref.unit, select: takes })}`,
|
|
600
602
|
});
|
|
601
603
|
}
|
|
602
604
|
else
|
|
603
605
|
add(c, ref.unit);
|
|
604
606
|
}
|
|
605
607
|
});
|
|
608
|
+
for (const [unit, count] of held) {
|
|
609
|
+
if (count > 1)
|
|
610
|
+
issues.push({ kind: "too-many-concerns", unit, commit: k, count });
|
|
611
|
+
}
|
|
606
612
|
return placed;
|
|
607
613
|
});
|
|
608
|
-
for (const [unit, commits] of holders) {
|
|
609
|
-
if (commits.length > 1) {
|
|
610
|
-
issues.push({ kind: "too-many-concerns", unit, commits: [...new Set(commits)] });
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
614
|
return placedPerCommit;
|
|
614
615
|
}
|
|
615
616
|
/** "o_" + 8 hex over the unit and the label: stable while the offer stays at its place. */
|
|
@@ -744,6 +745,36 @@ function answerProblem(file, r, isQuestion) {
|
|
|
744
745
|
* Parses and checks a draft against the diff: every change line used exactly once, every
|
|
745
746
|
* commit's tree valid, and the last commit equal to the tip. Collects all issues at once.
|
|
746
747
|
*/
|
|
748
|
+
/**
|
|
749
|
+
* Sections read top-down: story, steps, details, wiring. A later section never comes back, so a
|
|
750
|
+
* commit whose section ranks below an earlier commit's must move ahead of the first such commit.
|
|
751
|
+
*/
|
|
752
|
+
function sectionOrder(file) {
|
|
753
|
+
const issues = [];
|
|
754
|
+
const placed = [];
|
|
755
|
+
file.commits.forEach((c, commit) => {
|
|
756
|
+
if (c.section === undefined)
|
|
757
|
+
return;
|
|
758
|
+
const rank = SECTIONS.indexOf(c.section);
|
|
759
|
+
const first = placed.find((p) => p.rank > rank);
|
|
760
|
+
if (first !== undefined) {
|
|
761
|
+
issues.push({
|
|
762
|
+
kind: "section-order",
|
|
763
|
+
commit,
|
|
764
|
+
section: c.section,
|
|
765
|
+
before: first.commit,
|
|
766
|
+
beforeSection: first.section,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
else
|
|
770
|
+
placed.push({ commit, rank, section: c.section });
|
|
771
|
+
});
|
|
772
|
+
return issues;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Judges a draft against the diff. `frozen` re-checks a plan that already passed when it was
|
|
776
|
+
* frozen: section order is a rule for drafts, so a plan frozen before it was one still fits.
|
|
777
|
+
*/
|
|
747
778
|
export function validatePlan(diff, draft, opts = {}) {
|
|
748
779
|
const parsed = parsePlanFile(draft);
|
|
749
780
|
if (Array.isArray(parsed))
|
|
@@ -766,6 +797,8 @@ export function validatePlan(diff, draft, opts = {}) {
|
|
|
766
797
|
issues.push({ kind: "empty-commit", commit: k });
|
|
767
798
|
});
|
|
768
799
|
issues.push(...offerLimits(file));
|
|
800
|
+
if (opts.frozen !== true)
|
|
801
|
+
issues.push(...sectionOrder(file));
|
|
769
802
|
const assignment = [];
|
|
770
803
|
const claims = claim(diff, file, assignment);
|
|
771
804
|
issues.push(...assignment);
|
|
@@ -788,17 +821,6 @@ export function validatePlan(diff, draft, opts = {}) {
|
|
|
788
821
|
if (length > LONG_TITLE)
|
|
789
822
|
warnings.push({ kind: "long-title", commit, length });
|
|
790
823
|
});
|
|
791
|
-
// Sections read top-down: story, steps, details, wiring. A later section never comes back.
|
|
792
|
-
let reached = null;
|
|
793
|
-
file.commits.forEach((c, commit) => {
|
|
794
|
-
if (c.section === undefined)
|
|
795
|
-
return;
|
|
796
|
-
if (reached !== null && SECTIONS.indexOf(c.section) < SECTIONS.indexOf(reached)) {
|
|
797
|
-
warnings.push({ kind: "section-order", commit, section: c.section, after: reached });
|
|
798
|
-
}
|
|
799
|
-
else
|
|
800
|
-
reached = c.section;
|
|
801
|
-
});
|
|
802
824
|
const asked = new Map(review?.comments.map((c) => [c.id, c.kind === "question"]));
|
|
803
825
|
for (const r of file.responses) {
|
|
804
826
|
const isQuestion = asked.get(r.comment);
|