@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.
Files changed (101) hide show
  1. package/README.md +7 -1
  2. package/dist/src/cli.js +210 -23
  3. package/dist/src/comments.js +152 -8
  4. package/dist/src/coverage.js +411 -0
  5. package/dist/src/git.js +18 -0
  6. package/dist/src/host.js +27 -0
  7. package/dist/src/plan.js +128 -6
  8. package/dist/src/prompt.js +113 -25
  9. package/dist/src/server.js +115 -11
  10. package/dist/src/session.js +34 -0
  11. package/dist/src/types.js +2 -0
  12. package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-fYSYLdMl.js → abnfDiagram-WEYOIA3C-Bp14ygmz.js} +1 -1
  13. package/dist/ui/dist/assets/{arc-DGuGSnwa.js → arc-DUR6uzhI.js} +1 -1
  14. package/dist/ui/dist/assets/{architecture-WOLXFQ4H-VFox_hV1.js → architecture-WOLXFQ4H-CyfnP65O.js} +1 -1
  15. package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-CBVK3nHZ.js → architectureDiagram-ZHVYKT53-dJTicIk2.js} +1 -1
  16. package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-BFdsIWEr.js → blockDiagram-RJAE7QA3-BYiqh5Ib.js} +1 -1
  17. package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-BGntcPId.js → c4Diagram-XDPPPE54-DHHPQ7xE.js} +1 -1
  18. package/dist/ui/dist/assets/channel-CY2cQobm.js +1 -0
  19. package/dist/ui/dist/assets/{chunk-2E7WHVMD-CNMuK9zj.js → chunk-2E7WHVMD-BODm_awL.js} +1 -1
  20. package/dist/ui/dist/assets/{chunk-4EMNKR2T-ByeTTJWS.js → chunk-4EMNKR2T-CpZfjf2T.js} +1 -1
  21. package/dist/ui/dist/assets/{chunk-55XCU76K-99xE7cLH.js → chunk-55XCU76K-CoP2pJTQ.js} +1 -1
  22. package/dist/ui/dist/assets/{chunk-7S6XSTFO-ZVmT7eMZ.js → chunk-7S6XSTFO-CaQcUvXY.js} +1 -1
  23. package/dist/ui/dist/assets/{chunk-AQDTSKZB-B981JORL.js → chunk-AQDTSKZB-BZ9Czvbu.js} +1 -1
  24. package/dist/ui/dist/assets/{chunk-DSNLSHJZ-B2z7FX4h.js → chunk-DSNLSHJZ-CNjUo110.js} +1 -1
  25. package/dist/ui/dist/assets/{chunk-HAGFOTED-CHFbBS-4.js → chunk-HAGFOTED-BTmBE_D3.js} +1 -1
  26. package/dist/ui/dist/assets/{chunk-HNE4Q36P-rq11xv9X.js → chunk-HNE4Q36P-D2cDuT0C.js} +1 -1
  27. package/dist/ui/dist/assets/{chunk-NQOKRVIF-BYmg9kSX.js → chunk-NQOKRVIF-AZcemFgG.js} +1 -1
  28. package/dist/ui/dist/assets/{chunk-UHW7PFBP-JvL9IXWg.js → chunk-UHW7PFBP-BszwjUqG.js} +1 -1
  29. package/dist/ui/dist/assets/{chunk-VQ5W62QI-CYMQaAKn.js → chunk-VQ5W62QI-vFSenYSe.js} +1 -1
  30. package/dist/ui/dist/assets/{chunk-WQQL4CDK-CXnR0PHT.js → chunk-WQQL4CDK-DZR6is1Q.js} +1 -1
  31. package/dist/ui/dist/assets/{chunk-XXDRQBXY-CDUWJCXi.js → chunk-XXDRQBXY-CACFh-si.js} +1 -1
  32. package/dist/ui/dist/assets/{chunk-YZO67HFG-mZfXIx0h.js → chunk-YZO67HFG-DRRTjGuW.js} +1 -1
  33. package/dist/ui/dist/assets/{chunk-ZAUX2LTX-DBxVTCHX.js → chunk-ZAUX2LTX-Bxuy-Roa.js} +1 -1
  34. package/dist/ui/dist/assets/{chunk-ZCHMQSOR-DrWA4IBo.js → chunk-ZCHMQSOR-rYXk__1o.js} +1 -1
  35. package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-Beah5uxA.js → classDiagram-v2-4MYWR3UA-RiF44tTz.js} +1 -1
  36. package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-UaE-jnHk.js → cose-bilkent-JH36ORCC-0oiQoThS.js} +1 -1
  37. package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-DKfGaCR8.js → cynefin-EF2NZ3EQ-BAssFK5K.js} +1 -1
  38. package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-tTUzTs3D.js → cynefinDiagram-WF24UDRP-Bmd05bak.js} +1 -1
  39. package/dist/ui/dist/assets/{dagre-NK4GIZVX-jSn35_T7.js → dagre-NK4GIZVX-QJkLjPxj.js} +1 -1
  40. package/dist/ui/dist/assets/{diagram-CBK3MWTC-7W2uw8Zc.js → diagram-CBK3MWTC-PSaSiYXz.js} +1 -1
  41. package/dist/ui/dist/assets/{diagram-I5XIIV2D-CcfyQVh9.js → diagram-I5XIIV2D-BmU5X_hr.js} +1 -1
  42. package/dist/ui/dist/assets/{diagram-ODIL2UCW-BjGvaLis.js → diagram-ODIL2UCW-AAV39kUu.js} +1 -1
  43. package/dist/ui/dist/assets/{diagram-PFBEJUXE-jvdCrt8B.js → diagram-PFBEJUXE-DWlV-oxd.js} +1 -1
  44. package/dist/ui/dist/assets/{diagram-RJA3I4VV-D65_Vpi_.js → diagram-RJA3I4VV-D8wHRCNX.js} +1 -1
  45. package/dist/ui/dist/assets/{diagram-TYUNFSNR-C52roQBE.js → diagram-TYUNFSNR-B_JQEsNY.js} +1 -1
  46. package/dist/ui/dist/assets/{dist-BJd4jp9Y.js → dist-DxQIK_Ay.js} +1 -1
  47. package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-DXysoIBq.js → ebnfDiagram-V4P3TTPT-D83qV8mv.js} +1 -1
  48. package/dist/ui/dist/assets/{elk-QONTBCTQ-BVZcsgV3.js → elk-QONTBCTQ-BKFAKVSD.js} +1 -1
  49. package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-BJnT4fVU.js → erDiagram-KYQIGQAA-CHYRd4aM.js} +1 -1
  50. package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-C5MzxCMY.js → eventmodeling-K75KTNOO-BDJDv3Zf.js} +1 -1
  51. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Bke3CfUH.js +1 -0
  52. package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-CZRrnuah.js → ganttDiagram-R7GPNTJY-gTHjYkFj.js} +1 -1
  53. package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-l58aGSkh.js → gitGraph-VSP46ZUC-D4cWkxKB.js} +1 -1
  54. package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-D4ks1WO-.js → gitGraphDiagram-PDYL7XHY-D3M9wYMr.js} +1 -1
  55. package/dist/ui/dist/assets/index-BUOFihod.js +134 -0
  56. package/dist/ui/dist/assets/index-DLNJT0VS.css +1 -0
  57. package/dist/ui/dist/assets/{info-OHQRW6UA-BSqfzkdk.js → info-OHQRW6UA-B9y0ksVq.js} +1 -1
  58. package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-B6NgVF_N.js → infoDiagram-MVDRKAX5-s4FMwb8r.js} +1 -1
  59. package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-DvtfssEZ.js → ishikawaDiagram-HPEHV2GE-CnQdxybf.js} +1 -1
  60. package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-DM64gT5v.js → journeyDiagram-BOSHW7O7-I-9OpDWY.js} +1 -1
  61. package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-AliyRJHy.js → kanban-definition-FNSA4HC5-DqUbIdH4.js} +1 -1
  62. package/dist/ui/dist/assets/{line-CHncXbKO.js → line-BELSui4d.js} +1 -1
  63. package/dist/ui/dist/assets/{linear-BWEWydI0.js → linear-77hmn2iQ.js} +1 -1
  64. package/dist/ui/dist/assets/{mermaid-parser.core-DGpOP8AM.js → mermaid-parser.core-Bxa71wW-.js} +3 -3
  65. package/dist/ui/dist/assets/{mermaid.core-BJtBFbJx.js → mermaid.core-DBI9F4bh.js} +4 -4
  66. package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-NjavnWBi.js → mindmap-definition-6OY57UG3-BxXMOapP.js} +1 -1
  67. package/dist/ui/dist/assets/{packet-JDAUHWVQ-zuhpVsME.js → packet-JDAUHWVQ-DTT77cQh.js} +1 -1
  68. package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-sL4grM97.js → pegDiagram-ECIUJNBX-HzrOQPT5.js} +1 -1
  69. package/dist/ui/dist/assets/{pie-XZMESJXO-C9l1Mp09.js → pie-XZMESJXO-iHwL1Qw-.js} +1 -1
  70. package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-rlRBr2U4.js → pieDiagram-GNMPY74T-DZ1tLVjw.js} +1 -1
  71. package/dist/ui/dist/assets/{prism-core-DmuST30i.js → prism-core-NtO0YF9C.js} +1 -1
  72. package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-4oQMip2Q.js → quadrantDiagram-D57K4MC3-BjTi9H5L.js} +1 -1
  73. package/dist/ui/dist/assets/{radar-ABXABTNO-DvGg96L7.js → radar-ABXABTNO-D2hYgXSB.js} +1 -1
  74. package/dist/ui/dist/assets/{railroad-I3PHUGI6-8wdFKMZp.js → railroad-I3PHUGI6-DpakXN74.js} +1 -1
  75. package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-DUtOO0gn.js → railroad-abnf-LNEFI6M7-B8ZFR3iy.js} +1 -1
  76. package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-5YCGY1_d.js → railroad-ebnf-DWYD2UWJ-S1XUyTsC.js} +1 -1
  77. package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-Bwywxjhs.js → railroad-peg-JR7BVNR7-BbeHC_6K.js} +1 -1
  78. package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-CwO6HSNQ.js → railroadDiagram-Y72NRBL7-fd-2iyBo.js} +1 -1
  79. package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-Cp0IbfNr.js → requirementDiagram-EQYSW5RA-B9-HHKH-.js} +1 -1
  80. package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-DeQKsO68.js → sankeyDiagram-SY24JHO5-CeQN1ZEG.js} +1 -1
  81. package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-DTDalCxF.js → sequenceDiagram-BLEHNHQU-Vff7tx4t.js} +1 -1
  82. package/dist/ui/dist/assets/{src-XnrqevnL.js → src-D5-ZzLwt.js} +1 -1
  83. package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-2DZ-tKo9.js → stateDiagram-v2-A6IESKWZ-Bzn_ma9c.js} +1 -1
  84. package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-66yZJeRU.js → swimlanes-B3ENRKFQ-CevqLchV.js} +1 -1
  85. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-Bkv8hAmN.js +8 -0
  86. package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-Dm1xGU-d.js → timeline-definition-JXYKGLGB-D9jSDGJI.js} +1 -1
  87. package/dist/ui/dist/assets/{treeView-D4JQ5SDB-BvgsbSot.js → treeView-D4JQ5SDB-CoXKj11m.js} +1 -1
  88. package/dist/ui/dist/assets/{treemap-SAJKECNS-C3lnbG1a.js → treemap-SAJKECNS-cHsansGe.js} +1 -1
  89. package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-B6iPNIhZ.js → usecaseDiagram-WPOLBNNX-CW9i3bW6.js} +1 -1
  90. package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-D5dgflg9.js → vennDiagram-Q3Y3IDCW-DuU3qEKJ.js} +1 -1
  91. package/dist/ui/dist/assets/{wardley-7MLQ67FV-Dn9mLIB4.js → wardley-7MLQ67FV-dl9RgHUI.js} +1 -1
  92. package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-W1EwzN_X.js → wardleyDiagram-IVAIDKWA-BOXhrK-a.js} +1 -1
  93. package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-CKRzMUvp.js → xychartDiagram-4URNHDU4-CLQNGrh4.js} +1 -1
  94. package/dist/ui/dist/index.html +2 -2
  95. package/package.json +1 -1
  96. package/skills/follow-me/SKILL.md +4 -0
  97. package/dist/ui/dist/assets/channel-gz6cS9Z1.js +0 -1
  98. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-BqUgZSCz.js +0 -1
  99. package/dist/ui/dist/assets/index-BW95UA5q.css +0 -1
  100. package/dist/ui/dist/assets/index-DOGXgoHF.js +0 -134
  101. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-rbX6bYG4.js +0 -8
@@ -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
- group(path, sorted.filter((i) => i.where.path === path));
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 commentSpan(ctx, c) {
727
+ function commentRows(ctx, c) {
723
728
  const { anchor } = c;
724
729
  if (anchor.kind !== "hunk")
725
- return NO_SPAN;
730
+ return null;
726
731
  const hunk = ctx.units.get(anchor.unit);
727
732
  if (hunk?.kind !== "hunk")
728
- return NO_SPAN;
733
+ return null;
729
734
  const { offsets, changes } = anchor;
730
- if (offsets !== undefined)
731
- return spanOf(numberedRows(hunk, ctx.units, (i) => offsets.includes(i)));
732
- return spanOf(numberedRows(hunk, ctx.units, (_, n) => n !== null && (changes === null || changes.includes(n))));
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 concernSpan(ctx, concern) {
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 NO_SPAN;
743
+ return null;
738
744
  const lines = concern.lines;
739
- return spanOf(numberedRows(hunk, ctx.units, (_, n) => n !== null && (lines === undefined || lines.includes(n))));
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
- ...concernSpan(ctx, kept.concern),
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
- ...commentSpan(ctx, first),
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
- ...commentSpan(ctx, q),
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.earlier);
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, earlier) {
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.earlier);
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 diagram = reply.diagram === undefined ? "" : " (with a diagram)";
1053
- lines.push(...said(`${reply.author} replied${diagram}`, reply.body));
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 });
@@ -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: view.loaded.state.anchor.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
- const hasWork = () => view.comments.size > 0 || view.decisions.size > 0 || view.earlierReplies.size > 0;
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)));
@@ -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.
@@ -1 +1 @@
1
- import{m as e,t}from"./mermaid-parser.core-DGpOP8AM.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-XnrqevnL.js";import"./chunk-DSNLSHJZ-B2z7FX4h.js";import{n as i,r as a,t as o}from"./chunk-2E7WHVMD-CNMuK9zj.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-BJtBFbJx.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
+ 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-BJd4jp9Y.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};
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};