@shiyuphan/follow-me 0.6.0 → 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 +7 -1
- package/dist/src/cli.js +210 -23
- package/dist/src/comments.js +152 -8
- package/dist/src/coverage.js +411 -0
- package/dist/src/git.js +18 -0
- package/dist/src/host.js +27 -0
- package/dist/src/plan.js +128 -6
- package/dist/src/prompt.js +113 -25
- package/dist/src/server.js +115 -11
- package/dist/src/session.js +34 -0
- package/dist/src/types.js +2 -0
- package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-fYSYLdMl.js → abnfDiagram-WEYOIA3C-Bp14ygmz.js} +1 -1
- package/dist/ui/dist/assets/{arc-DGuGSnwa.js → arc-DUR6uzhI.js} +1 -1
- package/dist/ui/dist/assets/{architecture-WOLXFQ4H-VFox_hV1.js → architecture-WOLXFQ4H-CyfnP65O.js} +1 -1
- package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-CBVK3nHZ.js → architectureDiagram-ZHVYKT53-dJTicIk2.js} +1 -1
- package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-BFdsIWEr.js → blockDiagram-RJAE7QA3-BYiqh5Ib.js} +1 -1
- package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-BGntcPId.js → c4Diagram-XDPPPE54-DHHPQ7xE.js} +1 -1
- package/dist/ui/dist/assets/channel-CY2cQobm.js +1 -0
- package/dist/ui/dist/assets/{chunk-2E7WHVMD-CNMuK9zj.js → chunk-2E7WHVMD-BODm_awL.js} +1 -1
- package/dist/ui/dist/assets/{chunk-4EMNKR2T-ByeTTJWS.js → chunk-4EMNKR2T-CpZfjf2T.js} +1 -1
- package/dist/ui/dist/assets/{chunk-55XCU76K-99xE7cLH.js → chunk-55XCU76K-CoP2pJTQ.js} +1 -1
- package/dist/ui/dist/assets/{chunk-7S6XSTFO-ZVmT7eMZ.js → chunk-7S6XSTFO-CaQcUvXY.js} +1 -1
- package/dist/ui/dist/assets/{chunk-AQDTSKZB-B981JORL.js → chunk-AQDTSKZB-BZ9Czvbu.js} +1 -1
- package/dist/ui/dist/assets/{chunk-DSNLSHJZ-B2z7FX4h.js → chunk-DSNLSHJZ-CNjUo110.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HAGFOTED-CHFbBS-4.js → chunk-HAGFOTED-BTmBE_D3.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HNE4Q36P-rq11xv9X.js → chunk-HNE4Q36P-D2cDuT0C.js} +1 -1
- package/dist/ui/dist/assets/{chunk-NQOKRVIF-BYmg9kSX.js → chunk-NQOKRVIF-AZcemFgG.js} +1 -1
- package/dist/ui/dist/assets/{chunk-UHW7PFBP-JvL9IXWg.js → chunk-UHW7PFBP-BszwjUqG.js} +1 -1
- package/dist/ui/dist/assets/{chunk-VQ5W62QI-CYMQaAKn.js → chunk-VQ5W62QI-vFSenYSe.js} +1 -1
- package/dist/ui/dist/assets/{chunk-WQQL4CDK-CXnR0PHT.js → chunk-WQQL4CDK-DZR6is1Q.js} +1 -1
- package/dist/ui/dist/assets/{chunk-XXDRQBXY-CDUWJCXi.js → chunk-XXDRQBXY-CACFh-si.js} +1 -1
- package/dist/ui/dist/assets/{chunk-YZO67HFG-mZfXIx0h.js → chunk-YZO67HFG-DRRTjGuW.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZAUX2LTX-DBxVTCHX.js → chunk-ZAUX2LTX-Bxuy-Roa.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZCHMQSOR-DrWA4IBo.js → chunk-ZCHMQSOR-rYXk__1o.js} +1 -1
- package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-Beah5uxA.js → classDiagram-v2-4MYWR3UA-RiF44tTz.js} +1 -1
- package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-UaE-jnHk.js → cose-bilkent-JH36ORCC-0oiQoThS.js} +1 -1
- package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-DKfGaCR8.js → cynefin-EF2NZ3EQ-BAssFK5K.js} +1 -1
- package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-tTUzTs3D.js → cynefinDiagram-WF24UDRP-Bmd05bak.js} +1 -1
- package/dist/ui/dist/assets/{dagre-NK4GIZVX-jSn35_T7.js → dagre-NK4GIZVX-QJkLjPxj.js} +1 -1
- package/dist/ui/dist/assets/{diagram-CBK3MWTC-7W2uw8Zc.js → diagram-CBK3MWTC-PSaSiYXz.js} +1 -1
- package/dist/ui/dist/assets/{diagram-I5XIIV2D-CcfyQVh9.js → diagram-I5XIIV2D-BmU5X_hr.js} +1 -1
- package/dist/ui/dist/assets/{diagram-ODIL2UCW-BjGvaLis.js → diagram-ODIL2UCW-AAV39kUu.js} +1 -1
- package/dist/ui/dist/assets/{diagram-PFBEJUXE-jvdCrt8B.js → diagram-PFBEJUXE-DWlV-oxd.js} +1 -1
- package/dist/ui/dist/assets/{diagram-RJA3I4VV-D65_Vpi_.js → diagram-RJA3I4VV-D8wHRCNX.js} +1 -1
- package/dist/ui/dist/assets/{diagram-TYUNFSNR-C52roQBE.js → diagram-TYUNFSNR-B_JQEsNY.js} +1 -1
- package/dist/ui/dist/assets/{dist-BJd4jp9Y.js → dist-DxQIK_Ay.js} +1 -1
- package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-DXysoIBq.js → ebnfDiagram-V4P3TTPT-D83qV8mv.js} +1 -1
- package/dist/ui/dist/assets/{elk-QONTBCTQ-BVZcsgV3.js → elk-QONTBCTQ-BKFAKVSD.js} +1 -1
- package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-BJnT4fVU.js → erDiagram-KYQIGQAA-CHYRd4aM.js} +1 -1
- package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-C5MzxCMY.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-CZRrnuah.js → ganttDiagram-R7GPNTJY-gTHjYkFj.js} +1 -1
- package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-l58aGSkh.js → gitGraph-VSP46ZUC-D4cWkxKB.js} +1 -1
- package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-D4ks1WO-.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-BSqfzkdk.js → info-OHQRW6UA-B9y0ksVq.js} +1 -1
- package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-B6NgVF_N.js → infoDiagram-MVDRKAX5-s4FMwb8r.js} +1 -1
- package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-DvtfssEZ.js → ishikawaDiagram-HPEHV2GE-CnQdxybf.js} +1 -1
- package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-DM64gT5v.js → journeyDiagram-BOSHW7O7-I-9OpDWY.js} +1 -1
- package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-AliyRJHy.js → kanban-definition-FNSA4HC5-DqUbIdH4.js} +1 -1
- package/dist/ui/dist/assets/{line-CHncXbKO.js → line-BELSui4d.js} +1 -1
- package/dist/ui/dist/assets/{linear-BWEWydI0.js → linear-77hmn2iQ.js} +1 -1
- package/dist/ui/dist/assets/{mermaid-parser.core-DGpOP8AM.js → mermaid-parser.core-Bxa71wW-.js} +3 -3
- package/dist/ui/dist/assets/{mermaid.core-BJtBFbJx.js → mermaid.core-DBI9F4bh.js} +4 -4
- package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-NjavnWBi.js → mindmap-definition-6OY57UG3-BxXMOapP.js} +1 -1
- package/dist/ui/dist/assets/{packet-JDAUHWVQ-zuhpVsME.js → packet-JDAUHWVQ-DTT77cQh.js} +1 -1
- package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-sL4grM97.js → pegDiagram-ECIUJNBX-HzrOQPT5.js} +1 -1
- package/dist/ui/dist/assets/{pie-XZMESJXO-C9l1Mp09.js → pie-XZMESJXO-iHwL1Qw-.js} +1 -1
- package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-rlRBr2U4.js → pieDiagram-GNMPY74T-DZ1tLVjw.js} +1 -1
- package/dist/ui/dist/assets/{prism-core-DmuST30i.js → prism-core-NtO0YF9C.js} +1 -1
- package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-4oQMip2Q.js → quadrantDiagram-D57K4MC3-BjTi9H5L.js} +1 -1
- package/dist/ui/dist/assets/{radar-ABXABTNO-DvGg96L7.js → radar-ABXABTNO-D2hYgXSB.js} +1 -1
- package/dist/ui/dist/assets/{railroad-I3PHUGI6-8wdFKMZp.js → railroad-I3PHUGI6-DpakXN74.js} +1 -1
- package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-DUtOO0gn.js → railroad-abnf-LNEFI6M7-B8ZFR3iy.js} +1 -1
- package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-5YCGY1_d.js → railroad-ebnf-DWYD2UWJ-S1XUyTsC.js} +1 -1
- package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-Bwywxjhs.js → railroad-peg-JR7BVNR7-BbeHC_6K.js} +1 -1
- package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-CwO6HSNQ.js → railroadDiagram-Y72NRBL7-fd-2iyBo.js} +1 -1
- package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-Cp0IbfNr.js → requirementDiagram-EQYSW5RA-B9-HHKH-.js} +1 -1
- package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-DeQKsO68.js → sankeyDiagram-SY24JHO5-CeQN1ZEG.js} +1 -1
- package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-DTDalCxF.js → sequenceDiagram-BLEHNHQU-Vff7tx4t.js} +1 -1
- package/dist/ui/dist/assets/{src-XnrqevnL.js → src-D5-ZzLwt.js} +1 -1
- package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-2DZ-tKo9.js → stateDiagram-v2-A6IESKWZ-Bzn_ma9c.js} +1 -1
- package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-66yZJeRU.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-Dm1xGU-d.js → timeline-definition-JXYKGLGB-D9jSDGJI.js} +1 -1
- package/dist/ui/dist/assets/{treeView-D4JQ5SDB-BvgsbSot.js → treeView-D4JQ5SDB-CoXKj11m.js} +1 -1
- package/dist/ui/dist/assets/{treemap-SAJKECNS-C3lnbG1a.js → treemap-SAJKECNS-cHsansGe.js} +1 -1
- package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-B6iPNIhZ.js → usecaseDiagram-WPOLBNNX-CW9i3bW6.js} +1 -1
- package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-D5dgflg9.js → vennDiagram-Q3Y3IDCW-DuU3qEKJ.js} +1 -1
- package/dist/ui/dist/assets/{wardley-7MLQ67FV-Dn9mLIB4.js → wardley-7MLQ67FV-dl9RgHUI.js} +1 -1
- package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-W1EwzN_X.js → wardleyDiagram-IVAIDKWA-BOXhrK-a.js} +1 -1
- package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-CKRzMUvp.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 +4 -0
- package/dist/ui/dist/assets/channel-gz6cS9Z1.js +0 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-BqUgZSCz.js +0 -1
- package/dist/ui/dist/assets/index-BW95UA5q.css +0 -1
- package/dist/ui/dist/assets/index-DOGXgoHF.js +0 -134
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-rbX6bYG4.js +0 -8
package/dist/src/prompt.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { changedLines } from "./coverage.js";
|
|
1
2
|
import { tipStartOf } from "./diff.js";
|
|
2
3
|
import { anchorOn, concernOn } from "./evolve.js";
|
|
3
4
|
/** Consecutive runs collapse: [0,1,2,3,7] becomes "0-3,7". */
|
|
@@ -581,14 +582,14 @@ function itemHeading(item, level) {
|
|
|
581
582
|
parts.push(kept === null ? "raised by the reviewer" : "raised by the agent, kept by the reviewer");
|
|
582
583
|
return `${"#".repeat(level)} ${parts.join(" · ")}`;
|
|
583
584
|
}
|
|
584
|
-
function renderItems(items, units, level) {
|
|
585
|
+
function renderItems(items, units, level, tallies) {
|
|
585
586
|
const out = [];
|
|
586
587
|
const sorted = sortItems(items, units);
|
|
587
588
|
const paths = [...new Set(sorted.flatMap((i) => (i.where.path === null ? [] : [i.where.path])))];
|
|
588
|
-
const group = (title, members) => {
|
|
589
|
+
const group = (title, members, lead = []) => {
|
|
589
590
|
if (members.length === 0)
|
|
590
591
|
return;
|
|
591
|
-
out.push(`${"#".repeat(level)} ${title}`, "");
|
|
592
|
+
out.push(`${"#".repeat(level)} ${title}`, "", ...lead);
|
|
592
593
|
for (const item of members) {
|
|
593
594
|
out.push(itemHeading(item, level + 1), "", ...quoteBlock(item.where.quote));
|
|
594
595
|
if (item.where.quote.length > 0)
|
|
@@ -596,8 +597,12 @@ function renderItems(items, units, level) {
|
|
|
596
597
|
out.push(...voices(item), "");
|
|
597
598
|
}
|
|
598
599
|
};
|
|
599
|
-
for (const path of paths)
|
|
600
|
-
|
|
600
|
+
for (const path of paths) {
|
|
601
|
+
const tally = tallies.get(path);
|
|
602
|
+
group(path, sorted.filter((i) => i.where.path === path), tally === undefined
|
|
603
|
+
? []
|
|
604
|
+
: [`coverage: ${tally.covered} of ${tally.executable} changed lines`, ""]);
|
|
605
|
+
}
|
|
601
606
|
group("General", sorted.filter((i) => i.where.path === null));
|
|
602
607
|
return out;
|
|
603
608
|
}
|
|
@@ -719,24 +724,43 @@ function spanOf(rows) {
|
|
|
719
724
|
startOldLine: range && first.op !== "+" ? first.base : null,
|
|
720
725
|
};
|
|
721
726
|
}
|
|
722
|
-
function
|
|
727
|
+
function commentRows(ctx, c) {
|
|
723
728
|
const { anchor } = c;
|
|
724
729
|
if (anchor.kind !== "hunk")
|
|
725
|
-
return
|
|
730
|
+
return null;
|
|
726
731
|
const hunk = ctx.units.get(anchor.unit);
|
|
727
732
|
if (hunk?.kind !== "hunk")
|
|
728
|
-
return
|
|
733
|
+
return null;
|
|
729
734
|
const { offsets, changes } = anchor;
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
735
|
+
const want = offsets !== undefined
|
|
736
|
+
? (i) => offsets.includes(i)
|
|
737
|
+
: (_, n) => n !== null && (changes === null || changes.includes(n));
|
|
738
|
+
return { path: hunk.path, rows: numberedRows(hunk, ctx.units, want) };
|
|
733
739
|
}
|
|
734
|
-
function
|
|
740
|
+
function concernRows(ctx, concern) {
|
|
735
741
|
const hunk = concern.unit === undefined ? undefined : ctx.units.get(concern.unit);
|
|
736
742
|
if (hunk?.kind !== "hunk")
|
|
737
|
-
return
|
|
743
|
+
return null;
|
|
738
744
|
const lines = concern.lines;
|
|
739
|
-
|
|
745
|
+
const want = (_, n) => n !== null && (lines === undefined || lines.includes(n));
|
|
746
|
+
return { path: hunk.path, rows: numberedRows(hunk, ctx.units, want) };
|
|
747
|
+
}
|
|
748
|
+
function spanAt(at) {
|
|
749
|
+
return at === null ? NO_SPAN : spanOf(at.rows);
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Whether the test run ran every executable added row the item points at. Absent without a run
|
|
753
|
+
* that still fits the file, when it picks no such row, or when its code changed after it was said.
|
|
754
|
+
*/
|
|
755
|
+
function coveredOf(ctx, id, at) {
|
|
756
|
+
const file = at === null ? undefined : ctx.coverage?.get(at.path);
|
|
757
|
+
if (at === null || file === undefined || ctx.marks?.has(id))
|
|
758
|
+
return {};
|
|
759
|
+
const hits = at.rows.flatMap((r) => {
|
|
760
|
+
const n = r.op === "+" ? file.lines.get(r.tip) : undefined;
|
|
761
|
+
return n === undefined ? [] : [n];
|
|
762
|
+
});
|
|
763
|
+
return hits.length === 0 ? {} : { covered: hits.every((n) => n > 0) };
|
|
740
764
|
}
|
|
741
765
|
function threadOf(c) {
|
|
742
766
|
return (c.replies ?? []).map((r) => ({ author: r.author, body: r.body }));
|
|
@@ -761,6 +785,7 @@ function collectItems(review, ctx, responses) {
|
|
|
761
785
|
const { kept } = it;
|
|
762
786
|
const first = it.comments[0];
|
|
763
787
|
if (kept !== null) {
|
|
788
|
+
const at = concernRows(ctx, kept.concern);
|
|
764
789
|
const note = kept.note === null ? "" : `\n\nReviewer: ${kept.note}`;
|
|
765
790
|
const thread = it.comments.flatMap((c) => [
|
|
766
791
|
{ author: "reviewer", body: c.body },
|
|
@@ -770,7 +795,7 @@ function collectItems(review, ctx, responses) {
|
|
|
770
795
|
where: it.where,
|
|
771
796
|
item: {
|
|
772
797
|
id: kept.concern.id,
|
|
773
|
-
...
|
|
798
|
+
...spanAt(at),
|
|
774
799
|
path: it.where.path,
|
|
775
800
|
oldPath: it.where.path,
|
|
776
801
|
severity: it.severity,
|
|
@@ -780,16 +805,18 @@ function collectItems(review, ctx, responses) {
|
|
|
780
805
|
resolved: false,
|
|
781
806
|
excluded: noConcerns.has(kept.concern.id),
|
|
782
807
|
...changedOf(ctx, kept.concern.id),
|
|
808
|
+
...coveredOf(ctx, kept.concern.id, at),
|
|
783
809
|
},
|
|
784
810
|
};
|
|
785
811
|
}
|
|
786
812
|
if (first === undefined)
|
|
787
813
|
throw new Error("an item with neither a concern nor a comment");
|
|
814
|
+
const at = commentRows(ctx, first);
|
|
788
815
|
return {
|
|
789
816
|
where: it.where,
|
|
790
817
|
item: {
|
|
791
818
|
id: first.id,
|
|
792
|
-
...
|
|
819
|
+
...spanAt(at),
|
|
793
820
|
path: it.where.path,
|
|
794
821
|
oldPath: it.where.path,
|
|
795
822
|
severity: it.severity,
|
|
@@ -799,6 +826,7 @@ function collectItems(review, ctx, responses) {
|
|
|
799
826
|
resolved: resolved.has(first.id),
|
|
800
827
|
excluded: noComments.has(first.id),
|
|
801
828
|
...changedOf(ctx, first.id),
|
|
829
|
+
...coveredOf(ctx, first.id, at),
|
|
802
830
|
},
|
|
803
831
|
};
|
|
804
832
|
});
|
|
@@ -812,7 +840,7 @@ function collectItems(review, ctx, responses) {
|
|
|
812
840
|
where,
|
|
813
841
|
item: {
|
|
814
842
|
id: q.id,
|
|
815
|
-
...
|
|
843
|
+
...spanAt(commentRows(ctx, q)),
|
|
816
844
|
path: where.path,
|
|
817
845
|
oldPath: where.path,
|
|
818
846
|
severity: q.severity ?? null,
|
|
@@ -832,7 +860,7 @@ function collectItems(review, ctx, responses) {
|
|
|
832
860
|
* Excluded ones are present and marked; whoever posts them skips those.
|
|
833
861
|
*/
|
|
834
862
|
export function reportItems(review, commits, units, opts) {
|
|
835
|
-
const session = folded(review, commits, units, opts
|
|
863
|
+
const session = folded(review, commits, units, opts);
|
|
836
864
|
return collectItems(session.review, session.ctx, opts.responses).map((c) => c.item);
|
|
837
865
|
}
|
|
838
866
|
/** A short list of what the reviewer left out, so nothing vanishes without a trace. */
|
|
@@ -855,9 +883,11 @@ function renderExcluded(collected) {
|
|
|
855
883
|
* taking their unit, and a mark on each whose code changed. Each id appears once; this
|
|
856
884
|
* version's own copy wins.
|
|
857
885
|
*/
|
|
858
|
-
function folded(review, commits, units,
|
|
886
|
+
function folded(review, commits, units, opts) {
|
|
887
|
+
const { earlier } = opts;
|
|
888
|
+
const coverage = opts.coverage === undefined ? {} : { coverage: opts.coverage };
|
|
859
889
|
if (earlier === undefined)
|
|
860
|
-
return { review, ctx: { commits, units } };
|
|
890
|
+
return { review, ctx: { commits, units, ...coverage } };
|
|
861
891
|
const marks = new Map();
|
|
862
892
|
const mark = (id, at) => {
|
|
863
893
|
if (at.since !== null)
|
|
@@ -895,6 +925,7 @@ function folded(review, commits, units, earlier) {
|
|
|
895
925
|
ctx: {
|
|
896
926
|
units,
|
|
897
927
|
marks,
|
|
928
|
+
...coverage,
|
|
898
929
|
commits: commits.map((commit, k) => {
|
|
899
930
|
const mine = lost.filter((c) => owner(c.unit, c.commit) === k);
|
|
900
931
|
if (mine.length === 0)
|
|
@@ -915,7 +946,7 @@ function folded(review, commits, units, earlier) {
|
|
|
915
946
|
* Concerns the reviewer has not decided are left out, as are those dismissed.
|
|
916
947
|
*/
|
|
917
948
|
export function renderReport(full, commits, units, opts) {
|
|
918
|
-
const session = folded(full, commits, units, opts
|
|
949
|
+
const session = folded(full, commits, units, opts);
|
|
919
950
|
const { ctx } = session;
|
|
920
951
|
const excluded = session.review.excluded;
|
|
921
952
|
if (excluded === undefined)
|
|
@@ -942,12 +973,14 @@ function renderIncluded(review, ctx, opts) {
|
|
|
942
973
|
const issues = walk ? rest : rest.filter((c) => c.severity !== undefined);
|
|
943
974
|
const feedback = walk ? [] : rest.filter((c) => c.severity === undefined);
|
|
944
975
|
const items = buildItems(ctx, keptConcerns(review, commits), issues);
|
|
976
|
+
// Keyed by path as the items name it, so a file's heading finds its count.
|
|
977
|
+
const tallies = new Map(ctx.coverage === undefined ? [] : changedLines(units, ctx.coverage));
|
|
945
978
|
const out = [];
|
|
946
979
|
if (walk) {
|
|
947
980
|
out.push(`# Code review: ${opts.branch} (${commits.length === 1 ? "1 commit" : `${commits.length} commits`}, ${summarizeIssues(items)})`, "");
|
|
948
981
|
if (items.length === 0 && questions.length === 0)
|
|
949
982
|
out.push("The reviewer raised no issues.", "");
|
|
950
|
-
out.push(...renderItems(items, units, 2));
|
|
983
|
+
out.push(...renderItems(items, units, 2, tallies));
|
|
951
984
|
}
|
|
952
985
|
else {
|
|
953
986
|
out.push(`# Review of plan v${review.planVersion}`, "");
|
|
@@ -960,7 +993,7 @@ function renderIncluded(review, ctx, opts) {
|
|
|
960
993
|
}
|
|
961
994
|
out.push(...renderPlanFeedback(ctx, feedback));
|
|
962
995
|
if (items.length > 0) {
|
|
963
|
-
out.push(`## Code issues (${summarizeIssues(items)})`, "", ...renderItems(items, units, 3));
|
|
996
|
+
out.push(`## Code issues (${summarizeIssues(items)})`, "", ...renderItems(items, units, 3, tallies));
|
|
964
997
|
}
|
|
965
998
|
}
|
|
966
999
|
if (questions.length > 0)
|
|
@@ -985,6 +1018,28 @@ function indented(body) {
|
|
|
985
1018
|
function said(head, body) {
|
|
986
1019
|
return body.includes("\n") ? [`${head}:`, indented(body)] : [`${head}: ${body}`];
|
|
987
1020
|
}
|
|
1021
|
+
/**
|
|
1022
|
+
* The one line that tells the agent a reviewer accepted an offer: where it sits, as the tip lines
|
|
1023
|
+
* of its hunk or its commit, its label, and the command that answers it.
|
|
1024
|
+
*/
|
|
1025
|
+
export function acceptedOfferLine(c, offer, ctx) {
|
|
1026
|
+
const found = (ctx?.commits ?? []).flatMap((commit, k) => (commit.offers ?? []).flatMap((o) => (o.id === offer ? [{ o, k }] : [])))[0];
|
|
1027
|
+
let place = describeAnchor(c.anchor, ctx);
|
|
1028
|
+
if (found !== undefined && found.o.unit === undefined)
|
|
1029
|
+
place = `commit ${found.k + 1}`;
|
|
1030
|
+
else if (found?.o.unit !== undefined) {
|
|
1031
|
+
const unit = ctx?.units.get(found.o.unit);
|
|
1032
|
+
let where = unit?.path ?? found.o.unit;
|
|
1033
|
+
if (unit?.kind === "hunk" && ctx !== null) {
|
|
1034
|
+
const from = tipStartOf(unit, ctx.units);
|
|
1035
|
+
const kept = unit.body.filter((row) => row.op !== "-").length;
|
|
1036
|
+
where = `${unit.path}:${from}-${from + Math.max(kept, 1) - 1}`;
|
|
1037
|
+
}
|
|
1038
|
+
place = `${where} (commit ${found.k + 1})`;
|
|
1039
|
+
}
|
|
1040
|
+
const label = (found?.o.label ?? c.body).trim().replace(/\.+$/, "");
|
|
1041
|
+
return `Reviewer accepted offer ${offer} on ${place}: ${label}. Reply with follow-me reply ${c.id} --diagram <file> [--map <file>].`;
|
|
1042
|
+
}
|
|
988
1043
|
/**
|
|
989
1044
|
* A new comment carries its short quote, since the agent has not seen it; everything else is one
|
|
990
1045
|
* line naming its id, because the agent already has the comment it refers to.
|
|
@@ -993,6 +1048,8 @@ function renderEvent(event, ctx) {
|
|
|
993
1048
|
switch (event.type) {
|
|
994
1049
|
case "comment": {
|
|
995
1050
|
const { comment: c } = event;
|
|
1051
|
+
if (c.offer !== undefined)
|
|
1052
|
+
return [acceptedOfferLine(c, c.offer, ctx)];
|
|
996
1053
|
const what = c.kind === "question" ? "question" : (c.severity ?? "comment");
|
|
997
1054
|
const lines = [`- ${c.id} ${what} on ${describeAnchor(c.anchor, ctx)}`];
|
|
998
1055
|
if (c.follows)
|
|
@@ -1014,8 +1071,12 @@ function renderEvent(event, ctx) {
|
|
|
1014
1071
|
}
|
|
1015
1072
|
case "resolved":
|
|
1016
1073
|
return [`- ${event.commentId} ${event.resolved ? "resolved" : "reopened"}`];
|
|
1074
|
+
case "offer-dismissed":
|
|
1075
|
+
return [`- the reviewer dismissed offer ${event.offer}`];
|
|
1017
1076
|
case "version":
|
|
1018
1077
|
return [`- plan v${event.version} is open`];
|
|
1078
|
+
case "coverage":
|
|
1079
|
+
return [`- the page shows coverage from ${event.tip.slice(0, 12)}`];
|
|
1019
1080
|
case "submitted":
|
|
1020
1081
|
return ["- the reviewer submitted the review"];
|
|
1021
1082
|
case "ended":
|
|
@@ -1049,11 +1110,38 @@ export function renderShownComment(c, ctx, version) {
|
|
|
1049
1110
|
lines.push(...quoted.map((r) => ` ${r}`));
|
|
1050
1111
|
lines.push(...said(c.author === "agent" ? "agent" : "reviewer", c.body));
|
|
1051
1112
|
for (const reply of c.replies ?? []) {
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1113
|
+
const extras = [
|
|
1114
|
+
reply.diagram === undefined ? null : "a diagram",
|
|
1115
|
+
reply.map === undefined ? null : "a state map",
|
|
1116
|
+
];
|
|
1117
|
+
const parts = extras.filter((x) => x !== null);
|
|
1118
|
+
const head = `${reply.author} replied${parts.length === 0 ? "" : ` (with ${parts.join(" and ")})`}`;
|
|
1119
|
+
lines.push(...(reply.body.trim() === "" ? [head] : said(head, reply.body)));
|
|
1120
|
+
if (reply.map !== undefined)
|
|
1121
|
+
lines.push(...renderMap(reply.map).map((l) => ` ${l}`));
|
|
1054
1122
|
}
|
|
1055
1123
|
return `${lines.join("\n")}\n`;
|
|
1056
1124
|
}
|
|
1125
|
+
/** "src/a.ts:12-20" or "src/a.ts:12", a following place in the same file as ":30-41". */
|
|
1126
|
+
function formatLocs(locs) {
|
|
1127
|
+
return locs
|
|
1128
|
+
.map((loc, i) => {
|
|
1129
|
+
const lines = loc.start === loc.end ? `${loc.start}` : `${loc.start}-${loc.end}`;
|
|
1130
|
+
return i > 0 && locs[i - 1]?.path === loc.path ? `:${lines}` : `${utf8(loc.path)}:${lines}`;
|
|
1131
|
+
})
|
|
1132
|
+
.join(", ");
|
|
1133
|
+
}
|
|
1134
|
+
/** A state map, one line per state and per transition. */
|
|
1135
|
+
function renderMap(map) {
|
|
1136
|
+
return [
|
|
1137
|
+
...Object.entries(map.states).map(([name, locs]) => `state ${name}: ${formatLocs(locs)}`),
|
|
1138
|
+
...(map.transitions ?? []).map((t) => {
|
|
1139
|
+
const event = t.event === undefined ? "" : ` on ${t.event}`;
|
|
1140
|
+
const at = t.at.length === 0 ? "" : `: ${formatLocs(t.at)}`;
|
|
1141
|
+
return `${t.from} -> ${t.to}${event}${at}`;
|
|
1142
|
+
}),
|
|
1143
|
+
];
|
|
1144
|
+
}
|
|
1057
1145
|
/** A concern of the plan with the lines it is pinned to, as its commit writes them. */
|
|
1058
1146
|
export function renderShownConcern(concern, commit, ctx) {
|
|
1059
1147
|
const where = whereConcern(ctx, { concern, commit, severity: concern.severity, note: null });
|
package/dist/src/server.js
CHANGED
|
@@ -5,12 +5,13 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { basename, isAbsolute, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { isReplyId, parseComment, parseDraft, parseNewDecision, parseNewReply, } from "./comments.js";
|
|
8
|
+
import { serializeCoverage, staleFiles } from "./coverage.js";
|
|
8
9
|
import { definitionIndex, isIdentifier } from "./definition.js";
|
|
9
10
|
import { isRepoPath } from "./diff.js";
|
|
10
11
|
import { carryConcerns, carrySession } from "./evolve.js";
|
|
11
|
-
import { isConcernId, isPlanVersion } from "./plan.js";
|
|
12
|
+
import { isConcernId, isOfferId, isPlanVersion } from "./plan.js";
|
|
12
13
|
import { renderReport, reportItems } from "./prompt.js";
|
|
13
|
-
import { isLockToken } from "./session.js";
|
|
14
|
+
import { coverageAt, isLockToken, readCoverageIn, SessionFileError } from "./session.js";
|
|
14
15
|
/** The requested port is taken; host.ts decides whether that is our server or a stranger. */
|
|
15
16
|
export class PortInUseError extends Error {
|
|
16
17
|
name = "PortInUseError";
|
|
@@ -93,6 +94,7 @@ export async function loadDraft(reviewsDir, version) {
|
|
|
93
94
|
earlierReplies: [],
|
|
94
95
|
carried: null,
|
|
95
96
|
carriedConcerns: [],
|
|
97
|
+
dismissedOffers: [],
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
throw error;
|
|
@@ -257,6 +259,8 @@ function viewOf(registration, loaded, draft, earlier = loaded.earlier) {
|
|
|
257
259
|
unitIds: new Set(state.units.map((u) => u.id)),
|
|
258
260
|
hunkRows: new Map(state.units.flatMap((u) => u.kind === "hunk" ? [[u.id, u.body.length]] : [])),
|
|
259
261
|
concernIds: new Set(state.commits.flatMap((c) => (c.concerns ?? []).map((k) => k.id))),
|
|
262
|
+
offers: new Map(state.commits.flatMap((c) => (c.offers ?? []).map((offer) => [offer.id, { offer, commit: c.index }]))),
|
|
263
|
+
dismissedOffers: new Set(draft.dismissedOffers),
|
|
260
264
|
definitions: definitionIndex(loaded.repo, state.anchor.tip, new Map(state.units.map((u) => [u.id, u])), state.commits),
|
|
261
265
|
draftFile: draftPath(reviewsDir, state.version),
|
|
262
266
|
reviewFile: join(reviewsDir, `v${state.version}.json`),
|
|
@@ -313,6 +317,21 @@ function findSaid(view, id) {
|
|
|
313
317
|
view.loaded.previous?.comments.find((c) => c.id === id) ??
|
|
314
318
|
view.earlier?.find((c) => c.comment.id === id)?.comment);
|
|
315
319
|
}
|
|
320
|
+
/** The comment of this review, on this version or an earlier one, that accepted the offer. */
|
|
321
|
+
function acceptedBy(view, offer) {
|
|
322
|
+
const said = [...view.comments.values(), ...(view.earlier ?? []).map((c) => c.comment)];
|
|
323
|
+
return said.find((c) => c.offer === offer);
|
|
324
|
+
}
|
|
325
|
+
/** Why a comment may not accept this offer, or null when it may. */
|
|
326
|
+
function offerProblem(view, offer) {
|
|
327
|
+
if (!view.offers.has(offer)) {
|
|
328
|
+
return `offer ${offer} is not in plan v${view.loaded.state.version}`;
|
|
329
|
+
}
|
|
330
|
+
const by = acceptedBy(view, offer);
|
|
331
|
+
if (by !== undefined)
|
|
332
|
+
return `offer ${offer} is already accepted by ${by.id}`;
|
|
333
|
+
return view.dismissedOffers.has(offer) ? `offer ${offer} was dismissed` : null;
|
|
334
|
+
}
|
|
316
335
|
function earlierOf(view) {
|
|
317
336
|
return [...view.earlierReplies].map(([comment, replies]) => ({ comment, replies }));
|
|
318
337
|
}
|
|
@@ -413,6 +432,7 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
413
432
|
const resolved = [...view.resolved];
|
|
414
433
|
const excluded = excludedOf();
|
|
415
434
|
const earlierReplies = earlierOf(view);
|
|
435
|
+
const dismissedOffers = [...view.dismissedOffers];
|
|
416
436
|
const snapshot = JSON.stringify({
|
|
417
437
|
schema: "follow-me/review-draft@1",
|
|
418
438
|
planVersion: state.version,
|
|
@@ -422,6 +442,7 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
422
442
|
...(excluded === null ? {} : { excluded }),
|
|
423
443
|
...(earlierReplies.length === 0 ? {} : { earlierReplies }),
|
|
424
444
|
...carriedRefs(view),
|
|
445
|
+
...(dismissedOffers.length === 0 ? {} : { dismissedOffers }),
|
|
425
446
|
}, null, 2);
|
|
426
447
|
const dir = view.loaded.reviewsDir;
|
|
427
448
|
writes = writes.then(async () => {
|
|
@@ -457,6 +478,7 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
457
478
|
const resolved = [...view.resolved];
|
|
458
479
|
const excluded = excludedOf();
|
|
459
480
|
const earlierReplies = earlierOf(view);
|
|
481
|
+
const dismissedOffers = [...view.dismissedOffers];
|
|
460
482
|
return {
|
|
461
483
|
schema: "follow-me/review@1",
|
|
462
484
|
mode: view.loaded.state.mode,
|
|
@@ -469,28 +491,51 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
469
491
|
...(excluded === null ? {} : { excluded }),
|
|
470
492
|
...(earlierReplies.length === 0 ? {} : { earlierReplies }),
|
|
471
493
|
...carriedRefs(view),
|
|
494
|
+
...(dismissedOffers.length === 0 ? {} : { dismissedOffers }),
|
|
472
495
|
};
|
|
473
496
|
};
|
|
474
|
-
const reportOptions = () => {
|
|
497
|
+
const reportOptions = async () => {
|
|
475
498
|
const comments = sessionOf(view);
|
|
499
|
+
const { repo, state } = view.loaded;
|
|
500
|
+
const coverage = await coverageAt(repo, view.registration.dir, state.anchor.tip);
|
|
476
501
|
return {
|
|
477
|
-
branch:
|
|
502
|
+
branch: state.anchor.branch,
|
|
478
503
|
responses: [],
|
|
479
504
|
...(comments === null ? {} : { earlier: { comments, concerns: view.earlierConcerns } }),
|
|
505
|
+
...(coverage === null ? {} : { coverage }),
|
|
480
506
|
};
|
|
481
507
|
};
|
|
482
508
|
const unitsOf = () => new Map(view.loaded.state.units.map((u) => [u.id, u]));
|
|
483
|
-
const report = (finished) => renderReport(finished, view.loaded.state.commits, unitsOf(), reportOptions());
|
|
509
|
+
const report = async (finished) => renderReport(finished, view.loaded.state.commits, unitsOf(), await reportOptions());
|
|
484
510
|
/** Writes reviews/vN.json and its report once, and removes the autosave. */
|
|
485
511
|
const writeReview = async (finished) => {
|
|
486
512
|
await writes;
|
|
487
513
|
await mkdir(view.loaded.reviewsDir, { recursive: true });
|
|
488
514
|
await writeAtomic(view.reviewFile, `${JSON.stringify(finished, null, 2)}\n`);
|
|
489
|
-
await writeAtomic(view.reviewFile.replace(/\.json$/, ".md"), report(finished));
|
|
515
|
+
await writeAtomic(view.reviewFile.replace(/\.json$/, ".md"), await report(finished));
|
|
490
516
|
await rm(view.draftFile, { force: true });
|
|
491
517
|
return view.reviewFile;
|
|
492
518
|
};
|
|
493
|
-
|
|
519
|
+
/** coverage.json as the agent last wrote it, or a 404 before its first run. */
|
|
520
|
+
const coverageFile = () => {
|
|
521
|
+
let coverage;
|
|
522
|
+
try {
|
|
523
|
+
coverage = readCoverageIn(view.registration.dir);
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
if (error instanceof SessionFileError)
|
|
527
|
+
throw new HttpError(500, error.message);
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
if (coverage === null) {
|
|
531
|
+
throw new HttpError(404, "no coverage yet; the agent records it with follow-me coverage");
|
|
532
|
+
}
|
|
533
|
+
return coverage;
|
|
534
|
+
};
|
|
535
|
+
const hasWork = () => view.comments.size > 0 ||
|
|
536
|
+
view.decisions.size > 0 ||
|
|
537
|
+
view.earlierReplies.size > 0 ||
|
|
538
|
+
view.dismissedOffers.size > 0;
|
|
494
539
|
return {
|
|
495
540
|
id,
|
|
496
541
|
done,
|
|
@@ -514,6 +559,7 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
514
559
|
concerns: [...view.decisions.values()],
|
|
515
560
|
resolved: [...view.resolved],
|
|
516
561
|
excluded: { comments: [...view.excludedComments], concerns: [...view.excludedConcerns] },
|
|
562
|
+
dismissedOffers: [...view.dismissedOffers],
|
|
517
563
|
seq: events.length,
|
|
518
564
|
previous: previousOf(view),
|
|
519
565
|
carried: carriedOf(view),
|
|
@@ -547,18 +593,37 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
547
593
|
return waitForEvents(since, timeoutMs, signal);
|
|
548
594
|
},
|
|
549
595
|
report: () => report(review()),
|
|
596
|
+
/**
|
|
597
|
+
* The agent's last test run, read from disk so a restarted server serves it too, with the
|
|
598
|
+
* covered files whose content changed since its tip. A new plan for the same tip stales nothing.
|
|
599
|
+
*/
|
|
600
|
+
coverage: async () => {
|
|
601
|
+
const coverage = coverageFile();
|
|
602
|
+
const stale = await staleFiles(view.loaded.repo, coverage, view.loaded.state.anchor.tip);
|
|
603
|
+
return { ...serializeCoverage(coverage), stale };
|
|
604
|
+
},
|
|
605
|
+
/** The agent wrote coverage.json; the page hears it as one event and reloads its marks. */
|
|
606
|
+
coverageTaken: () => {
|
|
607
|
+
open();
|
|
608
|
+
const { tip, takenAt } = coverageFile();
|
|
609
|
+
emit({ type: "coverage", tip, takenAt });
|
|
610
|
+
return { tip, takenAt };
|
|
611
|
+
},
|
|
550
612
|
/** The page reads a definition the plan shows from its hunk, so only others carry a preview. */
|
|
551
613
|
define: async (name, from) => {
|
|
552
614
|
const found = await view.definitions.lookup(name, from !== null && isRepoPath(from) ? from : null);
|
|
553
615
|
return found.map((c) => (c.unit === null ? c : { ...c, preview: [] }));
|
|
554
616
|
},
|
|
555
|
-
reportItems: () => reportItems(review(), view.loaded.state.commits, unitsOf(), reportOptions()),
|
|
617
|
+
reportItems: async () => reportItems(review(), view.loaded.state.commits, unitsOf(), await reportOptions()),
|
|
556
618
|
add: async (raw) => {
|
|
557
619
|
open();
|
|
558
620
|
if (!isRecord(raw))
|
|
559
621
|
throw new HttpError(400, "malformed comment");
|
|
560
622
|
const commentId = mintCommentId(new Set([...view.comments.keys(), ...(view.earlier ?? []).map((c) => c.comment.id)]));
|
|
561
623
|
const comment = withReplies(accept(parseComment({ ...raw, id: commentId, createdAt: new Date().toISOString() })), undefined);
|
|
624
|
+
const problem = comment.offer === undefined ? null : offerProblem(view, comment.offer);
|
|
625
|
+
if (problem !== null)
|
|
626
|
+
throw new HttpError(400, problem);
|
|
562
627
|
view.comments.set(commentId, comment);
|
|
563
628
|
emit({ type: "comment", comment });
|
|
564
629
|
await saveDraft();
|
|
@@ -569,6 +634,14 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
569
634
|
const parsed = withReplies(accept(parseComment(raw)), undefined);
|
|
570
635
|
if (parsed.id !== target)
|
|
571
636
|
throw new HttpError(400, "comment id does not match the URL");
|
|
637
|
+
const before = view.comments.get(target);
|
|
638
|
+
if (parsed.offer !== before?.offer) {
|
|
639
|
+
const problem = before !== undefined || parsed.offer === undefined
|
|
640
|
+
? "a comment keeps the offer it accepted"
|
|
641
|
+
: offerProblem(view, parsed.offer);
|
|
642
|
+
if (problem !== null)
|
|
643
|
+
throw new HttpError(400, problem);
|
|
644
|
+
}
|
|
572
645
|
// Replies belong to the conversation, not to the page's copy of the comment.
|
|
573
646
|
const comment = withReplies(parsed, view.comments.get(target)?.replies);
|
|
574
647
|
view.comments.set(comment.id, comment);
|
|
@@ -635,6 +708,25 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
635
708
|
await saveDraft();
|
|
636
709
|
return decision;
|
|
637
710
|
},
|
|
711
|
+
/**
|
|
712
|
+
* Waves an offer of this plan away. It stays dismissed for the rest of the review, across
|
|
713
|
+
* versions; dismissing it again changes nothing, and an accepted offer cannot be dismissed.
|
|
714
|
+
*/
|
|
715
|
+
dismissOffer: async (target) => {
|
|
716
|
+
open();
|
|
717
|
+
if (!isOfferId(target) || !view.offers.has(target)) {
|
|
718
|
+
throw new HttpError(404, "no such offer in this plan");
|
|
719
|
+
}
|
|
720
|
+
const by = acceptedBy(view, target);
|
|
721
|
+
if (by !== undefined)
|
|
722
|
+
throw new HttpError(400, `offer ${target} is already accepted by ${by.id}`);
|
|
723
|
+
if (!view.dismissedOffers.has(target)) {
|
|
724
|
+
view.dismissedOffers.add(target);
|
|
725
|
+
emit({ type: "offer-dismissed", offer: target });
|
|
726
|
+
await saveDraft();
|
|
727
|
+
}
|
|
728
|
+
return [...view.dismissedOffers];
|
|
729
|
+
},
|
|
638
730
|
/** Leaves a comment or an agent concern out of the hand-off, or puts it back. */
|
|
639
731
|
exclude: async (raw) => {
|
|
640
732
|
open();
|
|
@@ -728,13 +820,15 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
728
820
|
comments: [...view.excludedComments],
|
|
729
821
|
concerns: [...view.excludedConcerns].filter(stillNamed),
|
|
730
822
|
},
|
|
823
|
+
dismissedOffers: [...view.dismissedOffers],
|
|
731
824
|
}, earlier);
|
|
732
825
|
const carrying = earlier.comments.length +
|
|
733
826
|
earlier.concerns.length +
|
|
734
827
|
view.decisions.size +
|
|
735
828
|
view.resolved.size +
|
|
736
829
|
view.excludedComments.size +
|
|
737
|
-
view.excludedConcerns.size
|
|
830
|
+
view.excludedConcerns.size +
|
|
831
|
+
view.dismissedOffers.size;
|
|
738
832
|
if (carrying > 0)
|
|
739
833
|
await saveDraft();
|
|
740
834
|
emit({ type: "version", version, hash, carried: carriedOf(view) });
|
|
@@ -958,12 +1052,18 @@ export function startServer(opts) {
|
|
|
958
1052
|
const candidates = await session.define(name, url.searchParams.get("from"));
|
|
959
1053
|
return sendJson(res, 200, { candidates });
|
|
960
1054
|
}
|
|
1055
|
+
if (method === "GET" && rest === "/api/coverage") {
|
|
1056
|
+
return sendJson(res, 200, await session.coverage());
|
|
1057
|
+
}
|
|
1058
|
+
if (method === "POST" && rest === "/api/coverage") {
|
|
1059
|
+
return sendJson(res, 200, session.coverageTaken());
|
|
1060
|
+
}
|
|
961
1061
|
if (method === "GET" && rest === "/api/report" && url.searchParams.get("format") === "json") {
|
|
962
|
-
return sendJson(res, 200, session.reportItems());
|
|
1062
|
+
return sendJson(res, 200, await session.reportItems());
|
|
963
1063
|
}
|
|
964
1064
|
if (method === "GET" && rest === "/api/report") {
|
|
965
1065
|
res.writeHead(200, { "content-type": "text/markdown; charset=utf-8" });
|
|
966
|
-
res.end(session.report());
|
|
1066
|
+
res.end(await session.report());
|
|
967
1067
|
return;
|
|
968
1068
|
}
|
|
969
1069
|
if (method === "POST" && rest === "/api/version") {
|
|
@@ -989,6 +1089,10 @@ export function startServer(opts) {
|
|
|
989
1089
|
if (replies !== undefined && method === "POST") {
|
|
990
1090
|
return sendJson(res, 201, { id: await session.reply(replies, await readJson(req)) });
|
|
991
1091
|
}
|
|
1092
|
+
const offer = /^\/api\/offers\/([^/]+)\/dismiss$/.exec(rest ?? "")?.[1];
|
|
1093
|
+
if (offer !== undefined && method === "POST") {
|
|
1094
|
+
return sendJson(res, 200, { dismissedOffers: await session.dismissOffer(offer) });
|
|
1095
|
+
}
|
|
992
1096
|
const concern = /^\/api\/concerns\/([^/]+)$/.exec(rest ?? "")?.[1];
|
|
993
1097
|
if (concern !== undefined && method === "POST") {
|
|
994
1098
|
return sendJson(res, 200, await session.decide(concern, await readJson(req)));
|
package/dist/src/session.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { parseDraft, parseReview } from "./comments.js";
|
|
5
|
+
import { freshFiles, parseCoverage, serializeCoverage, staleFiles } from "./coverage.js";
|
|
5
6
|
import { isSha } from "./diff.js";
|
|
6
7
|
import { evolvePlan } from "./evolve.js";
|
|
7
8
|
import { branchExists, currentBranch, defaultBaseRef, GitError, loadDiff, objectsDir, QUARANTINE_PREFIX, repoRoot, resolveCommit, } from "./git.js";
|
|
@@ -200,6 +201,36 @@ function parseApplied(v) {
|
|
|
200
201
|
return null;
|
|
201
202
|
return { schema, version, branch, head, hash, signed };
|
|
202
203
|
}
|
|
204
|
+
function coveragePath(dir) {
|
|
205
|
+
return join(dir, "coverage.json");
|
|
206
|
+
}
|
|
207
|
+
/** The coverage.json of the session in `dir`, or null when there is none. */
|
|
208
|
+
export function readCoverageIn(dir) {
|
|
209
|
+
const path = coveragePath(dir);
|
|
210
|
+
const raw = readJson(path);
|
|
211
|
+
if (raw === undefined)
|
|
212
|
+
return null;
|
|
213
|
+
const coverage = parseCoverage(raw);
|
|
214
|
+
if (typeof coverage === "string")
|
|
215
|
+
throw new SessionFileError(`${path}: ${coverage}`);
|
|
216
|
+
return coverage;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* What the report counts: the files of the session's test run that still describe `tip`. Null
|
|
220
|
+
* without a run, and when coverage.json cannot be read, so a report never fails on it.
|
|
221
|
+
*/
|
|
222
|
+
export async function coverageAt(repo, dir, tip) {
|
|
223
|
+
let coverage;
|
|
224
|
+
try {
|
|
225
|
+
coverage = readCoverageIn(dir);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (error instanceof SessionFileError)
|
|
229
|
+
return null;
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
return coverage === null ? null : freshFiles(coverage, await staleFiles(repo, coverage, tip));
|
|
233
|
+
}
|
|
203
234
|
function makeSession(root, dir, anchor, diff) {
|
|
204
235
|
const plansDir = join(dir, "plans");
|
|
205
236
|
const splitReviews = join(dir, "reviews");
|
|
@@ -310,6 +341,9 @@ function makeSession(root, dir, anchor, diff) {
|
|
|
310
341
|
writeJson(join(plansDir, `v${version}.json`), serializePlanFile(plan.file));
|
|
311
342
|
return version;
|
|
312
343
|
},
|
|
344
|
+
coverage: () => readCoverageIn(dir),
|
|
345
|
+
writeCoverage: (coverage) => writeJson(coveragePath(dir), serializeCoverage(coverage)),
|
|
346
|
+
clearCoverage: () => rmSync(coveragePath(dir), { force: true }),
|
|
313
347
|
lock: (command) => acquireLock(dir, anchor.branch, command),
|
|
314
348
|
adoptLock: (from, command) => adoptLock(dir, from, command),
|
|
315
349
|
lockState: () => readLock(join(dir, "lock")),
|
package/dist/src/types.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* review file, the autosaved draft and the server's events. Pure parsers.
|
|
13
13
|
* definition.ts where an identifier is defined at the branch tip, and where that lands in the
|
|
14
14
|
* plan: per-language patterns, ranking, placement and a per-tip cache.
|
|
15
|
+
* coverage.ts lcov from the agent's test run: parse, place at the tip, merge runs, count the
|
|
16
|
+
* changed lines covered, and which files went stale since.
|
|
15
17
|
* prompt.ts the planner prompt for an agent, the feedback and the consolidated report.
|
|
16
18
|
* server.ts the review server: a registry of hosted sessions, their JSON API, and ui/.
|
|
17
19
|
* host.ts the one server per user: its record in the state dir, attach, start, stop.
|
package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-fYSYLdMl.js → abnfDiagram-WEYOIA3C-Bp14ygmz.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{m as e,t}from"./mermaid-parser.core-
|
|
1
|
+
import{m as e,t}from"./mermaid-parser.core-Bxa71wW-.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-D5-ZzLwt.js";import"./chunk-DSNLSHJZ-CNjUo110.js";import{n as i,r as a,t as o}from"./chunk-2E7WHVMD-BODm_awL.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-DBI9F4bh.js";var c=e().RailroadAbnf.parser.LangiumParser,l=n(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=n(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=n(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=n(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=n(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=n(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=n(e=>{s(e,o),e.title&&o.setTitle(e.title),e.rules.map(e=>o.addRule(m(e)))},`populateDb`),g={parser:{parse:n(e=>{o.clear(),r.debug(`[ABNF Parser] Starting Langium parse`);let n=c.parse(e);if(n.lexerErrors.length>0||n.parserErrors.length>0)throw new t(n);let i=n.value;r.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),r.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:o}},db:o,renderer:a,styles:i};export{g as diagram};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e,t}from"./path-fybaL0A-.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-
|
|
1
|
+
import{n as e,t}from"./path-fybaL0A-.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-DxQIK_Ay.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var c=e-n,l=t-r,u=(o?a:-a)/d(c*c+l*l),f=u*l,p=-u*c,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*d(s(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var s=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+s.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-r,_=T.apply(this,arguments)-r,v=l(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>c-1e-12)D.moveTo(h*u(g),h*a(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*u(_),m*a(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):d(m*m+h*h)),R=p(l(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*a(I)),W=o(L/h*a(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*u(A),K=h*a(A),q=m*u(N),J=m*a(N);if(R>1e-12){var Y=h*u(j),X=h*a(j),Z=m*u(M),Q=m*a(M),$;if(v<i){if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/a(f((ee*ne+te*re)/(d(ee*ee+te*te)*d(ne*ne+re*re)))/2),ae=d($[0]*$[0]+$[1]*$[1]);z=p(R,(m-ae)/(ie-1)),B=p(R,(h-ae)/(ie+1))}else z=B=0}}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,h,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,n(H.y11,H.x11),n(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,m,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,n(H.y11,H.x11),n(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+s.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-i/2;return[u(t)*e,a(t)*e]},k.innerRadius=function(t){return arguments.length?(s=typeof t==`function`?t:e(+t),k):s},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};
|