@shiyuphan/follow-me 0.8.0 → 0.9.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 +8 -3
- package/dist/src/cli.js +54 -19
- package/dist/src/comments.js +70 -3
- package/dist/src/coverage.js +147 -23
- package/dist/src/refs.js +239 -0
- package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-Bp14ygmz.js → abnfDiagram-WEYOIA3C-DztIEwNE.js} +1 -1
- package/dist/ui/dist/assets/{arc-DUR6uzhI.js → arc-DUYh-EqZ.js} +1 -1
- package/dist/ui/dist/assets/{architecture-WOLXFQ4H-CyfnP65O.js → architecture-WOLXFQ4H-BQYTB92_.js} +1 -1
- package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-dJTicIk2.js → architectureDiagram-ZHVYKT53-Dl7095A5.js} +1 -1
- package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-BYiqh5Ib.js → blockDiagram-RJAE7QA3-QDZ5kE1J.js} +1 -1
- package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-DHHPQ7xE.js → c4Diagram-XDPPPE54-CKJZ1xOw.js} +1 -1
- package/dist/ui/dist/assets/channel-DF08Hd8f.js +1 -0
- package/dist/ui/dist/assets/{chunk-2E7WHVMD-BODm_awL.js → chunk-2E7WHVMD-BXBjaqd1.js} +1 -1
- package/dist/ui/dist/assets/{chunk-4EMNKR2T-CpZfjf2T.js → chunk-4EMNKR2T-DPzfVuke.js} +1 -1
- package/dist/ui/dist/assets/{chunk-55XCU76K-CoP2pJTQ.js → chunk-55XCU76K-B33Sj3w8.js} +1 -1
- package/dist/ui/dist/assets/{chunk-7S6XSTFO-CaQcUvXY.js → chunk-7S6XSTFO-B_u7dS03.js} +1 -1
- package/dist/ui/dist/assets/{chunk-AQDTSKZB-BZ9Czvbu.js → chunk-AQDTSKZB-B4EzH5gx.js} +1 -1
- package/dist/ui/dist/assets/{chunk-DSNLSHJZ-CNjUo110.js → chunk-DSNLSHJZ-CfWeHABr.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HAGFOTED-BTmBE_D3.js → chunk-HAGFOTED-B8G8PjLZ.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HNE4Q36P-D2cDuT0C.js → chunk-HNE4Q36P-BA-PqoTF.js} +1 -1
- package/dist/ui/dist/assets/{chunk-NQOKRVIF-AZcemFgG.js → chunk-NQOKRVIF-JvhuFcnR.js} +1 -1
- package/dist/ui/dist/assets/{chunk-UHW7PFBP-BszwjUqG.js → chunk-UHW7PFBP-BrfL5CGY.js} +1 -1
- package/dist/ui/dist/assets/{chunk-VQ5W62QI-vFSenYSe.js → chunk-VQ5W62QI-dBJqgdDy.js} +1 -1
- package/dist/ui/dist/assets/{chunk-WQQL4CDK-DZR6is1Q.js → chunk-WQQL4CDK-dfqd3k_P.js} +1 -1
- package/dist/ui/dist/assets/{chunk-XXDRQBXY-CACFh-si.js → chunk-XXDRQBXY-kViWdB9M.js} +1 -1
- package/dist/ui/dist/assets/{chunk-YZO67HFG-DRRTjGuW.js → chunk-YZO67HFG-ukfs4wT3.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZAUX2LTX-Bxuy-Roa.js → chunk-ZAUX2LTX-BxZHki81.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZCHMQSOR-rYXk__1o.js → chunk-ZCHMQSOR-PjluhR_E.js} +1 -1
- package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-RiF44tTz.js → classDiagram-v2-4MYWR3UA-Ct89LJec.js} +1 -1
- package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-0oiQoThS.js → cose-bilkent-JH36ORCC-BnQceGmB.js} +1 -1
- package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-BAssFK5K.js → cynefin-EF2NZ3EQ-D6WkCVE8.js} +1 -1
- package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-Bmd05bak.js → cynefinDiagram-WF24UDRP-dn6DUPAb.js} +1 -1
- package/dist/ui/dist/assets/{dagre-NK4GIZVX-QJkLjPxj.js → dagre-NK4GIZVX-BtZSlLk7.js} +1 -1
- package/dist/ui/dist/assets/{diagram-CBK3MWTC-PSaSiYXz.js → diagram-CBK3MWTC-Ds4_tv6X.js} +1 -1
- package/dist/ui/dist/assets/{diagram-I5XIIV2D-BmU5X_hr.js → diagram-I5XIIV2D-DcoVkg45.js} +1 -1
- package/dist/ui/dist/assets/{diagram-ODIL2UCW-AAV39kUu.js → diagram-ODIL2UCW-T47FAA3k.js} +1 -1
- package/dist/ui/dist/assets/{diagram-PFBEJUXE-DWlV-oxd.js → diagram-PFBEJUXE-CRZcaVKP.js} +1 -1
- package/dist/ui/dist/assets/{diagram-RJA3I4VV-D8wHRCNX.js → diagram-RJA3I4VV-D6KXeUPg.js} +1 -1
- package/dist/ui/dist/assets/{diagram-TYUNFSNR-B_JQEsNY.js → diagram-TYUNFSNR-BHEIVyNN.js} +1 -1
- package/dist/ui/dist/assets/{dist-DxQIK_Ay.js → dist-DLlKIWBp.js} +1 -1
- package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-D83qV8mv.js → ebnfDiagram-V4P3TTPT-nUDvYRBV.js} +1 -1
- package/dist/ui/dist/assets/{elk-QONTBCTQ-BKFAKVSD.js → elk-QONTBCTQ-D21BcRzc.js} +1 -1
- package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-CHYRd4aM.js → erDiagram-KYQIGQAA-CTpHuftU.js} +1 -1
- package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-BDJDv3Zf.js → eventmodeling-K75KTNOO-B6JaKuDY.js} +1 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-BTeOUR-p.js +1 -0
- package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-gTHjYkFj.js → ganttDiagram-R7GPNTJY-D2-pMcnC.js} +1 -1
- package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-D4cWkxKB.js → gitGraph-VSP46ZUC-B2yxwhEo.js} +1 -1
- package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-D3M9wYMr.js → gitGraphDiagram-PDYL7XHY-DnrhBMH1.js} +1 -1
- package/dist/ui/dist/assets/index-D74Skifa.css +1 -0
- package/dist/ui/dist/assets/index-Dnte9tPd.js +136 -0
- package/dist/ui/dist/assets/{info-OHQRW6UA-B9y0ksVq.js → info-OHQRW6UA-D8oh6gvs.js} +1 -1
- package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-s4FMwb8r.js → infoDiagram-MVDRKAX5-BLSZjjgB.js} +1 -1
- package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-CnQdxybf.js → ishikawaDiagram-HPEHV2GE-BXiznLSn.js} +1 -1
- package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-I-9OpDWY.js → journeyDiagram-BOSHW7O7-BSYmVcO_.js} +1 -1
- package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-DqUbIdH4.js → kanban-definition-FNSA4HC5-COthkkzK.js} +1 -1
- package/dist/ui/dist/assets/{line-BELSui4d.js → line-6ND8OaOL.js} +1 -1
- package/dist/ui/dist/assets/{linear-77hmn2iQ.js → linear-DbWEPFJX.js} +1 -1
- package/dist/ui/dist/assets/{mermaid-parser.core-Bxa71wW-.js → mermaid-parser.core-Btsk294z.js} +3 -3
- package/dist/ui/dist/assets/{mermaid.core-DBI9F4bh.js → mermaid.core-B4llDSFO.js} +4 -4
- package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-BxXMOapP.js → mindmap-definition-6OY57UG3-hv2PfX-x.js} +1 -1
- package/dist/ui/dist/assets/{packet-JDAUHWVQ-DTT77cQh.js → packet-JDAUHWVQ-Dr4vBgjt.js} +1 -1
- package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-HzrOQPT5.js → pegDiagram-ECIUJNBX-DvlFov3N.js} +1 -1
- package/dist/ui/dist/assets/{pie-XZMESJXO-iHwL1Qw-.js → pie-XZMESJXO-C9QsqC4n.js} +1 -1
- package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-DZ1tLVjw.js → pieDiagram-GNMPY74T-CrrHOj67.js} +1 -1
- package/dist/ui/dist/assets/{prism-core-NtO0YF9C.js → prism-core-Cc8H7LPG.js} +1 -1
- package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-BjTi9H5L.js → quadrantDiagram-D57K4MC3-DQ7J6-ti.js} +1 -1
- package/dist/ui/dist/assets/{radar-ABXABTNO-D2hYgXSB.js → radar-ABXABTNO-BBBPaujh.js} +1 -1
- package/dist/ui/dist/assets/{railroad-I3PHUGI6-DpakXN74.js → railroad-I3PHUGI6-C-P6Igaz.js} +1 -1
- package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-B8ZFR3iy.js → railroad-abnf-LNEFI6M7-CAkorRLs.js} +1 -1
- package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-S1XUyTsC.js → railroad-ebnf-DWYD2UWJ-D8Hz6gCf.js} +1 -1
- package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-BbeHC_6K.js → railroad-peg-JR7BVNR7-D-XNMkOs.js} +1 -1
- package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-fd-2iyBo.js → railroadDiagram-Y72NRBL7-DE9LwxJQ.js} +1 -1
- package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-B9-HHKH-.js → requirementDiagram-EQYSW5RA-FrKFY6NJ.js} +1 -1
- package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-CeQN1ZEG.js → sankeyDiagram-SY24JHO5-BDxI6RDX.js} +1 -1
- package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-Vff7tx4t.js → sequenceDiagram-BLEHNHQU-pV1XcKX7.js} +1 -1
- package/dist/ui/dist/assets/{src-D5-ZzLwt.js → src-snNJTXXq.js} +1 -1
- package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-Bzn_ma9c.js → stateDiagram-v2-A6IESKWZ-BSc_5aAf.js} +1 -1
- package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-CevqLchV.js → swimlanes-B3ENRKFQ-DC6qUXLh.js} +1 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-F1HykV3o.js +8 -0
- package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-D9jSDGJI.js → timeline-definition-JXYKGLGB-C8L0Dxtu.js} +1 -1
- package/dist/ui/dist/assets/{treeView-D4JQ5SDB-CoXKj11m.js → treeView-D4JQ5SDB-DDyZIG5T.js} +1 -1
- package/dist/ui/dist/assets/{treemap-SAJKECNS-cHsansGe.js → treemap-SAJKECNS-CUaLgge0.js} +1 -1
- package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-CW9i3bW6.js → usecaseDiagram-WPOLBNNX-BHRBd5pV.js} +1 -1
- package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-DuU3qEKJ.js → vennDiagram-Q3Y3IDCW-Du4iwwkh.js} +1 -1
- package/dist/ui/dist/assets/{wardley-7MLQ67FV-dl9RgHUI.js → wardley-7MLQ67FV-DUgENI0s.js} +1 -1
- package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-BOXhrK-a.js → wardleyDiagram-IVAIDKWA-B86zOdke.js} +1 -1
- package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-CLQNGrh4.js → xychartDiagram-4URNHDU4-twq9irqO.js} +1 -1
- package/dist/ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/follow-me/SKILL.md +4 -4
- package/dist/ui/dist/assets/channel-CY2cQobm.js +0 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Bke3CfUH.js +0 -1
- package/dist/ui/dist/assets/index-BUOFihod.js +0 -134
- package/dist/ui/dist/assets/index-DLNJT0VS.css +0 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-Bkv8hAmN.js +0 -8
package/README.md
CHANGED
|
@@ -75,11 +75,16 @@ The red blob in the party hat is your branch tip. Nothing follow-me does moves i
|
|
|
75
75
|

|
|
76
76
|
*Cmd-click a name to jump to where it is defined, even in a later commit, and Alt+Left to come back. A definition the branch does not change opens as a peek.*
|
|
77
77
|
|
|
78
|
+
When your agent's answer names code, like `src/lib.js:4-5`, each place becomes a numbered chip. Hover the answer to tint those lines, click a chip to jump there and Alt+Left to come back. An answer that names several places is a walkthrough: press Enter on it, then `.` and `,` to step.
|
|
79
|
+
|
|
80
|
+

|
|
81
|
+
*A walkthrough. Each place the agent names is tinted and numbered on the code, the current step strongest, and the bar at the bottom says where you are.*
|
|
82
|
+
|
|
78
83
|

|
|
79
84
|
*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
85
|
|
|
81
|
-

|
|
87
|
+
*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. A transition is green with the number of tests that take it, or red and dashed when none does. When the tests run one at a time, the table names them, and a list names the sequences each step of which has a test but no one test drives. Click a state or a transition to jump to its code.*
|
|
83
88
|
|
|
84
89
|

|
|
85
90
|
*Diagrams sit full width under the commit or reply they explain, and open full size on click.*
|
|
@@ -87,7 +92,7 @@ The red blob in the party hat is your branch tip. Nothing follow-me does moves i
|
|
|
87
92
|

|
|
88
93
|
*Submit review. Each item carries the original branch's path and line, so your agent can post it to the merge request as is.*
|
|
89
94
|
|
|
90
|
-
Keys: `[` shows or hides the index, `j` and `k` move between commits, Cmd-click (Ctrl-click) goes to a definition and Alt+Left comes back, arrows and Shift+arrows pick rows inside a hunk, `c` comments on the pick, `v` marks a commit viewed, `g` shows or hides coverage and `x` jumps to the next uncovered line, `?` lists the rest. An unfinished review autosaves and comes back when the page is opened again.
|
|
95
|
+
Keys: `[` shows or hides the index, `j` and `k` move between commits, Cmd-click (Ctrl-click) goes to a definition and Alt+Left comes back, arrows and Shift+arrows pick rows inside a hunk, `c` comments on the pick, `v` marks a commit viewed, `g` shows or hides coverage and `x` jumps to the next uncovered line, `.` and `,` step through the agent's walkthrough, `?` lists the rest. An unfinished review autosaves and comes back when the page is opened again.
|
|
91
96
|
|
|
92
97
|
### Reviewing someone else's branch
|
|
93
98
|
|
package/dist/src/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { findSkillDir, InstallError, runInstall, runUninstall, syncSkills, } fro
|
|
|
13
13
|
import { naiveDraft } from "./naive.js";
|
|
14
14
|
import { isCommentId, isConcernId, isFields, OFFERS_PER_COMMIT, OFFERS_PER_PLAN, serializePlanFile, validatePlan, } from "./plan.js";
|
|
15
15
|
import { renderEvents, renderPrompt, renderReport, renderShownComment, renderShownConcern, renderShownDefinitions, renderShownUnit, reportItems, } from "./prompt.js";
|
|
16
|
+
import { parseRefText, refsIn, repoPathOfTyped, resolveRefs } from "./refs.js";
|
|
16
17
|
import { RegistrationError, sessionIdOf } from "./server.js";
|
|
17
18
|
import { BranchMovedError, cleanSessions, coverageAt, extractSession, MissingDraftError, NotARepositoryError, openSession, SessionFileError, SessionLockedError, sessionComments, sinceLastPlan, } from "./session.js";
|
|
18
19
|
import { checkSyntax } from "./syntax.js";
|
|
@@ -38,9 +39,9 @@ Other commands:
|
|
|
38
39
|
tip lines
|
|
39
40
|
explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
|
|
40
41
|
start a thread on the page without being asked
|
|
41
|
-
show <h_…|c_…|k_…|path:
|
|
42
|
+
show <h_…|c_…|k_…|path:a[-b][@n|@base]|name>
|
|
42
43
|
print a unit, a comment's thread, a concern, the
|
|
43
|
-
lines around path:
|
|
44
|
+
lines around path:a-b in commit n, or where a name
|
|
44
45
|
is defined, instead of reading files
|
|
45
46
|
report [--json] [--markdown] print the report of the latest review; --json lists
|
|
46
47
|
the items to post to a merge request
|
|
@@ -92,16 +93,17 @@ Interactive loop (while the review page is open):
|
|
|
92
93
|
reply <commentId> --body <text> [--diagram <file> [--map <file>]]
|
|
93
94
|
answer under a comment on the page; the diagram is
|
|
94
95
|
Mermaid source, the map JSON tying its states to
|
|
95
|
-
tip lines
|
|
96
|
+
tip lines. \`path:a-b[@n]\` in backticks in the body
|
|
97
|
+
points at code, and is refused if it does not exist
|
|
96
98
|
explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
|
|
97
99
|
start a thread on the page without being asked;
|
|
98
100
|
--lines are change lines of the unit, commits count
|
|
99
101
|
from 1; with no target it is about the whole plan.
|
|
100
102
|
watch leaves these out like your replies
|
|
101
|
-
show <h_…|c_…|k_…|path:
|
|
103
|
+
show <h_…|c_…|k_…|path:a[-b][@n|@base]|name> [--branch <name>]
|
|
102
104
|
print a unit with the commits that take it, a
|
|
103
105
|
comment with its rows and thread, a concern with its
|
|
104
|
-
lines, the lines around path:
|
|
106
|
+
lines, the lines around path:a-b at the tip, in
|
|
105
107
|
commit n of the plan, or in the base, or each place
|
|
106
108
|
a name is defined at the tip with the commit it
|
|
107
109
|
lands in
|
|
@@ -1529,11 +1531,13 @@ async function reply(cwd, args) {
|
|
|
1529
1531
|
const open = await openOnServer(session, null);
|
|
1530
1532
|
if (open === null)
|
|
1531
1533
|
return notOpen(session);
|
|
1534
|
+
const refs = await bodyRefs(session, open.summary.version, body);
|
|
1532
1535
|
const id = await postReply(open.port, open.summary, commentId, {
|
|
1533
1536
|
author: "agent",
|
|
1534
1537
|
body,
|
|
1535
1538
|
...(diagram === undefined ? {} : { diagram }),
|
|
1536
1539
|
...(map === undefined ? {} : { map }),
|
|
1540
|
+
...(refs.length === 0 ? {} : { refs }),
|
|
1537
1541
|
});
|
|
1538
1542
|
say(`replied ${id} to ${commentId}`);
|
|
1539
1543
|
next(`run follow-me again; ${STILL_WORKING}`);
|
|
@@ -1586,6 +1590,33 @@ async function readMap(session, file, diagram) {
|
|
|
1586
1590
|
throw new UsageError(`${file}:\n ${problems.join("\n ")}`);
|
|
1587
1591
|
return map;
|
|
1588
1592
|
}
|
|
1593
|
+
/**
|
|
1594
|
+
* The refs --body writes, resolved against plan `version` and the tip. Every one that does not
|
|
1595
|
+
* resolve is named at once, as --map's are.
|
|
1596
|
+
*/
|
|
1597
|
+
async function bodyRefs(session, version, body) {
|
|
1598
|
+
const specs = refsIn(body);
|
|
1599
|
+
if (specs.length === 0)
|
|
1600
|
+
return [];
|
|
1601
|
+
const { root, diff } = session;
|
|
1602
|
+
const otherChanged = new Set([...diff.units.values()].flatMap((u) => (diff.textFiles.has(u.path) ? [] : [u.path])));
|
|
1603
|
+
const asked = [...new Set(specs.map((s) => repoPathOfTyped(s.path)))].filter((p) => isRepoPath(p) && !diff.textFiles.has(p) && !otherChanged.has(p));
|
|
1604
|
+
const tip = asked.length === 0 ? new Set() : await pathsAt(root, diff.tip);
|
|
1605
|
+
const present = asked.filter((p) => tip.has(p));
|
|
1606
|
+
const unchanged = new Map([...(await blobsAt(root, diff.tip, present))].map(([p, bytes]) => [p, splitLines(bytes)]));
|
|
1607
|
+
const { refs, problems } = resolveRefs(specs, {
|
|
1608
|
+
tip: diff.tip,
|
|
1609
|
+
textFiles: diff.textFiles,
|
|
1610
|
+
otherChanged,
|
|
1611
|
+
unchanged,
|
|
1612
|
+
commits: projectedCommits(session, version),
|
|
1613
|
+
version,
|
|
1614
|
+
});
|
|
1615
|
+
if (problems.length > 0) {
|
|
1616
|
+
throw new UsageError(`--body points at code that does not resolve:\n ${problems.join("\n ")}`);
|
|
1617
|
+
}
|
|
1618
|
+
return refs;
|
|
1619
|
+
}
|
|
1589
1620
|
/** The Mermaid source in --diagram's file, or undefined without the flag. */
|
|
1590
1621
|
function diagramOf(args) {
|
|
1591
1622
|
const file = args.values.get("diagram");
|
|
@@ -1693,17 +1724,19 @@ async function explain(cwd, args) {
|
|
|
1693
1724
|
if (open === null)
|
|
1694
1725
|
return notOpen(session);
|
|
1695
1726
|
const target = explainTarget(session, open.summary.version, args);
|
|
1727
|
+
const refs = await bodyRefs(session, open.summary.version, body);
|
|
1696
1728
|
const id = await postComment(open.port, open.summary, {
|
|
1697
1729
|
...target,
|
|
1698
1730
|
body,
|
|
1699
1731
|
author: "agent",
|
|
1700
1732
|
...(diagram === undefined ? {} : { diagram }),
|
|
1733
|
+
...(refs.length === 0 ? {} : { refs }),
|
|
1701
1734
|
});
|
|
1702
1735
|
say(`explained ${id}`);
|
|
1703
1736
|
next(`run follow-me again; ${STILL_WORKING}`);
|
|
1704
1737
|
return 0;
|
|
1705
1738
|
}
|
|
1706
|
-
const SHOW_USAGE = "usage: follow-me show <h_…|c_…|k_…|path:
|
|
1739
|
+
const SHOW_USAGE = "usage: follow-me show <h_…|c_…|k_…|path:a[-b][@<commit>|@base]|name>";
|
|
1707
1740
|
/** Every comment the session has, with the plan version it was made on: live ones first. */
|
|
1708
1741
|
async function knownComments(session) {
|
|
1709
1742
|
const open = await openOnServer(session, null);
|
|
@@ -1717,14 +1750,16 @@ async function knownComments(session) {
|
|
|
1717
1750
|
}
|
|
1718
1751
|
const SHOW_MARGIN = 3;
|
|
1719
1752
|
/**
|
|
1720
|
-
* The lines around path:
|
|
1721
|
-
* base, or the tip when `at` is
|
|
1753
|
+
* The lines around path:a-b as commit `at` of the latest plan writes the file (1-based), the
|
|
1754
|
+
* base, or the tip when `at` is null. Read from the plan, so it works before any apply.
|
|
1722
1755
|
*/
|
|
1723
1756
|
function showLines(session, raw) {
|
|
1724
|
-
const
|
|
1725
|
-
|
|
1726
|
-
if (typed === undefined || lineRaw === undefined)
|
|
1757
|
+
const spec = parseRefText(raw);
|
|
1758
|
+
if (spec === null)
|
|
1727
1759
|
throw new UsageError(SHOW_USAGE);
|
|
1760
|
+
const { path: typed, start, end, at } = spec;
|
|
1761
|
+
if (end < start)
|
|
1762
|
+
throw new UsageError(`${raw} ends before it starts`);
|
|
1728
1763
|
const path = Buffer.from(typed, "utf8").toString("latin1");
|
|
1729
1764
|
const file = [...session.diff.textFiles.values()].find((f) => f.path === path);
|
|
1730
1765
|
if (file === undefined) {
|
|
@@ -1732,7 +1767,7 @@ function showLines(session, raw) {
|
|
|
1732
1767
|
}
|
|
1733
1768
|
let text;
|
|
1734
1769
|
let label;
|
|
1735
|
-
if (at ===
|
|
1770
|
+
if (at === null) {
|
|
1736
1771
|
text = file.tip?.bytes ?? null;
|
|
1737
1772
|
label = "the tip";
|
|
1738
1773
|
}
|
|
@@ -1743,7 +1778,7 @@ function showLines(session, raw) {
|
|
|
1743
1778
|
else {
|
|
1744
1779
|
const latest = session.plans().at(-1);
|
|
1745
1780
|
const commits = latest === undefined ? [] : projectedCommits(session, latest.version);
|
|
1746
|
-
const n =
|
|
1781
|
+
const n = at;
|
|
1747
1782
|
if (n < 1 || n > commits.length) {
|
|
1748
1783
|
throw new UsageError(`@${at} is not one of the ${commits.length} commits of the plan`);
|
|
1749
1784
|
}
|
|
@@ -1762,18 +1797,18 @@ function showLines(session, raw) {
|
|
|
1762
1797
|
const lines = Buffer.from(text, "latin1").toString("utf8").split("\n");
|
|
1763
1798
|
if (lines.at(-1) === "")
|
|
1764
1799
|
lines.pop();
|
|
1765
|
-
|
|
1766
|
-
if (line < 1 || line > lines.length) {
|
|
1800
|
+
if (start < 1 || end > lines.length) {
|
|
1767
1801
|
return `${typed} has ${plural(lines.length, "line")} in ${label}\n`;
|
|
1768
1802
|
}
|
|
1769
|
-
const from = Math.max(1,
|
|
1770
|
-
const to = Math.min(lines.length,
|
|
1803
|
+
const from = Math.max(1, start - SHOW_MARGIN);
|
|
1804
|
+
const to = Math.min(lines.length, end + SHOW_MARGIN);
|
|
1771
1805
|
const width = String(to).length;
|
|
1772
1806
|
const rows = lines.slice(from - 1, to).map((l, i) => {
|
|
1773
1807
|
const n = from + i;
|
|
1774
|
-
return `${n
|
|
1808
|
+
return `${n >= start && n <= end ? ">" : " "} ${String(n).padStart(width)} ${l}`;
|
|
1775
1809
|
});
|
|
1776
|
-
|
|
1810
|
+
const span = end > start ? `${start}-${end}` : `${start}`;
|
|
1811
|
+
return `${typed}:${span} in ${label}\n${rows.join("\n")}\n`;
|
|
1777
1812
|
}
|
|
1778
1813
|
/** Expands a ref the agent was given to the lines and thread it names, so no file read is needed. */
|
|
1779
1814
|
async function showRef(cwd, args) {
|
package/dist/src/comments.js
CHANGED
|
@@ -148,6 +148,55 @@ export function mapOffTip(map, lines, show = (path) => path) {
|
|
|
148
148
|
...(map.transitions ?? []).flatMap((t, i) => t.at.flatMap((loc, j) => check(loc, `map.transitions[${i}].at[${j}]`))),
|
|
149
149
|
];
|
|
150
150
|
}
|
|
151
|
+
function parseSpan(v) {
|
|
152
|
+
if (v === null)
|
|
153
|
+
return null;
|
|
154
|
+
if (!isFields(v) || !isLine(v.start) || !isLine(v.end) || v.end < v.start) {
|
|
155
|
+
return "expected null or { start, end } with 1 <= start <= end";
|
|
156
|
+
}
|
|
157
|
+
return { start: v.start, end: v.end };
|
|
158
|
+
}
|
|
159
|
+
function parseCodeRef(v) {
|
|
160
|
+
if (!isFields(v))
|
|
161
|
+
return "expected an object";
|
|
162
|
+
const { n, path, start, end, at, quote, tip } = v;
|
|
163
|
+
if (!isLine(n))
|
|
164
|
+
return "n is not a number from 1";
|
|
165
|
+
if (typeof path !== "string" || !isRepoPath(path))
|
|
166
|
+
return "path is not a repo path";
|
|
167
|
+
if (!isLine(start) || !isLine(end) || end < start)
|
|
168
|
+
return "expected 1 <= start <= end";
|
|
169
|
+
if (at !== null && at !== "base" && !isLine(at))
|
|
170
|
+
return 'at is not a commit, "base" or null';
|
|
171
|
+
if (!isArray(quote) || !quote.every(isString))
|
|
172
|
+
return "quote is not a list of strings";
|
|
173
|
+
if (typeof tip !== "string" || !isSha(tip))
|
|
174
|
+
return "tip is not a commit sha";
|
|
175
|
+
const lines = parseSpan(v.lines);
|
|
176
|
+
const old = parseSpan(v.old);
|
|
177
|
+
if (typeof lines === "string")
|
|
178
|
+
return `lines: ${lines}`;
|
|
179
|
+
if (typeof old === "string")
|
|
180
|
+
return `old: ${old}`;
|
|
181
|
+
return { n, path, start, end, at, quote: quote.filter(isString), tip, lines, old };
|
|
182
|
+
}
|
|
183
|
+
/** refs as stored beside an agent's words: absent, or a list numbered 1, 2, 3 in order. */
|
|
184
|
+
function parseCodeRefs(v) {
|
|
185
|
+
if (v === undefined)
|
|
186
|
+
return [];
|
|
187
|
+
if (!isArray(v))
|
|
188
|
+
return "refs is not a list";
|
|
189
|
+
const refs = [];
|
|
190
|
+
for (const [i, raw] of v.entries()) {
|
|
191
|
+
const ref = parseCodeRef(raw);
|
|
192
|
+
if (typeof ref === "string")
|
|
193
|
+
return `refs[${i}]: ${ref}`;
|
|
194
|
+
if (ref.n !== i + 1)
|
|
195
|
+
return `refs[${i}]: n is not ${i + 1}`;
|
|
196
|
+
refs.push(ref);
|
|
197
|
+
}
|
|
198
|
+
return refs;
|
|
199
|
+
}
|
|
151
200
|
/** A stored reply. Replies a client posts go through parseNewReply, which mints id and time. */
|
|
152
201
|
export function parseReply(v) {
|
|
153
202
|
if (!isFields(v)) {
|
|
@@ -165,32 +214,43 @@ export function parseReply(v) {
|
|
|
165
214
|
const map = v.map === undefined ? undefined : parseStateMap(v.map, v.diagram);
|
|
166
215
|
if (typeof map === "string")
|
|
167
216
|
return map;
|
|
217
|
+
const refs = parseCodeRefs(v.refs);
|
|
218
|
+
if (typeof refs === "string")
|
|
219
|
+
return refs;
|
|
168
220
|
return {
|
|
169
221
|
id: v.id,
|
|
170
222
|
author: v.author,
|
|
171
223
|
body: v.body,
|
|
172
224
|
...(v.diagram === undefined ? {} : { diagram: v.diagram }),
|
|
173
225
|
...(map === undefined ? {} : { map }),
|
|
226
|
+
...(refs.length === 0 ? {} : { refs }),
|
|
174
227
|
createdAt: v.createdAt,
|
|
175
228
|
};
|
|
176
229
|
}
|
|
177
230
|
/**
|
|
178
231
|
* 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.
|
|
232
|
+
* empty only when the diagram is the answer. Only the agent's words carry refs.
|
|
180
233
|
*/
|
|
181
234
|
export function parseNewReply(v) {
|
|
182
|
-
if (!isFields(v))
|
|
235
|
+
if (!isFields(v)) {
|
|
183
236
|
return "the reply is not an object";
|
|
237
|
+
}
|
|
184
238
|
if (v.author !== "agent" && v.author !== "reviewer")
|
|
185
239
|
return 'author is not "agent" or "reviewer"';
|
|
186
240
|
if (typeof v.body !== "string")
|
|
187
241
|
return "body is not a string";
|
|
242
|
+
const refs = parseCodeRefs(v.refs);
|
|
243
|
+
if (typeof refs === "string")
|
|
244
|
+
return refs;
|
|
245
|
+
if (refs.length > 0 && v.author !== "agent")
|
|
246
|
+
return "refs are only for the agent's words";
|
|
247
|
+
const withRefs = refs.length === 0 ? {} : { refs };
|
|
188
248
|
if (v.diagram === undefined) {
|
|
189
249
|
if (v.map !== undefined)
|
|
190
250
|
return "map needs the diagram it ties to code";
|
|
191
251
|
if (v.body.trim() === "")
|
|
192
252
|
return "body is empty; a reply needs words or a diagram";
|
|
193
|
-
return { author: v.author, body: v.body };
|
|
253
|
+
return { author: v.author, body: v.body, ...withRefs };
|
|
194
254
|
}
|
|
195
255
|
if (typeof v.diagram !== "string")
|
|
196
256
|
return "diagram is not a string";
|
|
@@ -205,6 +265,7 @@ export function parseNewReply(v) {
|
|
|
205
265
|
body: v.body,
|
|
206
266
|
diagram: v.diagram,
|
|
207
267
|
...(map === undefined ? {} : { map }),
|
|
268
|
+
...withRefs,
|
|
208
269
|
};
|
|
209
270
|
}
|
|
210
271
|
function isString(v) {
|
|
@@ -262,6 +323,11 @@ export function parseComment(v) {
|
|
|
262
323
|
return "offer is only for a reviewer's question";
|
|
263
324
|
}
|
|
264
325
|
}
|
|
326
|
+
const refs = parseCodeRefs(v.refs);
|
|
327
|
+
if (typeof refs === "string")
|
|
328
|
+
return refs;
|
|
329
|
+
if (refs.length > 0 && v.author !== "agent")
|
|
330
|
+
return "refs are only for an agent's comment";
|
|
265
331
|
const replies = parseList(v.replies, parseReply, "replies");
|
|
266
332
|
if (typeof replies === "string")
|
|
267
333
|
return replies;
|
|
@@ -281,6 +347,7 @@ export function parseComment(v) {
|
|
|
281
347
|
...(replies.length === 0 ? {} : { replies }),
|
|
282
348
|
...(v.author === "agent" ? { author: "agent" } : {}),
|
|
283
349
|
...(v.diagram === undefined ? {} : { diagram: v.diagram }),
|
|
350
|
+
...(refs.length === 0 ? {} : { refs }),
|
|
284
351
|
...(offer === undefined ? {} : { offer }),
|
|
285
352
|
};
|
|
286
353
|
}
|
package/dist/src/coverage.js
CHANGED
|
@@ -15,9 +15,12 @@ export class LcovError extends Error {
|
|
|
15
15
|
const RECORD = /^([A-Z][A-Z0-9_]*):(.*)$/;
|
|
16
16
|
const DA = /^([1-9][0-9]*),([0-9]+)(?:,.*)?$/;
|
|
17
17
|
const BRDA = /^([1-9][0-9]*),([^,]+),(.+),(-|[0-9]+)$/;
|
|
18
|
+
/** FN:<start>,<name>, or lcov 2's FN:<start>,<end>,<name>. */
|
|
19
|
+
const FN = /^([1-9][0-9]*),(?:([1-9][0-9]*),)?(.+)$/;
|
|
18
20
|
/**
|
|
19
21
|
* Reads lcov's tracefile format: TN names the test of the records after it, SF opens a file, DA
|
|
20
|
-
* and BRDA give line and branch hits, end_of_record closes it. Other
|
|
22
|
+
* and BRDA give line and branch hits, FN a function's place, end_of_record closes it. Other
|
|
23
|
+
* records are skipped.
|
|
21
24
|
*/
|
|
22
25
|
export function parseLcov(text) {
|
|
23
26
|
const records = [];
|
|
@@ -50,10 +53,10 @@ export function parseLcov(text) {
|
|
|
50
53
|
close();
|
|
51
54
|
if (value === "")
|
|
52
55
|
throw new LcovError(n, "SF needs a path");
|
|
53
|
-
open = { path: value, test, lines: new Map(), branches: new Map() };
|
|
56
|
+
open = { path: value, test, lines: new Map(), branches: new Map(), functions: [] };
|
|
54
57
|
continue;
|
|
55
58
|
}
|
|
56
|
-
if (key !== "DA" && key !== "BRDA")
|
|
59
|
+
if (key !== "DA" && key !== "BRDA" && key !== "FN")
|
|
57
60
|
continue;
|
|
58
61
|
if (open === null)
|
|
59
62
|
throw new LcovError(n, `${key} comes before any SF`);
|
|
@@ -65,15 +68,27 @@ export function parseLcov(text) {
|
|
|
65
68
|
open.lines.set(line, (open.lines.get(line) ?? 0) + Number(da[2]));
|
|
66
69
|
continue;
|
|
67
70
|
}
|
|
71
|
+
if (key === "FN") {
|
|
72
|
+
const fn = FN.exec(value);
|
|
73
|
+
if (fn === null)
|
|
74
|
+
throw new LcovError(n, `FN needs <line>,<name>, got "${value}"`);
|
|
75
|
+
const start = Number(fn[1]);
|
|
76
|
+
const end = fn[2] === undefined ? undefined : Number(fn[2]);
|
|
77
|
+
const name = fn[3] ?? "";
|
|
78
|
+
if (end !== undefined && end < start)
|
|
79
|
+
throw new LcovError(n, "FN ends before it starts");
|
|
80
|
+
open.functions.push(end === undefined ? { name, start } : { name, start, end });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
68
83
|
const br = BRDA.exec(value);
|
|
69
84
|
if (br === null) {
|
|
70
85
|
throw new LcovError(n, `BRDA needs <line>,<block>,<branch>,<taken>, got "${value}"`);
|
|
71
86
|
}
|
|
72
87
|
const line = Number(br[1]);
|
|
73
88
|
const id = `${br[2]},${br[3]}`;
|
|
74
|
-
const taken = br[4]
|
|
89
|
+
const taken = br[4] === "-" ? 0 : Number(br[4]);
|
|
75
90
|
const branches = open.branches.get(line) ?? new Map();
|
|
76
|
-
branches.set(id, (branches.get(id) ??
|
|
91
|
+
branches.set(id, (branches.get(id) ?? 0) + taken);
|
|
77
92
|
open.branches.set(line, branches);
|
|
78
93
|
}
|
|
79
94
|
close();
|
|
@@ -110,10 +125,42 @@ export function tipPlacer(root, cwd, atTip) {
|
|
|
110
125
|
return null;
|
|
111
126
|
};
|
|
112
127
|
}
|
|
128
|
+
/** Adds a record's branch counts into `into`, keeping lcov's branch order per line. */
|
|
129
|
+
function countInto(into, path, record) {
|
|
130
|
+
const joined = into.get(path) ?? new Map();
|
|
131
|
+
for (const [line, ids] of record.branches) {
|
|
132
|
+
const on = joined.get(line) ?? new Map();
|
|
133
|
+
for (const [id, n] of ids)
|
|
134
|
+
on.set(id, (on.get(id) ?? 0) + n);
|
|
135
|
+
joined.set(line, on);
|
|
136
|
+
}
|
|
137
|
+
into.set(path, joined);
|
|
138
|
+
}
|
|
139
|
+
function countsOf(counting) {
|
|
140
|
+
const out = new Map();
|
|
141
|
+
for (const [path, byLine] of counting) {
|
|
142
|
+
if (byLine.size === 0)
|
|
143
|
+
continue;
|
|
144
|
+
out.set(path, sorted(new Map([...byLine].map(([line, ids]) => [line, [...ids.values()]]))));
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
/** Functions of both lists, once each by start and name, in line order. */
|
|
149
|
+
function joinFunctions(a, b) {
|
|
150
|
+
const seen = new Map();
|
|
151
|
+
for (const f of [...a, ...b]) {
|
|
152
|
+
const key = `${f.start}|${f.name}`;
|
|
153
|
+
const had = seen.get(key);
|
|
154
|
+
if (had === undefined || (had.end === undefined && f.end !== undefined))
|
|
155
|
+
seen.set(key, f);
|
|
156
|
+
}
|
|
157
|
+
return [...seen.values()].sort((x, y) => x.start - y.start);
|
|
158
|
+
}
|
|
113
159
|
/** Sums hits and joins branches over records of the same file; drops what `place` rejects. */
|
|
114
160
|
export function runOf(records, place) {
|
|
115
161
|
const lines = new Map();
|
|
116
162
|
const branches = new Map();
|
|
163
|
+
const functions = new Map();
|
|
117
164
|
const tests = new Map();
|
|
118
165
|
for (const record of records) {
|
|
119
166
|
const path = place(record.path);
|
|
@@ -123,37 +170,42 @@ export function runOf(records, place) {
|
|
|
123
170
|
for (const [line, n] of record.lines)
|
|
124
171
|
hits.set(line, (hits.get(line) ?? 0) + n);
|
|
125
172
|
lines.set(path, hits);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const on = joined.get(line) ?? new Map();
|
|
129
|
-
for (const [id, taken] of ids)
|
|
130
|
-
on.set(id, (on.get(id) ?? false) || taken);
|
|
131
|
-
joined.set(line, on);
|
|
132
|
-
}
|
|
133
|
-
branches.set(path, joined);
|
|
173
|
+
countInto(branches, path, record);
|
|
174
|
+
functions.set(path, joinFunctions(functions.get(path) ?? [], record.functions));
|
|
134
175
|
if (record.test === null)
|
|
135
176
|
continue;
|
|
136
|
-
const ran = tests.get(record.test) ?? new Map();
|
|
137
|
-
const set = ran.get(path) ?? new Set();
|
|
177
|
+
const ran = tests.get(record.test) ?? { lines: new Map(), branches: new Map() };
|
|
178
|
+
const set = ran.lines.get(path) ?? new Set();
|
|
138
179
|
for (const [line, n] of record.lines)
|
|
139
180
|
if (n > 0)
|
|
140
181
|
set.add(line);
|
|
141
|
-
ran.set(path, set);
|
|
182
|
+
ran.lines.set(path, set);
|
|
183
|
+
countInto(ran.branches, path, record);
|
|
142
184
|
tests.set(record.test, ran);
|
|
143
185
|
}
|
|
144
186
|
const files = new Map();
|
|
145
187
|
for (const [path, hits] of lines) {
|
|
146
188
|
const counted = new Map();
|
|
147
189
|
for (const [line, ids] of branches.get(path) ?? []) {
|
|
148
|
-
|
|
190
|
+
const n = [...ids.values()];
|
|
191
|
+
counted.set(line, [n.filter((c) => c > 0).length, n.length]);
|
|
149
192
|
}
|
|
150
|
-
|
|
193
|
+
const fns = functions.get(path) ?? [];
|
|
194
|
+
files.set(path, {
|
|
195
|
+
lines: sorted(hits),
|
|
196
|
+
branches: sorted(counted),
|
|
197
|
+
...(fns.length === 0 ? {} : { functions: fns }),
|
|
198
|
+
});
|
|
151
199
|
}
|
|
152
200
|
return {
|
|
153
201
|
files,
|
|
202
|
+
counts: countsOf(branches),
|
|
154
203
|
tests: new Map([...tests].map(([id, ran]) => [
|
|
155
204
|
id,
|
|
156
|
-
|
|
205
|
+
{
|
|
206
|
+
lines: new Map([...ran.lines].map(([path, set]) => [path, [...set].sort((a, b) => a - b)])),
|
|
207
|
+
branches: countsOf(ran.branches),
|
|
208
|
+
},
|
|
157
209
|
])),
|
|
158
210
|
};
|
|
159
211
|
}
|
|
@@ -184,7 +236,12 @@ function mergeMax(a, b) {
|
|
|
184
236
|
const [t, n] = branches.get(l) ?? [0, 0];
|
|
185
237
|
branches.set(l, [Math.max(taken, t), Math.max(total, n)]);
|
|
186
238
|
}
|
|
187
|
-
|
|
239
|
+
const functions = joinFunctions(had.functions ?? [], f.functions ?? []);
|
|
240
|
+
out.set(path, {
|
|
241
|
+
lines: sorted(lines),
|
|
242
|
+
branches: sorted(branches),
|
|
243
|
+
...(functions.length === 0 ? {} : { functions }),
|
|
244
|
+
});
|
|
188
245
|
}
|
|
189
246
|
return out;
|
|
190
247
|
}
|
|
@@ -197,7 +254,7 @@ export const PREVIEW_LINES = 8;
|
|
|
197
254
|
export function recordRun(prior, run, at) {
|
|
198
255
|
const { tip, takenAt, test } = at;
|
|
199
256
|
if (test === undefined) {
|
|
200
|
-
const tests = new Map([...run.tests].map(([id,
|
|
257
|
+
const tests = new Map([...run.tests].map(([id, ran]) => [id, testEntry(ran)]));
|
|
201
258
|
return { tip, takenAt, files: run.files, tests };
|
|
202
259
|
}
|
|
203
260
|
const base = prior !== null && prior.tip === tip ? prior : null;
|
|
@@ -205,12 +262,16 @@ export function recordRun(prior, run, at) {
|
|
|
205
262
|
const entry = {
|
|
206
263
|
...(test.at === undefined ? {} : { at: test.at }),
|
|
207
264
|
...(test.preview === undefined ? {} : { preview: test.preview }),
|
|
208
|
-
lines: hitLines(run.files),
|
|
265
|
+
...testEntry({ lines: hitLines(run.files), branches: run.counts }),
|
|
209
266
|
};
|
|
210
267
|
tests.set(test.id, entry);
|
|
211
268
|
const files = base === null ? run.files : mergeMax(base.files, run.files);
|
|
212
269
|
return { tip, takenAt, files, tests };
|
|
213
270
|
}
|
|
271
|
+
/** A test's lines, and its branch counts when the run had any. */
|
|
272
|
+
function testEntry(ran) {
|
|
273
|
+
return { lines: ran.lines, ...(ran.branches.size === 0 ? {} : { branches: ran.branches }) };
|
|
274
|
+
}
|
|
214
275
|
/**
|
|
215
276
|
* Per file, the branch's added rows that lcov calls executable, and how many ran. Every added
|
|
216
277
|
* row is a tip line, which is where the run was taken. Files without such rows are left out.
|
|
@@ -278,6 +339,7 @@ export function serializeCoverage(c) {
|
|
|
278
339
|
{
|
|
279
340
|
lines: Object.fromEntries(f.lines),
|
|
280
341
|
...(f.branches.size === 0 ? {} : { branches: Object.fromEntries(f.branches) }),
|
|
342
|
+
...(f.functions === undefined ? {} : { functions: f.functions }),
|
|
281
343
|
},
|
|
282
344
|
]));
|
|
283
345
|
const tests = Object.fromEntries([...c.tests].map(([id, t]) => [
|
|
@@ -286,6 +348,11 @@ export function serializeCoverage(c) {
|
|
|
286
348
|
...(t.at === undefined ? {} : { at: t.at }),
|
|
287
349
|
...(t.preview === undefined ? {} : { preview: [...t.preview] }),
|
|
288
350
|
lines: Object.fromEntries([...t.lines].map(([p, ls]) => [p, [...ls]])),
|
|
351
|
+
...(t.branches === undefined
|
|
352
|
+
? {}
|
|
353
|
+
: {
|
|
354
|
+
branches: Object.fromEntries([...t.branches].map(([p, byLine]) => [p, Object.fromEntries(byLine)])),
|
|
355
|
+
}),
|
|
289
356
|
},
|
|
290
357
|
]));
|
|
291
358
|
return { tip: c.tip, takenAt: c.takenAt, files, ...(c.tests.size === 0 ? {} : { tests }) };
|
|
@@ -321,6 +388,52 @@ export function parseLocArg(raw, place) {
|
|
|
321
388
|
const end = m[3] === undefined ? start : Number(m[3]);
|
|
322
389
|
return end < start ? `--at ${raw} runs backwards` : { path, start, end };
|
|
323
390
|
}
|
|
391
|
+
function parseFunction(v) {
|
|
392
|
+
if (!isFields(v))
|
|
393
|
+
return null;
|
|
394
|
+
const { name, start, end } = v;
|
|
395
|
+
if (typeof name !== "string" || !isLine(start))
|
|
396
|
+
return null;
|
|
397
|
+
if (end === undefined)
|
|
398
|
+
return { name, start };
|
|
399
|
+
return isLine(end) && end >= start ? { name, start, end } : null;
|
|
400
|
+
}
|
|
401
|
+
function parseFunctions(v) {
|
|
402
|
+
if (v === undefined)
|
|
403
|
+
return undefined;
|
|
404
|
+
if (!Array.isArray(v))
|
|
405
|
+
return "functions is not a list";
|
|
406
|
+
const out = [];
|
|
407
|
+
for (const raw of v) {
|
|
408
|
+
const f = parseFunction(raw);
|
|
409
|
+
if (f === null)
|
|
410
|
+
return "a function is not a { name, start, end? }";
|
|
411
|
+
out.push(f);
|
|
412
|
+
}
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
function parseCounts(v) {
|
|
416
|
+
if (v === undefined)
|
|
417
|
+
return undefined;
|
|
418
|
+
const files = entries(v);
|
|
419
|
+
if (files === null)
|
|
420
|
+
return "branches is not an object";
|
|
421
|
+
const out = new Map();
|
|
422
|
+
for (const [path, raw] of files) {
|
|
423
|
+
const lines = entries(raw);
|
|
424
|
+
if (!isRepoPath(path) || lines === null)
|
|
425
|
+
return `branches of ${path} are not by line`;
|
|
426
|
+
const byLine = new Map();
|
|
427
|
+
for (const [key, counts] of lines) {
|
|
428
|
+
if (!LINE_KEY.test(key) || !Array.isArray(counts) || !counts.every(isCount)) {
|
|
429
|
+
return `branches of ${path}:${key} are not counts`;
|
|
430
|
+
}
|
|
431
|
+
byLine.set(Number(key), counts.filter(isCount));
|
|
432
|
+
}
|
|
433
|
+
out.set(path, sorted(byLine));
|
|
434
|
+
}
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
324
437
|
function parseFile(v) {
|
|
325
438
|
if (!isFields(v))
|
|
326
439
|
return "a file is not an object";
|
|
@@ -347,7 +460,14 @@ function parseFile(v) {
|
|
|
347
460
|
}
|
|
348
461
|
branches.set(Number(key), [taken, total]);
|
|
349
462
|
}
|
|
350
|
-
|
|
463
|
+
const functions = parseFunctions(v.functions);
|
|
464
|
+
if (typeof functions === "string")
|
|
465
|
+
return functions;
|
|
466
|
+
return {
|
|
467
|
+
lines: sorted(hits),
|
|
468
|
+
branches: sorted(branches),
|
|
469
|
+
...(functions === undefined ? {} : { functions }),
|
|
470
|
+
};
|
|
351
471
|
}
|
|
352
472
|
function parseTest(v) {
|
|
353
473
|
if (!isFields(v))
|
|
@@ -370,10 +490,14 @@ function parseTest(v) {
|
|
|
370
490
|
}
|
|
371
491
|
lines.set(path, list);
|
|
372
492
|
}
|
|
493
|
+
const branches = parseCounts(v.branches);
|
|
494
|
+
if (typeof branches === "string")
|
|
495
|
+
return branches;
|
|
373
496
|
return {
|
|
374
497
|
...(at === undefined ? {} : { at }),
|
|
375
498
|
...(preview === undefined ? {} : { preview: preview.filter((l) => typeof l === "string") }),
|
|
376
499
|
lines,
|
|
500
|
+
...(branches === undefined ? {} : { branches }),
|
|
377
501
|
};
|
|
378
502
|
}
|
|
379
503
|
/** The one place coverage.json becomes a Coverage. */
|