@shiyuphan/follow-me 0.7.1 → 0.8.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 +3 -0
- package/dist/src/cli.js +128 -24
- package/dist/src/comments.js +146 -7
- package/dist/src/coverage.js +16 -2
- package/dist/src/host.js +23 -0
- package/dist/src/plan.js +128 -6
- package/dist/src/prompt.js +55 -2
- package/dist/src/server.js +65 -3
- package/dist/src/session.js +1 -0
- package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-cHEJvcBn.js → abnfDiagram-WEYOIA3C-Bp14ygmz.js} +1 -1
- package/dist/ui/dist/assets/{arc-xolFyc0v.js → arc-DUR6uzhI.js} +1 -1
- package/dist/ui/dist/assets/{architecture-WOLXFQ4H-B3hvvNV2.js → architecture-WOLXFQ4H-CyfnP65O.js} +1 -1
- package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-BNTMbPJW.js → architectureDiagram-ZHVYKT53-dJTicIk2.js} +1 -1
- package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-NYY6qeuY.js → blockDiagram-RJAE7QA3-BYiqh5Ib.js} +1 -1
- package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-B_bNpXHQ.js → c4Diagram-XDPPPE54-DHHPQ7xE.js} +1 -1
- package/dist/ui/dist/assets/channel-CY2cQobm.js +1 -0
- package/dist/ui/dist/assets/{chunk-2E7WHVMD-BTet9STn.js → chunk-2E7WHVMD-BODm_awL.js} +1 -1
- package/dist/ui/dist/assets/{chunk-4EMNKR2T-qoQt4Fpx.js → chunk-4EMNKR2T-CpZfjf2T.js} +1 -1
- package/dist/ui/dist/assets/{chunk-55XCU76K-C3I6EB-H.js → chunk-55XCU76K-CoP2pJTQ.js} +1 -1
- package/dist/ui/dist/assets/{chunk-7S6XSTFO-CL-9s_8p.js → chunk-7S6XSTFO-CaQcUvXY.js} +1 -1
- package/dist/ui/dist/assets/{chunk-AQDTSKZB-DHoGnGyP.js → chunk-AQDTSKZB-BZ9Czvbu.js} +1 -1
- package/dist/ui/dist/assets/{chunk-DSNLSHJZ-Bk8WO5JF.js → chunk-DSNLSHJZ-CNjUo110.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HAGFOTED-CwAC85lg.js → chunk-HAGFOTED-BTmBE_D3.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HNE4Q36P-CIllywWr.js → chunk-HNE4Q36P-D2cDuT0C.js} +1 -1
- package/dist/ui/dist/assets/{chunk-NQOKRVIF-DDlpyYiH.js → chunk-NQOKRVIF-AZcemFgG.js} +1 -1
- package/dist/ui/dist/assets/{chunk-UHW7PFBP-Cxdndcon.js → chunk-UHW7PFBP-BszwjUqG.js} +1 -1
- package/dist/ui/dist/assets/{chunk-VQ5W62QI-CL9d9_iJ.js → chunk-VQ5W62QI-vFSenYSe.js} +1 -1
- package/dist/ui/dist/assets/{chunk-WQQL4CDK-C4kzN8QU.js → chunk-WQQL4CDK-DZR6is1Q.js} +1 -1
- package/dist/ui/dist/assets/{chunk-XXDRQBXY-Cv_2FSiY.js → chunk-XXDRQBXY-CACFh-si.js} +1 -1
- package/dist/ui/dist/assets/{chunk-YZO67HFG-oHiuYaCJ.js → chunk-YZO67HFG-DRRTjGuW.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZAUX2LTX-Cy_8d0XT.js → chunk-ZAUX2LTX-Bxuy-Roa.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZCHMQSOR-piIpIcQi.js → chunk-ZCHMQSOR-rYXk__1o.js} +1 -1
- package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-DNKRIujO.js → classDiagram-v2-4MYWR3UA-RiF44tTz.js} +1 -1
- package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-BWKTGCHY.js → cose-bilkent-JH36ORCC-0oiQoThS.js} +1 -1
- package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-yx7KKkAk.js → cynefin-EF2NZ3EQ-BAssFK5K.js} +1 -1
- package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-BTnZ7QVL.js → cynefinDiagram-WF24UDRP-Bmd05bak.js} +1 -1
- package/dist/ui/dist/assets/{dagre-NK4GIZVX-CPh5Zn5I.js → dagre-NK4GIZVX-QJkLjPxj.js} +1 -1
- package/dist/ui/dist/assets/{diagram-CBK3MWTC-pI6NFdrD.js → diagram-CBK3MWTC-PSaSiYXz.js} +1 -1
- package/dist/ui/dist/assets/{diagram-I5XIIV2D-DUIfARoS.js → diagram-I5XIIV2D-BmU5X_hr.js} +1 -1
- package/dist/ui/dist/assets/{diagram-ODIL2UCW-lafSPvAw.js → diagram-ODIL2UCW-AAV39kUu.js} +1 -1
- package/dist/ui/dist/assets/{diagram-PFBEJUXE-u1lZas3a.js → diagram-PFBEJUXE-DWlV-oxd.js} +1 -1
- package/dist/ui/dist/assets/{diagram-RJA3I4VV-mc0GLMHo.js → diagram-RJA3I4VV-D8wHRCNX.js} +1 -1
- package/dist/ui/dist/assets/{diagram-TYUNFSNR-1NRbBrZW.js → diagram-TYUNFSNR-B_JQEsNY.js} +1 -1
- package/dist/ui/dist/assets/{dist-CHZ9mrl_.js → dist-DxQIK_Ay.js} +1 -1
- package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-DdStjTDc.js → ebnfDiagram-V4P3TTPT-D83qV8mv.js} +1 -1
- package/dist/ui/dist/assets/{elk-QONTBCTQ-BzRoFO45.js → elk-QONTBCTQ-BKFAKVSD.js} +1 -1
- package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-DlMkgQxj.js → erDiagram-KYQIGQAA-CHYRd4aM.js} +1 -1
- package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-BRYIOrod.js → eventmodeling-K75KTNOO-BDJDv3Zf.js} +1 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Bke3CfUH.js +1 -0
- package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-D8fQZ7Ah.js → ganttDiagram-R7GPNTJY-gTHjYkFj.js} +1 -1
- package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-C7ZydyWX.js → gitGraph-VSP46ZUC-D4cWkxKB.js} +1 -1
- package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-DCItam0v.js → gitGraphDiagram-PDYL7XHY-D3M9wYMr.js} +1 -1
- package/dist/ui/dist/assets/index-BUOFihod.js +134 -0
- package/dist/ui/dist/assets/index-DLNJT0VS.css +1 -0
- package/dist/ui/dist/assets/{info-OHQRW6UA-C3KneBKT.js → info-OHQRW6UA-B9y0ksVq.js} +1 -1
- package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-6wK09D32.js → infoDiagram-MVDRKAX5-s4FMwb8r.js} +1 -1
- package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-DTP5C8VX.js → ishikawaDiagram-HPEHV2GE-CnQdxybf.js} +1 -1
- package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-B--UIMtq.js → journeyDiagram-BOSHW7O7-I-9OpDWY.js} +1 -1
- package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-Ku4N9QGt.js → kanban-definition-FNSA4HC5-DqUbIdH4.js} +1 -1
- package/dist/ui/dist/assets/{line-C4-H5h1-.js → line-BELSui4d.js} +1 -1
- package/dist/ui/dist/assets/{linear-DhAID3hm.js → linear-77hmn2iQ.js} +1 -1
- package/dist/ui/dist/assets/{mermaid-parser.core-CMPNc-u6.js → mermaid-parser.core-Bxa71wW-.js} +3 -3
- package/dist/ui/dist/assets/{mermaid.core-Nd_xOg2y.js → mermaid.core-DBI9F4bh.js} +4 -4
- package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-BlVsym5h.js → mindmap-definition-6OY57UG3-BxXMOapP.js} +1 -1
- package/dist/ui/dist/assets/{packet-JDAUHWVQ-rsk7SM7S.js → packet-JDAUHWVQ-DTT77cQh.js} +1 -1
- package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-C0L20M8z.js → pegDiagram-ECIUJNBX-HzrOQPT5.js} +1 -1
- package/dist/ui/dist/assets/{pie-XZMESJXO-B23-iZSN.js → pie-XZMESJXO-iHwL1Qw-.js} +1 -1
- package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-BqFhe4Oh.js → pieDiagram-GNMPY74T-DZ1tLVjw.js} +1 -1
- package/dist/ui/dist/assets/{prism-core-DYsiqJeR.js → prism-core-NtO0YF9C.js} +1 -1
- package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-BKP75B-m.js → quadrantDiagram-D57K4MC3-BjTi9H5L.js} +1 -1
- package/dist/ui/dist/assets/{radar-ABXABTNO-BrEk2mQ7.js → radar-ABXABTNO-D2hYgXSB.js} +1 -1
- package/dist/ui/dist/assets/{railroad-I3PHUGI6-wWO4Eapc.js → railroad-I3PHUGI6-DpakXN74.js} +1 -1
- package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-DfY-IPvE.js → railroad-abnf-LNEFI6M7-B8ZFR3iy.js} +1 -1
- package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-19wedWGk.js → railroad-ebnf-DWYD2UWJ-S1XUyTsC.js} +1 -1
- package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-9vIslnoJ.js → railroad-peg-JR7BVNR7-BbeHC_6K.js} +1 -1
- package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-DVnmXs8w.js → railroadDiagram-Y72NRBL7-fd-2iyBo.js} +1 -1
- package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-BEoMdlzR.js → requirementDiagram-EQYSW5RA-B9-HHKH-.js} +1 -1
- package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-D7Tn2Gbs.js → sankeyDiagram-SY24JHO5-CeQN1ZEG.js} +1 -1
- package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-CyjUDBaQ.js → sequenceDiagram-BLEHNHQU-Vff7tx4t.js} +1 -1
- package/dist/ui/dist/assets/{src-BiC9XNcI.js → src-D5-ZzLwt.js} +1 -1
- package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-CMp1VCO9.js → stateDiagram-v2-A6IESKWZ-Bzn_ma9c.js} +1 -1
- package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-KEciLd6r.js → swimlanes-B3ENRKFQ-CevqLchV.js} +1 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-Bkv8hAmN.js +8 -0
- package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-C8YQtv7M.js → timeline-definition-JXYKGLGB-D9jSDGJI.js} +1 -1
- package/dist/ui/dist/assets/{treeView-D4JQ5SDB-DgAzPaOO.js → treeView-D4JQ5SDB-CoXKj11m.js} +1 -1
- package/dist/ui/dist/assets/{treemap-SAJKECNS-eCRqZMYc.js → treemap-SAJKECNS-cHsansGe.js} +1 -1
- package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-Dirryw9G.js → usecaseDiagram-WPOLBNNX-CW9i3bW6.js} +1 -1
- package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-BjxQbAUx.js → vennDiagram-Q3Y3IDCW-DuU3qEKJ.js} +1 -1
- package/dist/ui/dist/assets/{wardley-7MLQ67FV-CqHX5RUz.js → wardley-7MLQ67FV-dl9RgHUI.js} +1 -1
- package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-Ds1HoEUY.js → wardleyDiagram-IVAIDKWA-BOXhrK-a.js} +1 -1
- package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-kodzDb12.js → xychartDiagram-4URNHDU4-CLQNGrh4.js} +1 -1
- package/dist/ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/follow-me/SKILL.md +2 -0
- package/dist/ui/dist/assets/channel-DPDY0XJP.js +0 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-C1IBh_GZ.js +0 -1
- package/dist/ui/dist/assets/index-8BehwQ7E.js +0 -134
- package/dist/ui/dist/assets/index-mC2OW2Av.css +0 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DzestpDE.js +0 -8
package/README.md
CHANGED
|
@@ -78,6 +78,9 @@ The red blob in the party hat is your branch tip. Nothing follow-me does moves i
|
|
|
78
78
|

|
|
79
79
|
*Coverage while reading. Your agent runs the tests with coverage and passes the lcov to `follow-me coverage`; follow-me never runs anything. Bars show covered, partly covered and uncovered lines, `x` jumps to the next uncovered line, and clicking a bar lists the tests that ran it.*
|
|
80
80
|
|
|
81
|
+

|
|
82
|
+
*Your agent offers what it can draw, like "a state machine for this guard", as chips under the commit. Tap one and it answers with the diagram, each state marked by whether a test reached it. Click a state to jump to its code.*
|
|
83
|
+
|
|
81
84
|

|
|
82
85
|
*Diagrams sit full width under the commit or reply they explain, and open full size on click.*
|
|
83
86
|
|
package/dist/src/cli.js
CHANGED
|
@@ -3,14 +3,15 @@ import { 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
|
-
import {
|
|
6
|
+
import { mapOffTip, parseStateMap } from "./comments.js";
|
|
7
|
+
import { changedLines, LcovError, PREVIEW_LINES, parseLcov, parseLocArg, recordRun, runOf, tipPlacer, totalOf, } from "./coverage.js";
|
|
7
8
|
import { definitionIndex, isIdentifier } from "./definition.js";
|
|
8
|
-
import { DiffParseError, formatUnitRef, isUnitId } from "./diff.js";
|
|
9
|
-
import { BranchRefusedError, branchHead, GitError, isOwnWork, pathsAt, pathsChanged, signing, treeOf, writeBranch, } from "./git.js";
|
|
10
|
-
import { fetchCarried, fetchComments, fetchReport, findServer, followEvents, idleMs, listSessions, openBrowser, pollEvents, portFallbackMessage, postComment, postCoverage, postReply, probeHealth, readServerRecord, registerWith, sessionUrl, spawnDetached, startOrAttach, stateDir, stillHosted, stopServer, swapVersion, } from "./host.js";
|
|
9
|
+
import { DiffParseError, formatUnitRef, isRepoPath, isUnitId, splitLines } from "./diff.js";
|
|
10
|
+
import { BranchRefusedError, blobsAt, branchHead, GitError, isOwnWork, pathsAt, pathsChanged, signing, treeOf, writeBranch, } from "./git.js";
|
|
11
|
+
import { fetchCarried, fetchComments, fetchReport, fetchWaitingOffers, findServer, followEvents, idleMs, listSessions, openBrowser, pollEvents, portFallbackMessage, postComment, postCoverage, postReply, probeHealth, readServerRecord, registerWith, sessionUrl, spawnDetached, startOrAttach, stateDir, stillHosted, stopServer, swapVersion, } from "./host.js";
|
|
11
12
|
import { findSkillDir, InstallError, runInstall, runUninstall, syncSkills, } from "./install.js";
|
|
12
13
|
import { naiveDraft } from "./naive.js";
|
|
13
|
-
import { isCommentId, isConcernId, isFields, serializePlanFile, validatePlan, } from "./plan.js";
|
|
14
|
+
import { isCommentId, isConcernId, isFields, OFFERS_PER_COMMIT, OFFERS_PER_PLAN, serializePlanFile, validatePlan, } from "./plan.js";
|
|
14
15
|
import { renderEvents, renderPrompt, renderReport, renderShownComment, renderShownConcern, renderShownDefinitions, renderShownUnit, reportItems, } from "./prompt.js";
|
|
15
16
|
import { RegistrationError, sessionIdOf } from "./server.js";
|
|
16
17
|
import { BranchMovedError, cleanSessions, coverageAt, extractSession, MissingDraftError, NotARepositoryError, openSession, SessionFileError, SessionLockedError, sessionComments, sinceLastPlan, } from "./session.js";
|
|
@@ -31,9 +32,10 @@ prints what they flagged or asked. It is interactive: do what its "next:" line s
|
|
|
31
32
|
again until it prints the report.
|
|
32
33
|
|
|
33
34
|
Other commands:
|
|
34
|
-
reply <commentId> --body <text> [--diagram <file>]
|
|
35
|
+
reply <commentId> --body <text> [--diagram <file> [--map <file>]]
|
|
35
36
|
answer under a comment on the page; the diagram is
|
|
36
|
-
Mermaid source
|
|
37
|
+
Mermaid source, the map JSON tying its states to
|
|
38
|
+
tip lines
|
|
37
39
|
explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
|
|
38
40
|
start a thread on the page without being asked
|
|
39
41
|
show <h_…|c_…|k_…|path:line[@n|@base]|name>
|
|
@@ -42,8 +44,9 @@ Other commands:
|
|
|
42
44
|
is defined, instead of reading files
|
|
43
45
|
report [--json] [--markdown] print the report of the latest review; --json lists
|
|
44
46
|
the items to post to a merge request
|
|
45
|
-
coverage <lcov> [--test <id> [--at <path:line>]]
|
|
46
|
-
record a test run taken at the tip for the page
|
|
47
|
+
coverage <lcov> [--test <id> [--at <path:line>]] [--reset]
|
|
48
|
+
record a test run taken at the tip for the page;
|
|
49
|
+
--reset alone clears it before per-test runs
|
|
47
50
|
|
|
48
51
|
help --all lists every command.`;
|
|
49
52
|
const USAGE = `usage: follow-me <command> [options]
|
|
@@ -86,9 +89,10 @@ Interactive loop (while the review page is open):
|
|
|
86
89
|
reports the end of the review. Your own replies are
|
|
87
90
|
left out. --follow streams every event until the
|
|
88
91
|
review ends instead of stopping at the first
|
|
89
|
-
reply <commentId> --body <text> [--diagram <file>]
|
|
92
|
+
reply <commentId> --body <text> [--diagram <file> [--map <file>]]
|
|
90
93
|
answer under a comment on the page; the diagram is
|
|
91
|
-
Mermaid source
|
|
94
|
+
Mermaid source, the map JSON tying its states to
|
|
95
|
+
tip lines
|
|
92
96
|
explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
|
|
93
97
|
start a thread on the page without being asked;
|
|
94
98
|
--lines are change lines of the unit, commits count
|
|
@@ -107,11 +111,12 @@ Interactive loop (while the review page is open):
|
|
|
107
111
|
line in both voices, then questions with answers;
|
|
108
112
|
--json lists the items to post to a merge request
|
|
109
113
|
(path, lines, severity, body, thread, excluded)
|
|
110
|
-
coverage <lcov> [--test <id> [--at <path:line>]] [--branch <name>]
|
|
114
|
+
coverage <lcov> [--test <id> [--at <path:line>]] [--reset] [--branch <name>]
|
|
111
115
|
record the lcov of a test run at the branch tip;
|
|
112
116
|
refuses when a covered file in the working tree
|
|
113
117
|
differs from the tip. A plain run replaces the last;
|
|
114
|
-
--test runs add one test each, --at is where it is
|
|
118
|
+
--test runs add one test each, --at is where it is.
|
|
119
|
+
--reset starts fresh; alone it clears the last run
|
|
115
120
|
apply, while the page is open, swaps it to the new plan version in place, same URL.
|
|
116
121
|
|
|
117
122
|
Old names, still working:
|
|
@@ -291,6 +296,12 @@ function formatIssue(issue) {
|
|
|
291
296
|
return `$.commits[${issue.commit}]: concern: ${issue.reason}`;
|
|
292
297
|
case "too-many-concerns":
|
|
293
298
|
return `${issue.unit} has more than one concern, in commits ${issue.commits.join(" and ")}; keep the one that matters`;
|
|
299
|
+
case "bad-offer":
|
|
300
|
+
return `$.commits[${issue.commit}]: offer: ${issue.reason}`;
|
|
301
|
+
case "too-many-offers":
|
|
302
|
+
return issue.commit === null
|
|
303
|
+
? `the plan has ${issue.count} offers; at most ${OFFERS_PER_PLAN}, and most plans need none`
|
|
304
|
+
: `$.commits[${issue.commit}] has ${issue.count} offers; at most ${OFFERS_PER_COMMIT} per commit`;
|
|
294
305
|
case "final-mismatch": {
|
|
295
306
|
const tip = issue.expected ? `${issue.expected.mode} ${issue.expected.blob}` : "absent";
|
|
296
307
|
return `${show(issue.path)}: the last commit has ${issue.got}, the tip has ${tip}`;
|
|
@@ -305,6 +316,9 @@ function formatWarning(w) {
|
|
|
305
316
|
if (w.kind === "long-title") {
|
|
306
317
|
return `warning: $.commits[${w.commit}]: title is ${w.length} characters; keep it to 50`;
|
|
307
318
|
}
|
|
319
|
+
if (w.kind === "section-order") {
|
|
320
|
+
return `warning: $.commits[${w.commit}]: section ${w.section} comes after ${w.after}; sections read story, steps, details, wiring`;
|
|
321
|
+
}
|
|
308
322
|
if (w.kind === "question-addressed") {
|
|
309
323
|
return `warning: question ${w.comment} is marked addressed; explain it with "answered" and "at" unless answering it changed the plan`;
|
|
310
324
|
}
|
|
@@ -1053,6 +1067,13 @@ function waitOf(args) {
|
|
|
1053
1067
|
/** One call fits inside a harness's tool timeout, so the agent's loop is one command per turn. */
|
|
1054
1068
|
const ENTRY_WAIT_SECONDS = 90;
|
|
1055
1069
|
const MAX_ENTRY_WAIT_SECONDS = 300;
|
|
1070
|
+
/** The step that names the oldest accepted offer the agent has not answered, or null. */
|
|
1071
|
+
async function offerStep(port, summary) {
|
|
1072
|
+
const [first] = await fetchWaitingOffers(port, summary);
|
|
1073
|
+
if (first === undefined)
|
|
1074
|
+
return null;
|
|
1075
|
+
return `${STILL_WORKING}; offer ${first.offer} waits for your diagram: follow-me reply ${first.comment} --diagram <file> [--map <file>], then run follow-me again`;
|
|
1076
|
+
}
|
|
1056
1077
|
/**
|
|
1057
1078
|
* Listens to the hosted review for up to --wait seconds and prints what the reviewer did. It
|
|
1058
1079
|
* returns at once with news, and with the report when the reviewer submitted or ended the review.
|
|
@@ -1112,11 +1133,13 @@ async function listen(session, open, args) {
|
|
|
1112
1133
|
next("done; the user can port it with follow-me report --json");
|
|
1113
1134
|
return 0;
|
|
1114
1135
|
}
|
|
1115
|
-
next(
|
|
1136
|
+
next((await offerStep(port, summary)) ??
|
|
1137
|
+
`${STILL_WORKING}; answer with follow-me reply <id> --body "<markdown>" [--diagram file], then run follow-me again`);
|
|
1116
1138
|
return 0;
|
|
1117
1139
|
}
|
|
1118
1140
|
if (left <= 0) {
|
|
1119
|
-
next(
|
|
1141
|
+
next((await offerStep(port, summary)) ??
|
|
1142
|
+
`${STILL_WORKING}; run follow-me again to keep listening`);
|
|
1120
1143
|
return 0;
|
|
1121
1144
|
}
|
|
1122
1145
|
}
|
|
@@ -1315,7 +1338,7 @@ async function report(cwd, args, walkOnly = false) {
|
|
|
1315
1338
|
process.stdout.write(text);
|
|
1316
1339
|
return 0;
|
|
1317
1340
|
}
|
|
1318
|
-
const COVERAGE_USAGE = "usage: follow-me coverage <lcov> [--test <id> [--at <path:line>]]";
|
|
1341
|
+
const COVERAGE_USAGE = "usage: follow-me coverage <lcov> [--test <id> [--at <path:line>]] [--reset]\n follow-me coverage --reset";
|
|
1319
1342
|
/**
|
|
1320
1343
|
* Records the lcov of a test run the agent made at the tip in the session's coverage.json and
|
|
1321
1344
|
* prints how much of the branch's changed code it ran. follow-me never runs the tests itself.
|
|
@@ -1324,6 +1347,13 @@ async function coverage(cwd, args) {
|
|
|
1324
1347
|
const [file, extra] = args.positionals;
|
|
1325
1348
|
const id = args.values.get("test");
|
|
1326
1349
|
const atArg = args.values.get("at");
|
|
1350
|
+
const reset = args.flags.has("reset");
|
|
1351
|
+
if (file === undefined && reset && id === undefined && atArg === undefined) {
|
|
1352
|
+
const session = await openFresh(cwd, args);
|
|
1353
|
+
session.clearCoverage();
|
|
1354
|
+
say("coverage cleared; the next run starts fresh");
|
|
1355
|
+
return 0;
|
|
1356
|
+
}
|
|
1327
1357
|
if (file === undefined || extra !== undefined)
|
|
1328
1358
|
throw new UsageError(COVERAGE_USAGE);
|
|
1329
1359
|
if (atArg !== undefined && id === undefined) {
|
|
@@ -1354,10 +1384,15 @@ async function coverage(cwd, args) {
|
|
|
1354
1384
|
const at = atArg === undefined ? undefined : parseLocArg(atArg, place);
|
|
1355
1385
|
if (typeof at === "string")
|
|
1356
1386
|
throw new UsageError(at);
|
|
1357
|
-
|
|
1387
|
+
const preview = at === undefined ? undefined : await testPreview(session, at);
|
|
1388
|
+
test = {
|
|
1389
|
+
id,
|
|
1390
|
+
...(at === undefined ? {} : { at }),
|
|
1391
|
+
...(preview === undefined ? {} : { preview }),
|
|
1392
|
+
};
|
|
1358
1393
|
}
|
|
1359
1394
|
const takenAt = new Date().toISOString();
|
|
1360
|
-
const recorded = recordRun(session.coverage(), run, {
|
|
1395
|
+
const recorded = recordRun(reset ? null : session.coverage(), run, {
|
|
1361
1396
|
tip,
|
|
1362
1397
|
takenAt,
|
|
1363
1398
|
...(test === undefined ? {} : { test }),
|
|
@@ -1370,6 +1405,19 @@ async function coverage(cwd, args) {
|
|
|
1370
1405
|
say(`coverage for ${plural(recorded.files.size, "file")} at ${short(tip)}; ${covered} of ${executable} changed lines covered`);
|
|
1371
1406
|
return 0;
|
|
1372
1407
|
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Up to PREVIEW_LINES tip lines from where a test starts, when the branch leaves its file alone:
|
|
1410
|
+
* the page shows the branch's own files from the plan, and the others need this to peek.
|
|
1411
|
+
*/
|
|
1412
|
+
async function testPreview(session, at) {
|
|
1413
|
+
const changed = new Set([...session.diff.units.values()].map((u) => u.path));
|
|
1414
|
+
if (changed.has(at.path))
|
|
1415
|
+
return undefined;
|
|
1416
|
+
const bytes = (await blobsAt(session.root, session.anchor.tip, [at.path])).get(at.path) ?? "";
|
|
1417
|
+
return splitLines(bytes)
|
|
1418
|
+
.slice(at.start - 1, at.start - 1 + PREVIEW_LINES)
|
|
1419
|
+
.map((line) => Buffer.from(line.replace(/\r?\n$/, ""), "latin1").toString("utf8"));
|
|
1420
|
+
}
|
|
1373
1421
|
/** The old name of report. */
|
|
1374
1422
|
async function feedback(cwd, args) {
|
|
1375
1423
|
console.error("follow-me: feedback is now report; printing the same");
|
|
@@ -1380,11 +1428,13 @@ function isCommentEvent(event) {
|
|
|
1380
1428
|
}
|
|
1381
1429
|
/**
|
|
1382
1430
|
* Events the reviewer caused; the agent's own replies, explanations, version swaps and coverage
|
|
1383
|
-
* are not news.
|
|
1431
|
+
* are not news, and neither is a dismissed offer, which asks nothing of the agent.
|
|
1384
1432
|
*/
|
|
1385
1433
|
function isNews(event) {
|
|
1386
1434
|
if (event.type === "version" || event.type === "coverage")
|
|
1387
1435
|
return false;
|
|
1436
|
+
if (event.type === "offer-dismissed")
|
|
1437
|
+
return false;
|
|
1388
1438
|
if (event.type === "comment" || event.type === "comment-updated") {
|
|
1389
1439
|
return event.comment.author !== "agent";
|
|
1390
1440
|
}
|
|
@@ -1464,12 +1514,18 @@ async function watch(cwd, args) {
|
|
|
1464
1514
|
/** Posts the agent's answer under a comment on the hosted page. */
|
|
1465
1515
|
async function reply(cwd, args) {
|
|
1466
1516
|
const [commentId, extra] = args.positionals;
|
|
1467
|
-
const
|
|
1517
|
+
const diagram = diagramOf(args);
|
|
1518
|
+
// A diagram can be the whole answer, as when it answers an accepted offer.
|
|
1519
|
+
const body = args.values.get("body") ?? (diagram === undefined ? undefined : "");
|
|
1468
1520
|
if (commentId === undefined || extra !== undefined || body === undefined) {
|
|
1469
|
-
throw new UsageError(
|
|
1521
|
+
throw new UsageError(REPLY_USAGE);
|
|
1522
|
+
}
|
|
1523
|
+
const mapFile = args.values.get("map");
|
|
1524
|
+
if (mapFile !== undefined && diagram === undefined) {
|
|
1525
|
+
throw new UsageError(`--map needs --diagram, the state diagram it ties to code\n${REPLY_USAGE}`);
|
|
1470
1526
|
}
|
|
1471
|
-
const diagram = diagramOf(args);
|
|
1472
1527
|
const session = await openSession(cwd, sessionOpts(args));
|
|
1528
|
+
const map = mapFile === undefined ? undefined : await readMap(session, mapFile, diagram);
|
|
1473
1529
|
const open = await openOnServer(session, null);
|
|
1474
1530
|
if (open === null)
|
|
1475
1531
|
return notOpen(session);
|
|
@@ -1477,11 +1533,59 @@ async function reply(cwd, args) {
|
|
|
1477
1533
|
author: "agent",
|
|
1478
1534
|
body,
|
|
1479
1535
|
...(diagram === undefined ? {} : { diagram }),
|
|
1536
|
+
...(map === undefined ? {} : { map }),
|
|
1480
1537
|
});
|
|
1481
1538
|
say(`replied ${id} to ${commentId}`);
|
|
1482
1539
|
next(`run follow-me again; ${STILL_WORKING}`);
|
|
1483
1540
|
return 0;
|
|
1484
1541
|
}
|
|
1542
|
+
const REPLY_USAGE = `usage: follow-me reply <commentId> --body <text> [--diagram <file> [--map <file>]]
|
|
1543
|
+
follow-me reply <commentId> --diagram <file> [--map <file>] [--body <text>]`;
|
|
1544
|
+
/** A map path as git names it: the JSON's UTF-8 text as the bytes git prints, read as latin1. */
|
|
1545
|
+
function repoPathOf(typed) {
|
|
1546
|
+
const path = Buffer.from(typed, "utf8").toString("latin1");
|
|
1547
|
+
if (!isRepoPath(path))
|
|
1548
|
+
throw new UsageError(`${typed} is not a repo path`);
|
|
1549
|
+
return path;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* --map's JSON, checked against the tip: every place is a file there and within its lines. Every
|
|
1553
|
+
* bad entry is named at once, so one fix round is enough.
|
|
1554
|
+
*/
|
|
1555
|
+
async function readMap(session, file, diagram) {
|
|
1556
|
+
let raw;
|
|
1557
|
+
try {
|
|
1558
|
+
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
1559
|
+
}
|
|
1560
|
+
catch (error) {
|
|
1561
|
+
throw new UsageError(`cannot read ${file} as JSON: ${error instanceof Error ? error.message : error}`);
|
|
1562
|
+
}
|
|
1563
|
+
const typed = parseStateMap(raw, diagram);
|
|
1564
|
+
if (typeof typed === "string")
|
|
1565
|
+
throw new UsageError(`${file}: ${typed}`);
|
|
1566
|
+
const at = (loc) => ({ ...loc, path: repoPathOf(loc.path) });
|
|
1567
|
+
const map = {
|
|
1568
|
+
states: Object.fromEntries(Object.entries(typed.states).map(([name, locs]) => [name, locs.map(at)])),
|
|
1569
|
+
...(typed.transitions === undefined
|
|
1570
|
+
? {}
|
|
1571
|
+
: { transitions: typed.transitions.map((t) => ({ ...t, at: t.at.map(at) })) }),
|
|
1572
|
+
};
|
|
1573
|
+
const { root, anchor } = session;
|
|
1574
|
+
const named = [
|
|
1575
|
+
...Object.values(map.states).flat(),
|
|
1576
|
+
...(map.transitions ?? []).flatMap((t) => t.at),
|
|
1577
|
+
].map((loc) => loc.path);
|
|
1578
|
+
const tip = await pathsAt(root, anchor.tip);
|
|
1579
|
+
const present = [...new Set(named)].filter((p) => tip.has(p));
|
|
1580
|
+
const lines = new Map([...(await blobsAt(root, anchor.tip, present))].map(([p, bytes]) => [
|
|
1581
|
+
p,
|
|
1582
|
+
splitLines(bytes).length,
|
|
1583
|
+
]));
|
|
1584
|
+
const problems = mapOffTip(map, lines, (p) => Buffer.from(p, "latin1").toString("utf8"));
|
|
1585
|
+
if (problems.length > 0)
|
|
1586
|
+
throw new UsageError(`${file}:\n ${problems.join("\n ")}`);
|
|
1587
|
+
return map;
|
|
1588
|
+
}
|
|
1485
1589
|
/** The Mermaid source in --diagram's file, or undefined without the flag. */
|
|
1486
1590
|
function diagramOf(args) {
|
|
1487
1591
|
const file = args.values.get("diagram");
|
|
@@ -1950,7 +2054,7 @@ export async function main(argv, cwd) {
|
|
|
1950
2054
|
return await watch(cwd, parseArgs(rest, spec));
|
|
1951
2055
|
}
|
|
1952
2056
|
case "reply":
|
|
1953
|
-
return await reply(cwd, parseArgs(rest, { values: ["body", "diagram", "branch"] }));
|
|
2057
|
+
return await reply(cwd, parseArgs(rest, { values: ["body", "diagram", "map", "branch"] }));
|
|
1954
2058
|
case "explain": {
|
|
1955
2059
|
const spec = { values: ["commit", "unit", "lines", "body", "diagram", "branch"] };
|
|
1956
2060
|
return await explain(cwd, parseArgs(rest, spec));
|
|
@@ -1958,7 +2062,7 @@ export async function main(argv, cwd) {
|
|
|
1958
2062
|
case "show":
|
|
1959
2063
|
return await showRef(cwd, parseArgs(rest, { values: ["branch"] }));
|
|
1960
2064
|
case "coverage":
|
|
1961
|
-
return await coverage(cwd, parseArgs(rest, { values: ["test", "at", "branch"] }));
|
|
2065
|
+
return await coverage(cwd, parseArgs(rest, { values: ["test", "at", "branch"], flags: ["reset"] }));
|
|
1962
2066
|
case "report":
|
|
1963
2067
|
return await report(cwd, parseArgs(rest, { values: ["branch"], flags: ["json", "markdown"] }));
|
|
1964
2068
|
case "clean": {
|
package/dist/src/comments.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { parseLoc } from "./coverage.js";
|
|
1
2
|
import { isRepoPath, isSha, isUnitId } from "./diff.js";
|
|
2
|
-
import { diagramProblem, isArray, isCommentId, isConcernId, isFields, isPlanVersion, isSeverity, parsePlacedConcern, } from "./plan.js";
|
|
3
|
+
import { diagramProblem, isArray, isCommentId, isConcernId, isFields, isOfferId, isPlanVersion, isSeverity, parsePlacedConcern, } from "./plan.js";
|
|
3
4
|
/**
|
|
4
5
|
* The wire formats of review state, parsed once at the boundary: comments, replies, concern
|
|
5
6
|
* decisions, the review file, its autosaved draft and the events a hosted session logs. Every
|
|
@@ -48,10 +49,110 @@ function parseAnchor(v) {
|
|
|
48
49
|
}
|
|
49
50
|
return { kind: "hunk", unit: v.unit, commit, changes, offsets };
|
|
50
51
|
}
|
|
52
|
+
function parseLocs(v, at, needed) {
|
|
53
|
+
const what = `${at}: expected a list of { path, start, end }${needed ? " with at least one place" : ""}`;
|
|
54
|
+
if (!isArray(v) || (needed && v.length === 0))
|
|
55
|
+
return what;
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const [i, raw] of v.entries()) {
|
|
58
|
+
const loc = parseLoc(raw);
|
|
59
|
+
if (loc === null)
|
|
60
|
+
return `${at}[${i}]: expected { path, start, end } with 1 <= start <= end`;
|
|
61
|
+
out.push(loc);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
const TRANSITION_ENDS = ["from", "to"];
|
|
66
|
+
function escapeRegExp(text) {
|
|
67
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 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. The reason
|
|
72
|
+
* names the bad entry, as map.states.<name>[i] or map.transitions[i].<field>.
|
|
73
|
+
*/
|
|
74
|
+
export function parseStateMap(v, diagram) {
|
|
75
|
+
if (!isFields(v))
|
|
76
|
+
return "map: expected { states, transitions }";
|
|
77
|
+
if (!isFields(v.states) || Object.keys(v.states).length === 0) {
|
|
78
|
+
return "map.states: expected an object of state name to places";
|
|
79
|
+
}
|
|
80
|
+
const states = [];
|
|
81
|
+
for (const [name, raw] of Object.entries(v.states)) {
|
|
82
|
+
if (name.trim() === "")
|
|
83
|
+
return "map.states: a state has an empty name";
|
|
84
|
+
const locs = parseLocs(raw, `map.states.${name}`, true);
|
|
85
|
+
if (typeof locs === "string")
|
|
86
|
+
return locs;
|
|
87
|
+
states.push([name, locs]);
|
|
88
|
+
}
|
|
89
|
+
const named = new Set(states.map(([name]) => name));
|
|
90
|
+
const transitions = [];
|
|
91
|
+
const rawTransitions = v.transitions ?? [];
|
|
92
|
+
if (!isArray(rawTransitions))
|
|
93
|
+
return "map.transitions: expected a list";
|
|
94
|
+
for (const [i, raw] of rawTransitions.entries()) {
|
|
95
|
+
const at = `map.transitions[${i}]`;
|
|
96
|
+
if (!isFields(raw))
|
|
97
|
+
return `${at}: expected { from, to, event, at }`;
|
|
98
|
+
for (const end of TRANSITION_ENDS) {
|
|
99
|
+
const state = raw[end];
|
|
100
|
+
if (typeof state !== "string" || !named.has(state)) {
|
|
101
|
+
return `${at}.${end}: ${JSON.stringify(state)} is not a state in map.states`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const { from, to, event } = raw;
|
|
105
|
+
if (typeof from !== "string" || typeof to !== "string")
|
|
106
|
+
return `${at}: expected from and to`;
|
|
107
|
+
if (event !== undefined && typeof event !== "string")
|
|
108
|
+
return `${at}.event: expected a string`;
|
|
109
|
+
const locs = parseLocs(raw.at, `${at}.at`, false);
|
|
110
|
+
if (typeof locs === "string")
|
|
111
|
+
return locs;
|
|
112
|
+
transitions.push({ from, to, ...(event === undefined ? {} : { event }), at: locs });
|
|
113
|
+
}
|
|
114
|
+
const first = (diagram ?? "")
|
|
115
|
+
.split("\n")
|
|
116
|
+
.find((line) => line.trim() !== "")
|
|
117
|
+
?.trim() ?? "";
|
|
118
|
+
if (first.startsWith("stateDiagram")) {
|
|
119
|
+
for (const name of named) {
|
|
120
|
+
if (!new RegExp(`(^|[^\\w])${escapeRegExp(name)}([^\\w]|$)`, "m").test(diagram ?? "")) {
|
|
121
|
+
return `map.states.${name}: not a state of the stateDiagram`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
states: Object.fromEntries(states),
|
|
127
|
+
...(transitions.length === 0 ? {} : { transitions }),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Where a map points past the tip: a path the tip has no file at, or lines beyond its end. lines
|
|
132
|
+
* holds the line count of every tip file the map names that exists; show prints a path.
|
|
133
|
+
*/
|
|
134
|
+
export function mapOffTip(map, lines, show = (path) => path) {
|
|
135
|
+
const check = (loc, at) => {
|
|
136
|
+
const count = lines.get(loc.path);
|
|
137
|
+
if (count === undefined)
|
|
138
|
+
return [`${at}: ${show(loc.path)} is not a file at the tip`];
|
|
139
|
+
if (loc.end > count) {
|
|
140
|
+
return [
|
|
141
|
+
`${at}: lines ${loc.start}-${loc.end} are past the end of ${show(loc.path)}, which has ${count} lines at the tip`,
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
return [];
|
|
145
|
+
};
|
|
146
|
+
return [
|
|
147
|
+
...Object.entries(map.states).flatMap(([name, locs]) => locs.flatMap((loc, i) => check(loc, `map.states.${name}[${i}]`))),
|
|
148
|
+
...(map.transitions ?? []).flatMap((t, i) => t.at.flatMap((loc, j) => check(loc, `map.transitions[${i}].at[${j}]`))),
|
|
149
|
+
];
|
|
150
|
+
}
|
|
51
151
|
/** A stored reply. Replies a client posts go through parseNewReply, which mints id and time. */
|
|
52
152
|
export function parseReply(v) {
|
|
53
|
-
if (!isFields(v))
|
|
153
|
+
if (!isFields(v)) {
|
|
54
154
|
return "not an object";
|
|
155
|
+
}
|
|
55
156
|
if (!isReplyId(v.id))
|
|
56
157
|
return "id is not a reply id";
|
|
57
158
|
if (v.author !== "agent" && v.author !== "reviewer")
|
|
@@ -61,30 +162,50 @@ export function parseReply(v) {
|
|
|
61
162
|
}
|
|
62
163
|
if (v.diagram !== undefined && typeof v.diagram !== "string")
|
|
63
164
|
return "diagram is not a string";
|
|
165
|
+
const map = v.map === undefined ? undefined : parseStateMap(v.map, v.diagram);
|
|
166
|
+
if (typeof map === "string")
|
|
167
|
+
return map;
|
|
64
168
|
return {
|
|
65
169
|
id: v.id,
|
|
66
170
|
author: v.author,
|
|
67
171
|
body: v.body,
|
|
68
172
|
...(v.diagram === undefined ? {} : { diagram: v.diagram }),
|
|
173
|
+
...(map === undefined ? {} : { map }),
|
|
69
174
|
createdAt: v.createdAt,
|
|
70
175
|
};
|
|
71
176
|
}
|
|
72
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* The body of POST .../replies: who speaks, what, and an optional Mermaid diagram. The body may be
|
|
179
|
+
* empty only when the diagram is the answer.
|
|
180
|
+
*/
|
|
73
181
|
export function parseNewReply(v) {
|
|
74
182
|
if (!isFields(v))
|
|
75
183
|
return "the reply is not an object";
|
|
76
184
|
if (v.author !== "agent" && v.author !== "reviewer")
|
|
77
185
|
return 'author is not "agent" or "reviewer"';
|
|
78
|
-
if (typeof v.body !== "string"
|
|
79
|
-
return "body is not a
|
|
80
|
-
if (v.diagram === undefined)
|
|
186
|
+
if (typeof v.body !== "string")
|
|
187
|
+
return "body is not a string";
|
|
188
|
+
if (v.diagram === undefined) {
|
|
189
|
+
if (v.map !== undefined)
|
|
190
|
+
return "map needs the diagram it ties to code";
|
|
191
|
+
if (v.body.trim() === "")
|
|
192
|
+
return "body is empty; a reply needs words or a diagram";
|
|
81
193
|
return { author: v.author, body: v.body };
|
|
194
|
+
}
|
|
82
195
|
if (typeof v.diagram !== "string")
|
|
83
196
|
return "diagram is not a string";
|
|
84
197
|
const problem = diagramProblem(v.diagram);
|
|
85
198
|
if (problem !== null)
|
|
86
199
|
return `diagram: ${problem}`;
|
|
87
|
-
|
|
200
|
+
const map = v.map === undefined ? undefined : parseStateMap(v.map, v.diagram);
|
|
201
|
+
if (typeof map === "string")
|
|
202
|
+
return map;
|
|
203
|
+
return {
|
|
204
|
+
author: v.author,
|
|
205
|
+
body: v.body,
|
|
206
|
+
diagram: v.diagram,
|
|
207
|
+
...(map === undefined ? {} : { map }),
|
|
208
|
+
};
|
|
88
209
|
}
|
|
89
210
|
function isString(v) {
|
|
90
211
|
return typeof v === "string";
|
|
@@ -133,6 +254,14 @@ export function parseComment(v) {
|
|
|
133
254
|
const severity = v.severity;
|
|
134
255
|
if (severity !== undefined && !isSeverity(severity))
|
|
135
256
|
return "severity is unknown";
|
|
257
|
+
const offer = v.offer;
|
|
258
|
+
if (offer !== undefined) {
|
|
259
|
+
if (!isOfferId(offer))
|
|
260
|
+
return "offer is not an offer id";
|
|
261
|
+
if (v.kind !== "question" || v.author !== undefined) {
|
|
262
|
+
return "offer is only for a reviewer's question";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
136
265
|
const replies = parseList(v.replies, parseReply, "replies");
|
|
137
266
|
if (typeof replies === "string")
|
|
138
267
|
return replies;
|
|
@@ -152,6 +281,7 @@ export function parseComment(v) {
|
|
|
152
281
|
...(replies.length === 0 ? {} : { replies }),
|
|
153
282
|
...(v.author === "agent" ? { author: "agent" } : {}),
|
|
154
283
|
...(v.diagram === undefined ? {} : { diagram: v.diagram }),
|
|
284
|
+
...(offer === undefined ? {} : { offer }),
|
|
155
285
|
};
|
|
156
286
|
}
|
|
157
287
|
function withDecision(concern, v) {
|
|
@@ -363,6 +493,9 @@ export function parseReview(v) {
|
|
|
363
493
|
const concernsCarried = parseList(v.carriedConcerns, parseCarriedConcern, "carriedConcerns");
|
|
364
494
|
if (typeof concernsCarried === "string")
|
|
365
495
|
return concernsCarried;
|
|
496
|
+
const dismissed = parseIds(v.dismissedOffers, isOfferId, "dismissedOffers", "offer");
|
|
497
|
+
if (typeof dismissed === "string")
|
|
498
|
+
return dismissed;
|
|
366
499
|
return {
|
|
367
500
|
schema: v.schema,
|
|
368
501
|
mode,
|
|
@@ -376,6 +509,7 @@ export function parseReview(v) {
|
|
|
376
509
|
...(earlier.length === 0 ? {} : { earlierReplies: earlier }),
|
|
377
510
|
...(carried === null ? {} : { carried }),
|
|
378
511
|
...(concernsCarried.length === 0 ? {} : { carriedConcerns: concernsCarried }),
|
|
512
|
+
...(dismissed.length === 0 ? {} : { dismissedOffers: dismissed }),
|
|
379
513
|
};
|
|
380
514
|
}
|
|
381
515
|
function lenient(items, parse) {
|
|
@@ -403,6 +537,7 @@ export function parseDraft(v) {
|
|
|
403
537
|
earlierReplies: lenient(v.earlierReplies, parseEarlierReplies),
|
|
404
538
|
carried: v.carried === undefined ? null : lenient(v.carried, parseCarriedRef),
|
|
405
539
|
carriedConcerns: lenient(v.carriedConcerns, parseCarriedConcern),
|
|
540
|
+
dismissedOffers: lenientIds(v.dismissedOffers, isOfferId),
|
|
406
541
|
};
|
|
407
542
|
}
|
|
408
543
|
export function parseEvent(v) {
|
|
@@ -444,6 +579,10 @@ export function parseEvent(v) {
|
|
|
444
579
|
}
|
|
445
580
|
return { seq, type: v.type, commentId: id, resolved: v.resolved };
|
|
446
581
|
}
|
|
582
|
+
case "offer-dismissed":
|
|
583
|
+
return isOfferId(v.offer)
|
|
584
|
+
? { seq, type: v.type, offer: v.offer }
|
|
585
|
+
: "offer is not an offer id";
|
|
447
586
|
case "version": {
|
|
448
587
|
if (!isPlanVersion(v.version) || typeof v.hash !== "string")
|
|
449
588
|
return "malformed version event";
|
package/dist/src/coverage.js
CHANGED
|
@@ -188,6 +188,8 @@ function mergeMax(a, b) {
|
|
|
188
188
|
}
|
|
189
189
|
return out;
|
|
190
190
|
}
|
|
191
|
+
/** How many tip lines a test outside the branch's changes carries for the page's peek. */
|
|
192
|
+
export const PREVIEW_LINES = 8;
|
|
191
193
|
/**
|
|
192
194
|
* The coverage after this run. A plain run replaces everything, its TN blocks becoming its tests.
|
|
193
195
|
* A named test's run adds to what is already there for the same tip, and starts over otherwise.
|
|
@@ -202,6 +204,7 @@ export function recordRun(prior, run, at) {
|
|
|
202
204
|
const tests = new Map(base?.tests ?? []);
|
|
203
205
|
const entry = {
|
|
204
206
|
...(test.at === undefined ? {} : { at: test.at }),
|
|
207
|
+
...(test.preview === undefined ? {} : { preview: test.preview }),
|
|
205
208
|
lines: hitLines(run.files),
|
|
206
209
|
};
|
|
207
210
|
tests.set(test.id, entry);
|
|
@@ -281,6 +284,7 @@ export function serializeCoverage(c) {
|
|
|
281
284
|
id,
|
|
282
285
|
{
|
|
283
286
|
...(t.at === undefined ? {} : { at: t.at }),
|
|
287
|
+
...(t.preview === undefined ? {} : { preview: [...t.preview] }),
|
|
284
288
|
lines: Object.fromEntries([...t.lines].map(([p, ls]) => [p, [...ls]])),
|
|
285
289
|
},
|
|
286
290
|
]));
|
|
@@ -296,7 +300,8 @@ function isLine(v) {
|
|
|
296
300
|
function entries(v) {
|
|
297
301
|
return isFields(v) ? Object.entries(v) : null;
|
|
298
302
|
}
|
|
299
|
-
|
|
303
|
+
/** A { path, start, end } with 1 <= start <= end, or null. */
|
|
304
|
+
export function parseLoc(v) {
|
|
300
305
|
if (!isFields(v))
|
|
301
306
|
return null;
|
|
302
307
|
const { path, start, end } = v;
|
|
@@ -350,6 +355,11 @@ function parseTest(v) {
|
|
|
350
355
|
const at = v.at === undefined ? undefined : parseLoc(v.at);
|
|
351
356
|
if (at === null)
|
|
352
357
|
return "at is not a { path, start, end }";
|
|
358
|
+
const preview = v.preview;
|
|
359
|
+
if (preview !== undefined &&
|
|
360
|
+
!(Array.isArray(preview) && preview.every((l) => typeof l === "string"))) {
|
|
361
|
+
return "preview is not a list of lines";
|
|
362
|
+
}
|
|
353
363
|
const lineEntries = entries(v.lines);
|
|
354
364
|
if (lineEntries === null)
|
|
355
365
|
return "lines is not an object";
|
|
@@ -360,7 +370,11 @@ function parseTest(v) {
|
|
|
360
370
|
}
|
|
361
371
|
lines.set(path, list);
|
|
362
372
|
}
|
|
363
|
-
return {
|
|
373
|
+
return {
|
|
374
|
+
...(at === undefined ? {} : { at }),
|
|
375
|
+
...(preview === undefined ? {} : { preview: preview.filter((l) => typeof l === "string") }),
|
|
376
|
+
lines,
|
|
377
|
+
};
|
|
364
378
|
}
|
|
365
379
|
/** The one place coverage.json becomes a Coverage. */
|
|
366
380
|
export function parseCoverage(v) {
|
package/dist/src/host.js
CHANGED
|
@@ -320,6 +320,29 @@ export async function fetchComments(port, summary) {
|
|
|
320
320
|
}
|
|
321
321
|
return out;
|
|
322
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Offers accepted during the hosted review, on this version or a carried one, whose comment has no
|
|
325
|
+
* reply from the agent yet, oldest first.
|
|
326
|
+
*/
|
|
327
|
+
export async function fetchWaitingOffers(port, summary) {
|
|
328
|
+
const state = await call(port, "GET", `${summary.url}api/state`);
|
|
329
|
+
if (!isFields(state))
|
|
330
|
+
throw new RegistrationError("the server sent no state");
|
|
331
|
+
const comments = Array.isArray(state["comments"]) ? state["comments"] : [];
|
|
332
|
+
const carried = Array.isArray(state["carried"]) ? state["carried"] : [];
|
|
333
|
+
const said = [
|
|
334
|
+
...comments.map((raw) => parseComment(raw)),
|
|
335
|
+
...carried.map((raw) => {
|
|
336
|
+
const c = parseCarried(raw);
|
|
337
|
+
return typeof c === "string" ? c : c.comment;
|
|
338
|
+
}),
|
|
339
|
+
];
|
|
340
|
+
return said
|
|
341
|
+
.flatMap((c) => (typeof c === "string" ? [] : [c]))
|
|
342
|
+
.filter((c) => !(c.replies ?? []).some((r) => r.author === "agent"))
|
|
343
|
+
.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
|
|
344
|
+
.flatMap((c) => (c.offer === undefined ? [] : [{ offer: c.offer, comment: c.id }]));
|
|
345
|
+
}
|
|
323
346
|
/** One long poll of the session's event log: what happened after `since`, or nothing at timeout. */
|
|
324
347
|
export async function pollEvents(port, summary, since, timeoutMs) {
|
|
325
348
|
const path = `${summary.url}api/events?since=${since}&timeout=${timeoutMs}`;
|