@shiyuphan/follow-me 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +9 -1
  2. package/dist/src/cli.js +181 -42
  3. package/dist/src/comments.js +215 -9
  4. package/dist/src/coverage.js +163 -25
  5. package/dist/src/host.js +23 -0
  6. package/dist/src/plan.js +128 -6
  7. package/dist/src/prompt.js +55 -2
  8. package/dist/src/refs.js +239 -0
  9. package/dist/src/server.js +65 -3
  10. package/dist/src/session.js +1 -0
  11. package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-cHEJvcBn.js → abnfDiagram-WEYOIA3C-DztIEwNE.js} +1 -1
  12. package/dist/ui/dist/assets/{arc-xolFyc0v.js → arc-DUYh-EqZ.js} +1 -1
  13. package/dist/ui/dist/assets/{architecture-WOLXFQ4H-B3hvvNV2.js → architecture-WOLXFQ4H-BQYTB92_.js} +1 -1
  14. package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-BNTMbPJW.js → architectureDiagram-ZHVYKT53-Dl7095A5.js} +1 -1
  15. package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-NYY6qeuY.js → blockDiagram-RJAE7QA3-QDZ5kE1J.js} +1 -1
  16. package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-B_bNpXHQ.js → c4Diagram-XDPPPE54-CKJZ1xOw.js} +1 -1
  17. package/dist/ui/dist/assets/channel-DF08Hd8f.js +1 -0
  18. package/dist/ui/dist/assets/{chunk-2E7WHVMD-BTet9STn.js → chunk-2E7WHVMD-BXBjaqd1.js} +1 -1
  19. package/dist/ui/dist/assets/{chunk-4EMNKR2T-qoQt4Fpx.js → chunk-4EMNKR2T-DPzfVuke.js} +1 -1
  20. package/dist/ui/dist/assets/{chunk-55XCU76K-C3I6EB-H.js → chunk-55XCU76K-B33Sj3w8.js} +1 -1
  21. package/dist/ui/dist/assets/{chunk-7S6XSTFO-CL-9s_8p.js → chunk-7S6XSTFO-B_u7dS03.js} +1 -1
  22. package/dist/ui/dist/assets/{chunk-AQDTSKZB-DHoGnGyP.js → chunk-AQDTSKZB-B4EzH5gx.js} +1 -1
  23. package/dist/ui/dist/assets/{chunk-DSNLSHJZ-Bk8WO5JF.js → chunk-DSNLSHJZ-CfWeHABr.js} +1 -1
  24. package/dist/ui/dist/assets/{chunk-HAGFOTED-CwAC85lg.js → chunk-HAGFOTED-B8G8PjLZ.js} +1 -1
  25. package/dist/ui/dist/assets/{chunk-HNE4Q36P-CIllywWr.js → chunk-HNE4Q36P-BA-PqoTF.js} +1 -1
  26. package/dist/ui/dist/assets/{chunk-NQOKRVIF-DDlpyYiH.js → chunk-NQOKRVIF-JvhuFcnR.js} +1 -1
  27. package/dist/ui/dist/assets/{chunk-UHW7PFBP-Cxdndcon.js → chunk-UHW7PFBP-BrfL5CGY.js} +1 -1
  28. package/dist/ui/dist/assets/{chunk-VQ5W62QI-CL9d9_iJ.js → chunk-VQ5W62QI-dBJqgdDy.js} +1 -1
  29. package/dist/ui/dist/assets/{chunk-WQQL4CDK-C4kzN8QU.js → chunk-WQQL4CDK-dfqd3k_P.js} +1 -1
  30. package/dist/ui/dist/assets/{chunk-XXDRQBXY-Cv_2FSiY.js → chunk-XXDRQBXY-kViWdB9M.js} +1 -1
  31. package/dist/ui/dist/assets/{chunk-YZO67HFG-oHiuYaCJ.js → chunk-YZO67HFG-ukfs4wT3.js} +1 -1
  32. package/dist/ui/dist/assets/{chunk-ZAUX2LTX-Cy_8d0XT.js → chunk-ZAUX2LTX-BxZHki81.js} +1 -1
  33. package/dist/ui/dist/assets/{chunk-ZCHMQSOR-piIpIcQi.js → chunk-ZCHMQSOR-PjluhR_E.js} +1 -1
  34. package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-DNKRIujO.js → classDiagram-v2-4MYWR3UA-Ct89LJec.js} +1 -1
  35. package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-BWKTGCHY.js → cose-bilkent-JH36ORCC-BnQceGmB.js} +1 -1
  36. package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-yx7KKkAk.js → cynefin-EF2NZ3EQ-D6WkCVE8.js} +1 -1
  37. package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-BTnZ7QVL.js → cynefinDiagram-WF24UDRP-dn6DUPAb.js} +1 -1
  38. package/dist/ui/dist/assets/{dagre-NK4GIZVX-CPh5Zn5I.js → dagre-NK4GIZVX-BtZSlLk7.js} +1 -1
  39. package/dist/ui/dist/assets/{diagram-CBK3MWTC-pI6NFdrD.js → diagram-CBK3MWTC-Ds4_tv6X.js} +1 -1
  40. package/dist/ui/dist/assets/{diagram-I5XIIV2D-DUIfARoS.js → diagram-I5XIIV2D-DcoVkg45.js} +1 -1
  41. package/dist/ui/dist/assets/{diagram-ODIL2UCW-lafSPvAw.js → diagram-ODIL2UCW-T47FAA3k.js} +1 -1
  42. package/dist/ui/dist/assets/{diagram-PFBEJUXE-u1lZas3a.js → diagram-PFBEJUXE-CRZcaVKP.js} +1 -1
  43. package/dist/ui/dist/assets/{diagram-RJA3I4VV-mc0GLMHo.js → diagram-RJA3I4VV-D6KXeUPg.js} +1 -1
  44. package/dist/ui/dist/assets/{diagram-TYUNFSNR-1NRbBrZW.js → diagram-TYUNFSNR-BHEIVyNN.js} +1 -1
  45. package/dist/ui/dist/assets/{dist-CHZ9mrl_.js → dist-DLlKIWBp.js} +1 -1
  46. package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-DdStjTDc.js → ebnfDiagram-V4P3TTPT-nUDvYRBV.js} +1 -1
  47. package/dist/ui/dist/assets/{elk-QONTBCTQ-BzRoFO45.js → elk-QONTBCTQ-D21BcRzc.js} +1 -1
  48. package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-DlMkgQxj.js → erDiagram-KYQIGQAA-CTpHuftU.js} +1 -1
  49. package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-BRYIOrod.js → eventmodeling-K75KTNOO-B6JaKuDY.js} +1 -1
  50. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-BTeOUR-p.js +1 -0
  51. package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-D8fQZ7Ah.js → ganttDiagram-R7GPNTJY-D2-pMcnC.js} +1 -1
  52. package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-C7ZydyWX.js → gitGraph-VSP46ZUC-B2yxwhEo.js} +1 -1
  53. package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-DCItam0v.js → gitGraphDiagram-PDYL7XHY-DnrhBMH1.js} +1 -1
  54. package/dist/ui/dist/assets/index-D74Skifa.css +1 -0
  55. package/dist/ui/dist/assets/index-Dnte9tPd.js +136 -0
  56. package/dist/ui/dist/assets/{info-OHQRW6UA-C3KneBKT.js → info-OHQRW6UA-D8oh6gvs.js} +1 -1
  57. package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-6wK09D32.js → infoDiagram-MVDRKAX5-BLSZjjgB.js} +1 -1
  58. package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-DTP5C8VX.js → ishikawaDiagram-HPEHV2GE-BXiznLSn.js} +1 -1
  59. package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-B--UIMtq.js → journeyDiagram-BOSHW7O7-BSYmVcO_.js} +1 -1
  60. package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-Ku4N9QGt.js → kanban-definition-FNSA4HC5-COthkkzK.js} +1 -1
  61. package/dist/ui/dist/assets/{line-C4-H5h1-.js → line-6ND8OaOL.js} +1 -1
  62. package/dist/ui/dist/assets/{linear-DhAID3hm.js → linear-DbWEPFJX.js} +1 -1
  63. package/dist/ui/dist/assets/{mermaid-parser.core-CMPNc-u6.js → mermaid-parser.core-Btsk294z.js} +3 -3
  64. package/dist/ui/dist/assets/{mermaid.core-Nd_xOg2y.js → mermaid.core-B4llDSFO.js} +4 -4
  65. package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-BlVsym5h.js → mindmap-definition-6OY57UG3-hv2PfX-x.js} +1 -1
  66. package/dist/ui/dist/assets/{packet-JDAUHWVQ-rsk7SM7S.js → packet-JDAUHWVQ-Dr4vBgjt.js} +1 -1
  67. package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-C0L20M8z.js → pegDiagram-ECIUJNBX-DvlFov3N.js} +1 -1
  68. package/dist/ui/dist/assets/{pie-XZMESJXO-B23-iZSN.js → pie-XZMESJXO-C9QsqC4n.js} +1 -1
  69. package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-BqFhe4Oh.js → pieDiagram-GNMPY74T-CrrHOj67.js} +1 -1
  70. package/dist/ui/dist/assets/{prism-core-DYsiqJeR.js → prism-core-Cc8H7LPG.js} +1 -1
  71. package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-BKP75B-m.js → quadrantDiagram-D57K4MC3-DQ7J6-ti.js} +1 -1
  72. package/dist/ui/dist/assets/{radar-ABXABTNO-BrEk2mQ7.js → radar-ABXABTNO-BBBPaujh.js} +1 -1
  73. package/dist/ui/dist/assets/{railroad-I3PHUGI6-wWO4Eapc.js → railroad-I3PHUGI6-C-P6Igaz.js} +1 -1
  74. package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-DfY-IPvE.js → railroad-abnf-LNEFI6M7-CAkorRLs.js} +1 -1
  75. package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-19wedWGk.js → railroad-ebnf-DWYD2UWJ-D8Hz6gCf.js} +1 -1
  76. package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-9vIslnoJ.js → railroad-peg-JR7BVNR7-D-XNMkOs.js} +1 -1
  77. package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-DVnmXs8w.js → railroadDiagram-Y72NRBL7-DE9LwxJQ.js} +1 -1
  78. package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-BEoMdlzR.js → requirementDiagram-EQYSW5RA-FrKFY6NJ.js} +1 -1
  79. package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-D7Tn2Gbs.js → sankeyDiagram-SY24JHO5-BDxI6RDX.js} +1 -1
  80. package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-CyjUDBaQ.js → sequenceDiagram-BLEHNHQU-pV1XcKX7.js} +1 -1
  81. package/dist/ui/dist/assets/{src-BiC9XNcI.js → src-snNJTXXq.js} +1 -1
  82. package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-CMp1VCO9.js → stateDiagram-v2-A6IESKWZ-BSc_5aAf.js} +1 -1
  83. package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-KEciLd6r.js → swimlanes-B3ENRKFQ-DC6qUXLh.js} +1 -1
  84. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-F1HykV3o.js +8 -0
  85. package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-C8YQtv7M.js → timeline-definition-JXYKGLGB-C8L0Dxtu.js} +1 -1
  86. package/dist/ui/dist/assets/{treeView-D4JQ5SDB-DgAzPaOO.js → treeView-D4JQ5SDB-DDyZIG5T.js} +1 -1
  87. package/dist/ui/dist/assets/{treemap-SAJKECNS-eCRqZMYc.js → treemap-SAJKECNS-CUaLgge0.js} +1 -1
  88. package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-Dirryw9G.js → usecaseDiagram-WPOLBNNX-BHRBd5pV.js} +1 -1
  89. package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-BjxQbAUx.js → vennDiagram-Q3Y3IDCW-Du4iwwkh.js} +1 -1
  90. package/dist/ui/dist/assets/{wardley-7MLQ67FV-CqHX5RUz.js → wardley-7MLQ67FV-DUgENI0s.js} +1 -1
  91. package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-Ds1HoEUY.js → wardleyDiagram-IVAIDKWA-B86zOdke.js} +1 -1
  92. package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-kodzDb12.js → xychartDiagram-4URNHDU4-twq9irqO.js} +1 -1
  93. package/dist/ui/dist/index.html +2 -2
  94. package/package.json +1 -1
  95. package/skills/follow-me/SKILL.md +5 -3
  96. package/dist/ui/dist/assets/channel-DPDY0XJP.js +0 -1
  97. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-C1IBh_GZ.js +0 -1
  98. package/dist/ui/dist/assets/index-8BehwQ7E.js +0 -134
  99. package/dist/ui/dist/assets/index-mC2OW2Av.css +0 -1
  100. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DzestpDE.js +0 -8
@@ -1,5 +1,6 @@
1
+ import { parseLoc } from "./coverage.js";
1
2
  import { isRepoPath, isSha, isUnitId } from "./diff.js";
2
- import { diagramProblem, isArray, isCommentId, isConcernId, isFields, isPlanVersion, isSeverity, parsePlacedConcern, } from "./plan.js";
3
+ import { diagramProblem, isArray, isCommentId, isConcernId, isFields, isOfferId, isPlanVersion, isSeverity, parsePlacedConcern, } from "./plan.js";
3
4
  /**
4
5
  * The wire formats of review state, parsed once at the boundary: comments, replies, concern
5
6
  * decisions, the review file, its autosaved draft and the events a hosted session logs. Every
@@ -48,10 +49,159 @@ function parseAnchor(v) {
48
49
  }
49
50
  return { kind: "hunk", unit: v.unit, commit, changes, offsets };
50
51
  }
52
+ function parseLocs(v, at, needed) {
53
+ const what = `${at}: expected a list of { path, start, end }${needed ? " with at least one place" : ""}`;
54
+ if (!isArray(v) || (needed && v.length === 0))
55
+ return what;
56
+ const out = [];
57
+ for (const [i, raw] of v.entries()) {
58
+ const loc = parseLoc(raw);
59
+ if (loc === null)
60
+ return `${at}[${i}]: expected { path, start, end } with 1 <= start <= end`;
61
+ out.push(loc);
62
+ }
63
+ return out;
64
+ }
65
+ const TRANSITION_ENDS = ["from", "to"];
66
+ function escapeRegExp(text) {
67
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
68
+ }
69
+ /**
70
+ * Boundary parse of a reply's state map. Every state has at least one place, every transition
71
+ * names states of the map, and with a stateDiagram every state name appears in it. The reason
72
+ * names the bad entry, as map.states.<name>[i] or map.transitions[i].<field>.
73
+ */
74
+ export function parseStateMap(v, diagram) {
75
+ if (!isFields(v))
76
+ return "map: expected { states, transitions }";
77
+ if (!isFields(v.states) || Object.keys(v.states).length === 0) {
78
+ return "map.states: expected an object of state name to places";
79
+ }
80
+ const states = [];
81
+ for (const [name, raw] of Object.entries(v.states)) {
82
+ if (name.trim() === "")
83
+ return "map.states: a state has an empty name";
84
+ const locs = parseLocs(raw, `map.states.${name}`, true);
85
+ if (typeof locs === "string")
86
+ return locs;
87
+ states.push([name, locs]);
88
+ }
89
+ const named = new Set(states.map(([name]) => name));
90
+ const transitions = [];
91
+ const rawTransitions = v.transitions ?? [];
92
+ if (!isArray(rawTransitions))
93
+ return "map.transitions: expected a list";
94
+ for (const [i, raw] of rawTransitions.entries()) {
95
+ const at = `map.transitions[${i}]`;
96
+ if (!isFields(raw))
97
+ return `${at}: expected { from, to, event, at }`;
98
+ for (const end of TRANSITION_ENDS) {
99
+ const state = raw[end];
100
+ if (typeof state !== "string" || !named.has(state)) {
101
+ return `${at}.${end}: ${JSON.stringify(state)} is not a state in map.states`;
102
+ }
103
+ }
104
+ const { from, to, event } = raw;
105
+ if (typeof from !== "string" || typeof to !== "string")
106
+ return `${at}: expected from and to`;
107
+ if (event !== undefined && typeof event !== "string")
108
+ return `${at}.event: expected a string`;
109
+ const locs = parseLocs(raw.at, `${at}.at`, false);
110
+ if (typeof locs === "string")
111
+ return locs;
112
+ transitions.push({ from, to, ...(event === undefined ? {} : { event }), at: locs });
113
+ }
114
+ const first = (diagram ?? "")
115
+ .split("\n")
116
+ .find((line) => line.trim() !== "")
117
+ ?.trim() ?? "";
118
+ if (first.startsWith("stateDiagram")) {
119
+ for (const name of named) {
120
+ if (!new RegExp(`(^|[^\\w])${escapeRegExp(name)}([^\\w]|$)`, "m").test(diagram ?? "")) {
121
+ return `map.states.${name}: not a state of the stateDiagram`;
122
+ }
123
+ }
124
+ }
125
+ return {
126
+ states: Object.fromEntries(states),
127
+ ...(transitions.length === 0 ? {} : { transitions }),
128
+ };
129
+ }
130
+ /**
131
+ * Where a map points past the tip: a path the tip has no file at, or lines beyond its end. lines
132
+ * holds the line count of every tip file the map names that exists; show prints a path.
133
+ */
134
+ export function mapOffTip(map, lines, show = (path) => path) {
135
+ const check = (loc, at) => {
136
+ const count = lines.get(loc.path);
137
+ if (count === undefined)
138
+ return [`${at}: ${show(loc.path)} is not a file at the tip`];
139
+ if (loc.end > count) {
140
+ return [
141
+ `${at}: lines ${loc.start}-${loc.end} are past the end of ${show(loc.path)}, which has ${count} lines at the tip`,
142
+ ];
143
+ }
144
+ return [];
145
+ };
146
+ return [
147
+ ...Object.entries(map.states).flatMap(([name, locs]) => locs.flatMap((loc, i) => check(loc, `map.states.${name}[${i}]`))),
148
+ ...(map.transitions ?? []).flatMap((t, i) => t.at.flatMap((loc, j) => check(loc, `map.transitions[${i}].at[${j}]`))),
149
+ ];
150
+ }
151
+ function parseSpan(v) {
152
+ if (v === null)
153
+ return null;
154
+ if (!isFields(v) || !isLine(v.start) || !isLine(v.end) || v.end < v.start) {
155
+ return "expected null or { start, end } with 1 <= start <= end";
156
+ }
157
+ return { start: v.start, end: v.end };
158
+ }
159
+ function parseCodeRef(v) {
160
+ if (!isFields(v))
161
+ return "expected an object";
162
+ const { n, path, start, end, at, quote, tip } = v;
163
+ if (!isLine(n))
164
+ return "n is not a number from 1";
165
+ if (typeof path !== "string" || !isRepoPath(path))
166
+ return "path is not a repo path";
167
+ if (!isLine(start) || !isLine(end) || end < start)
168
+ return "expected 1 <= start <= end";
169
+ if (at !== null && at !== "base" && !isLine(at))
170
+ return 'at is not a commit, "base" or null';
171
+ if (!isArray(quote) || !quote.every(isString))
172
+ return "quote is not a list of strings";
173
+ if (typeof tip !== "string" || !isSha(tip))
174
+ return "tip is not a commit sha";
175
+ const lines = parseSpan(v.lines);
176
+ const old = parseSpan(v.old);
177
+ if (typeof lines === "string")
178
+ return `lines: ${lines}`;
179
+ if (typeof old === "string")
180
+ return `old: ${old}`;
181
+ return { n, path, start, end, at, quote: quote.filter(isString), tip, lines, old };
182
+ }
183
+ /** refs as stored beside an agent's words: absent, or a list numbered 1, 2, 3 in order. */
184
+ function parseCodeRefs(v) {
185
+ if (v === undefined)
186
+ return [];
187
+ if (!isArray(v))
188
+ return "refs is not a list";
189
+ const refs = [];
190
+ for (const [i, raw] of v.entries()) {
191
+ const ref = parseCodeRef(raw);
192
+ if (typeof ref === "string")
193
+ return `refs[${i}]: ${ref}`;
194
+ if (ref.n !== i + 1)
195
+ return `refs[${i}]: n is not ${i + 1}`;
196
+ refs.push(ref);
197
+ }
198
+ return refs;
199
+ }
51
200
  /** A stored reply. Replies a client posts go through parseNewReply, which mints id and time. */
52
201
  export function parseReply(v) {
53
- if (!isFields(v))
202
+ if (!isFields(v)) {
54
203
  return "not an object";
204
+ }
55
205
  if (!isReplyId(v.id))
56
206
  return "id is not a reply id";
57
207
  if (v.author !== "agent" && v.author !== "reviewer")
@@ -61,30 +211,62 @@ export function parseReply(v) {
61
211
  }
62
212
  if (v.diagram !== undefined && typeof v.diagram !== "string")
63
213
  return "diagram is not a string";
214
+ const map = v.map === undefined ? undefined : parseStateMap(v.map, v.diagram);
215
+ if (typeof map === "string")
216
+ return map;
217
+ const refs = parseCodeRefs(v.refs);
218
+ if (typeof refs === "string")
219
+ return refs;
64
220
  return {
65
221
  id: v.id,
66
222
  author: v.author,
67
223
  body: v.body,
68
224
  ...(v.diagram === undefined ? {} : { diagram: v.diagram }),
225
+ ...(map === undefined ? {} : { map }),
226
+ ...(refs.length === 0 ? {} : { refs }),
69
227
  createdAt: v.createdAt,
70
228
  };
71
229
  }
72
- /** The body of POST .../replies: who speaks, what, and an optional Mermaid diagram. */
230
+ /**
231
+ * The body of POST .../replies: who speaks, what, and an optional Mermaid diagram. The body may be
232
+ * empty only when the diagram is the answer. Only the agent's words carry refs.
233
+ */
73
234
  export function parseNewReply(v) {
74
- if (!isFields(v))
235
+ if (!isFields(v)) {
75
236
  return "the reply is not an object";
237
+ }
76
238
  if (v.author !== "agent" && v.author !== "reviewer")
77
239
  return 'author is not "agent" or "reviewer"';
78
- if (typeof v.body !== "string" || v.body.trim() === "")
79
- return "body is not a non-empty string";
80
- if (v.diagram === undefined)
81
- return { author: v.author, body: v.body };
240
+ if (typeof v.body !== "string")
241
+ return "body is not a string";
242
+ const refs = parseCodeRefs(v.refs);
243
+ if (typeof refs === "string")
244
+ return refs;
245
+ if (refs.length > 0 && v.author !== "agent")
246
+ return "refs are only for the agent's words";
247
+ const withRefs = refs.length === 0 ? {} : { refs };
248
+ if (v.diagram === undefined) {
249
+ if (v.map !== undefined)
250
+ return "map needs the diagram it ties to code";
251
+ if (v.body.trim() === "")
252
+ return "body is empty; a reply needs words or a diagram";
253
+ return { author: v.author, body: v.body, ...withRefs };
254
+ }
82
255
  if (typeof v.diagram !== "string")
83
256
  return "diagram is not a string";
84
257
  const problem = diagramProblem(v.diagram);
85
258
  if (problem !== null)
86
259
  return `diagram: ${problem}`;
87
- return { author: v.author, body: v.body, diagram: v.diagram };
260
+ const map = v.map === undefined ? undefined : parseStateMap(v.map, v.diagram);
261
+ if (typeof map === "string")
262
+ return map;
263
+ return {
264
+ author: v.author,
265
+ body: v.body,
266
+ diagram: v.diagram,
267
+ ...(map === undefined ? {} : { map }),
268
+ ...withRefs,
269
+ };
88
270
  }
89
271
  function isString(v) {
90
272
  return typeof v === "string";
@@ -133,6 +315,19 @@ export function parseComment(v) {
133
315
  const severity = v.severity;
134
316
  if (severity !== undefined && !isSeverity(severity))
135
317
  return "severity is unknown";
318
+ const offer = v.offer;
319
+ if (offer !== undefined) {
320
+ if (!isOfferId(offer))
321
+ return "offer is not an offer id";
322
+ if (v.kind !== "question" || v.author !== undefined) {
323
+ return "offer is only for a reviewer's question";
324
+ }
325
+ }
326
+ const refs = parseCodeRefs(v.refs);
327
+ if (typeof refs === "string")
328
+ return refs;
329
+ if (refs.length > 0 && v.author !== "agent")
330
+ return "refs are only for an agent's comment";
136
331
  const replies = parseList(v.replies, parseReply, "replies");
137
332
  if (typeof replies === "string")
138
333
  return replies;
@@ -152,6 +347,8 @@ export function parseComment(v) {
152
347
  ...(replies.length === 0 ? {} : { replies }),
153
348
  ...(v.author === "agent" ? { author: "agent" } : {}),
154
349
  ...(v.diagram === undefined ? {} : { diagram: v.diagram }),
350
+ ...(refs.length === 0 ? {} : { refs }),
351
+ ...(offer === undefined ? {} : { offer }),
155
352
  };
156
353
  }
157
354
  function withDecision(concern, v) {
@@ -363,6 +560,9 @@ export function parseReview(v) {
363
560
  const concernsCarried = parseList(v.carriedConcerns, parseCarriedConcern, "carriedConcerns");
364
561
  if (typeof concernsCarried === "string")
365
562
  return concernsCarried;
563
+ const dismissed = parseIds(v.dismissedOffers, isOfferId, "dismissedOffers", "offer");
564
+ if (typeof dismissed === "string")
565
+ return dismissed;
366
566
  return {
367
567
  schema: v.schema,
368
568
  mode,
@@ -376,6 +576,7 @@ export function parseReview(v) {
376
576
  ...(earlier.length === 0 ? {} : { earlierReplies: earlier }),
377
577
  ...(carried === null ? {} : { carried }),
378
578
  ...(concernsCarried.length === 0 ? {} : { carriedConcerns: concernsCarried }),
579
+ ...(dismissed.length === 0 ? {} : { dismissedOffers: dismissed }),
379
580
  };
380
581
  }
381
582
  function lenient(items, parse) {
@@ -403,6 +604,7 @@ export function parseDraft(v) {
403
604
  earlierReplies: lenient(v.earlierReplies, parseEarlierReplies),
404
605
  carried: v.carried === undefined ? null : lenient(v.carried, parseCarriedRef),
405
606
  carriedConcerns: lenient(v.carriedConcerns, parseCarriedConcern),
607
+ dismissedOffers: lenientIds(v.dismissedOffers, isOfferId),
406
608
  };
407
609
  }
408
610
  export function parseEvent(v) {
@@ -444,6 +646,10 @@ export function parseEvent(v) {
444
646
  }
445
647
  return { seq, type: v.type, commentId: id, resolved: v.resolved };
446
648
  }
649
+ case "offer-dismissed":
650
+ return isOfferId(v.offer)
651
+ ? { seq, type: v.type, offer: v.offer }
652
+ : "offer is not an offer id";
447
653
  case "version": {
448
654
  if (!isPlanVersion(v.version) || typeof v.hash !== "string")
449
655
  return "malformed version event";
@@ -15,9 +15,12 @@ export class LcovError extends Error {
15
15
  const RECORD = /^([A-Z][A-Z0-9_]*):(.*)$/;
16
16
  const DA = /^([1-9][0-9]*),([0-9]+)(?:,.*)?$/;
17
17
  const BRDA = /^([1-9][0-9]*),([^,]+),(.+),(-|[0-9]+)$/;
18
+ /** FN:<start>,<name>, or lcov 2's FN:<start>,<end>,<name>. */
19
+ const FN = /^([1-9][0-9]*),(?:([1-9][0-9]*),)?(.+)$/;
18
20
  /**
19
21
  * Reads lcov's tracefile format: TN names the test of the records after it, SF opens a file, DA
20
- * and BRDA give line and branch hits, end_of_record closes it. Other records are skipped.
22
+ * and BRDA give line and branch hits, FN a function's place, end_of_record closes it. Other
23
+ * records are skipped.
21
24
  */
22
25
  export function parseLcov(text) {
23
26
  const records = [];
@@ -50,10 +53,10 @@ export function parseLcov(text) {
50
53
  close();
51
54
  if (value === "")
52
55
  throw new LcovError(n, "SF needs a path");
53
- open = { path: value, test, lines: new Map(), branches: new Map() };
56
+ open = { path: value, test, lines: new Map(), branches: new Map(), functions: [] };
54
57
  continue;
55
58
  }
56
- if (key !== "DA" && key !== "BRDA")
59
+ if (key !== "DA" && key !== "BRDA" && key !== "FN")
57
60
  continue;
58
61
  if (open === null)
59
62
  throw new LcovError(n, `${key} comes before any SF`);
@@ -65,15 +68,27 @@ export function parseLcov(text) {
65
68
  open.lines.set(line, (open.lines.get(line) ?? 0) + Number(da[2]));
66
69
  continue;
67
70
  }
71
+ if (key === "FN") {
72
+ const fn = FN.exec(value);
73
+ if (fn === null)
74
+ throw new LcovError(n, `FN needs <line>,<name>, got "${value}"`);
75
+ const start = Number(fn[1]);
76
+ const end = fn[2] === undefined ? undefined : Number(fn[2]);
77
+ const name = fn[3] ?? "";
78
+ if (end !== undefined && end < start)
79
+ throw new LcovError(n, "FN ends before it starts");
80
+ open.functions.push(end === undefined ? { name, start } : { name, start, end });
81
+ continue;
82
+ }
68
83
  const br = BRDA.exec(value);
69
84
  if (br === null) {
70
85
  throw new LcovError(n, `BRDA needs <line>,<block>,<branch>,<taken>, got "${value}"`);
71
86
  }
72
87
  const line = Number(br[1]);
73
88
  const id = `${br[2]},${br[3]}`;
74
- const taken = br[4] !== "-" && Number(br[4]) > 0;
89
+ const taken = br[4] === "-" ? 0 : Number(br[4]);
75
90
  const branches = open.branches.get(line) ?? new Map();
76
- branches.set(id, (branches.get(id) ?? false) || taken);
91
+ branches.set(id, (branches.get(id) ?? 0) + taken);
77
92
  open.branches.set(line, branches);
78
93
  }
79
94
  close();
@@ -110,10 +125,42 @@ export function tipPlacer(root, cwd, atTip) {
110
125
  return null;
111
126
  };
112
127
  }
128
+ /** Adds a record's branch counts into `into`, keeping lcov's branch order per line. */
129
+ function countInto(into, path, record) {
130
+ const joined = into.get(path) ?? new Map();
131
+ for (const [line, ids] of record.branches) {
132
+ const on = joined.get(line) ?? new Map();
133
+ for (const [id, n] of ids)
134
+ on.set(id, (on.get(id) ?? 0) + n);
135
+ joined.set(line, on);
136
+ }
137
+ into.set(path, joined);
138
+ }
139
+ function countsOf(counting) {
140
+ const out = new Map();
141
+ for (const [path, byLine] of counting) {
142
+ if (byLine.size === 0)
143
+ continue;
144
+ out.set(path, sorted(new Map([...byLine].map(([line, ids]) => [line, [...ids.values()]]))));
145
+ }
146
+ return out;
147
+ }
148
+ /** Functions of both lists, once each by start and name, in line order. */
149
+ function joinFunctions(a, b) {
150
+ const seen = new Map();
151
+ for (const f of [...a, ...b]) {
152
+ const key = `${f.start}|${f.name}`;
153
+ const had = seen.get(key);
154
+ if (had === undefined || (had.end === undefined && f.end !== undefined))
155
+ seen.set(key, f);
156
+ }
157
+ return [...seen.values()].sort((x, y) => x.start - y.start);
158
+ }
113
159
  /** Sums hits and joins branches over records of the same file; drops what `place` rejects. */
114
160
  export function runOf(records, place) {
115
161
  const lines = new Map();
116
162
  const branches = new Map();
163
+ const functions = new Map();
117
164
  const tests = new Map();
118
165
  for (const record of records) {
119
166
  const path = place(record.path);
@@ -123,37 +170,42 @@ export function runOf(records, place) {
123
170
  for (const [line, n] of record.lines)
124
171
  hits.set(line, (hits.get(line) ?? 0) + n);
125
172
  lines.set(path, hits);
126
- const joined = branches.get(path) ?? new Map();
127
- for (const [line, ids] of record.branches) {
128
- const on = joined.get(line) ?? new Map();
129
- for (const [id, taken] of ids)
130
- on.set(id, (on.get(id) ?? false) || taken);
131
- joined.set(line, on);
132
- }
133
- branches.set(path, joined);
173
+ countInto(branches, path, record);
174
+ functions.set(path, joinFunctions(functions.get(path) ?? [], record.functions));
134
175
  if (record.test === null)
135
176
  continue;
136
- const ran = tests.get(record.test) ?? new Map();
137
- const set = ran.get(path) ?? new Set();
177
+ const ran = tests.get(record.test) ?? { lines: new Map(), branches: new Map() };
178
+ const set = ran.lines.get(path) ?? new Set();
138
179
  for (const [line, n] of record.lines)
139
180
  if (n > 0)
140
181
  set.add(line);
141
- ran.set(path, set);
182
+ ran.lines.set(path, set);
183
+ countInto(ran.branches, path, record);
142
184
  tests.set(record.test, ran);
143
185
  }
144
186
  const files = new Map();
145
187
  for (const [path, hits] of lines) {
146
188
  const counted = new Map();
147
189
  for (const [line, ids] of branches.get(path) ?? []) {
148
- counted.set(line, [[...ids.values()].filter(Boolean).length, ids.size]);
190
+ const n = [...ids.values()];
191
+ counted.set(line, [n.filter((c) => c > 0).length, n.length]);
149
192
  }
150
- files.set(path, { lines: sorted(hits), branches: sorted(counted) });
193
+ const fns = functions.get(path) ?? [];
194
+ files.set(path, {
195
+ lines: sorted(hits),
196
+ branches: sorted(counted),
197
+ ...(fns.length === 0 ? {} : { functions: fns }),
198
+ });
151
199
  }
152
200
  return {
153
201
  files,
202
+ counts: countsOf(branches),
154
203
  tests: new Map([...tests].map(([id, ran]) => [
155
204
  id,
156
- new Map([...ran].map(([path, set]) => [path, [...set].sort((a, b) => a - b)])),
205
+ {
206
+ lines: new Map([...ran.lines].map(([path, set]) => [path, [...set].sort((a, b) => a - b)])),
207
+ branches: countsOf(ran.branches),
208
+ },
157
209
  ])),
158
210
  };
159
211
  }
@@ -184,10 +236,17 @@ function mergeMax(a, b) {
184
236
  const [t, n] = branches.get(l) ?? [0, 0];
185
237
  branches.set(l, [Math.max(taken, t), Math.max(total, n)]);
186
238
  }
187
- out.set(path, { lines: sorted(lines), branches: sorted(branches) });
239
+ const functions = joinFunctions(had.functions ?? [], f.functions ?? []);
240
+ out.set(path, {
241
+ lines: sorted(lines),
242
+ branches: sorted(branches),
243
+ ...(functions.length === 0 ? {} : { functions }),
244
+ });
188
245
  }
189
246
  return out;
190
247
  }
248
+ /** How many tip lines a test outside the branch's changes carries for the page's peek. */
249
+ export const PREVIEW_LINES = 8;
191
250
  /**
192
251
  * The coverage after this run. A plain run replaces everything, its TN blocks becoming its tests.
193
252
  * A named test's run adds to what is already there for the same tip, and starts over otherwise.
@@ -195,19 +254,24 @@ function mergeMax(a, b) {
195
254
  export function recordRun(prior, run, at) {
196
255
  const { tip, takenAt, test } = at;
197
256
  if (test === undefined) {
198
- const tests = new Map([...run.tests].map(([id, lines]) => [id, { lines }]));
257
+ const tests = new Map([...run.tests].map(([id, ran]) => [id, testEntry(ran)]));
199
258
  return { tip, takenAt, files: run.files, tests };
200
259
  }
201
260
  const base = prior !== null && prior.tip === tip ? prior : null;
202
261
  const tests = new Map(base?.tests ?? []);
203
262
  const entry = {
204
263
  ...(test.at === undefined ? {} : { at: test.at }),
205
- lines: hitLines(run.files),
264
+ ...(test.preview === undefined ? {} : { preview: test.preview }),
265
+ ...testEntry({ lines: hitLines(run.files), branches: run.counts }),
206
266
  };
207
267
  tests.set(test.id, entry);
208
268
  const files = base === null ? run.files : mergeMax(base.files, run.files);
209
269
  return { tip, takenAt, files, tests };
210
270
  }
271
+ /** A test's lines, and its branch counts when the run had any. */
272
+ function testEntry(ran) {
273
+ return { lines: ran.lines, ...(ran.branches.size === 0 ? {} : { branches: ran.branches }) };
274
+ }
211
275
  /**
212
276
  * Per file, the branch's added rows that lcov calls executable, and how many ran. Every added
213
277
  * row is a tip line, which is where the run was taken. Files without such rows are left out.
@@ -275,13 +339,20 @@ export function serializeCoverage(c) {
275
339
  {
276
340
  lines: Object.fromEntries(f.lines),
277
341
  ...(f.branches.size === 0 ? {} : { branches: Object.fromEntries(f.branches) }),
342
+ ...(f.functions === undefined ? {} : { functions: f.functions }),
278
343
  },
279
344
  ]));
280
345
  const tests = Object.fromEntries([...c.tests].map(([id, t]) => [
281
346
  id,
282
347
  {
283
348
  ...(t.at === undefined ? {} : { at: t.at }),
349
+ ...(t.preview === undefined ? {} : { preview: [...t.preview] }),
284
350
  lines: Object.fromEntries([...t.lines].map(([p, ls]) => [p, [...ls]])),
351
+ ...(t.branches === undefined
352
+ ? {}
353
+ : {
354
+ branches: Object.fromEntries([...t.branches].map(([p, byLine]) => [p, Object.fromEntries(byLine)])),
355
+ }),
285
356
  },
286
357
  ]));
287
358
  return { tip: c.tip, takenAt: c.takenAt, files, ...(c.tests.size === 0 ? {} : { tests }) };
@@ -296,7 +367,8 @@ function isLine(v) {
296
367
  function entries(v) {
297
368
  return isFields(v) ? Object.entries(v) : null;
298
369
  }
299
- function parseLoc(v) {
370
+ /** A { path, start, end } with 1 <= start <= end, or null. */
371
+ export function parseLoc(v) {
300
372
  if (!isFields(v))
301
373
  return null;
302
374
  const { path, start, end } = v;
@@ -316,6 +388,52 @@ export function parseLocArg(raw, place) {
316
388
  const end = m[3] === undefined ? start : Number(m[3]);
317
389
  return end < start ? `--at ${raw} runs backwards` : { path, start, end };
318
390
  }
391
+ function parseFunction(v) {
392
+ if (!isFields(v))
393
+ return null;
394
+ const { name, start, end } = v;
395
+ if (typeof name !== "string" || !isLine(start))
396
+ return null;
397
+ if (end === undefined)
398
+ return { name, start };
399
+ return isLine(end) && end >= start ? { name, start, end } : null;
400
+ }
401
+ function parseFunctions(v) {
402
+ if (v === undefined)
403
+ return undefined;
404
+ if (!Array.isArray(v))
405
+ return "functions is not a list";
406
+ const out = [];
407
+ for (const raw of v) {
408
+ const f = parseFunction(raw);
409
+ if (f === null)
410
+ return "a function is not a { name, start, end? }";
411
+ out.push(f);
412
+ }
413
+ return out;
414
+ }
415
+ function parseCounts(v) {
416
+ if (v === undefined)
417
+ return undefined;
418
+ const files = entries(v);
419
+ if (files === null)
420
+ return "branches is not an object";
421
+ const out = new Map();
422
+ for (const [path, raw] of files) {
423
+ const lines = entries(raw);
424
+ if (!isRepoPath(path) || lines === null)
425
+ return `branches of ${path} are not by line`;
426
+ const byLine = new Map();
427
+ for (const [key, counts] of lines) {
428
+ if (!LINE_KEY.test(key) || !Array.isArray(counts) || !counts.every(isCount)) {
429
+ return `branches of ${path}:${key} are not counts`;
430
+ }
431
+ byLine.set(Number(key), counts.filter(isCount));
432
+ }
433
+ out.set(path, sorted(byLine));
434
+ }
435
+ return out;
436
+ }
319
437
  function parseFile(v) {
320
438
  if (!isFields(v))
321
439
  return "a file is not an object";
@@ -342,7 +460,14 @@ function parseFile(v) {
342
460
  }
343
461
  branches.set(Number(key), [taken, total]);
344
462
  }
345
- return { lines: sorted(hits), branches: sorted(branches) };
463
+ const functions = parseFunctions(v.functions);
464
+ if (typeof functions === "string")
465
+ return functions;
466
+ return {
467
+ lines: sorted(hits),
468
+ branches: sorted(branches),
469
+ ...(functions === undefined ? {} : { functions }),
470
+ };
346
471
  }
347
472
  function parseTest(v) {
348
473
  if (!isFields(v))
@@ -350,6 +475,11 @@ function parseTest(v) {
350
475
  const at = v.at === undefined ? undefined : parseLoc(v.at);
351
476
  if (at === null)
352
477
  return "at is not a { path, start, end }";
478
+ const preview = v.preview;
479
+ if (preview !== undefined &&
480
+ !(Array.isArray(preview) && preview.every((l) => typeof l === "string"))) {
481
+ return "preview is not a list of lines";
482
+ }
353
483
  const lineEntries = entries(v.lines);
354
484
  if (lineEntries === null)
355
485
  return "lines is not an object";
@@ -360,7 +490,15 @@ function parseTest(v) {
360
490
  }
361
491
  lines.set(path, list);
362
492
  }
363
- return { ...(at === undefined ? {} : { at }), lines };
493
+ const branches = parseCounts(v.branches);
494
+ if (typeof branches === "string")
495
+ return branches;
496
+ return {
497
+ ...(at === undefined ? {} : { at }),
498
+ ...(preview === undefined ? {} : { preview: preview.filter((l) => typeof l === "string") }),
499
+ lines,
500
+ ...(branches === undefined ? {} : { branches }),
501
+ };
364
502
  }
365
503
  /** The one place coverage.json becomes a Coverage. */
366
504
  export function parseCoverage(v) {
package/dist/src/host.js CHANGED
@@ -320,6 +320,29 @@ export async function fetchComments(port, summary) {
320
320
  }
321
321
  return out;
322
322
  }
323
+ /**
324
+ * Offers accepted during the hosted review, on this version or a carried one, whose comment has no
325
+ * reply from the agent yet, oldest first.
326
+ */
327
+ export async function fetchWaitingOffers(port, summary) {
328
+ const state = await call(port, "GET", `${summary.url}api/state`);
329
+ if (!isFields(state))
330
+ throw new RegistrationError("the server sent no state");
331
+ const comments = Array.isArray(state["comments"]) ? state["comments"] : [];
332
+ const carried = Array.isArray(state["carried"]) ? state["carried"] : [];
333
+ const said = [
334
+ ...comments.map((raw) => parseComment(raw)),
335
+ ...carried.map((raw) => {
336
+ const c = parseCarried(raw);
337
+ return typeof c === "string" ? c : c.comment;
338
+ }),
339
+ ];
340
+ return said
341
+ .flatMap((c) => (typeof c === "string" ? [] : [c]))
342
+ .filter((c) => !(c.replies ?? []).some((r) => r.author === "agent"))
343
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt))
344
+ .flatMap((c) => (c.offer === undefined ? [] : [{ offer: c.offer, comment: c.id }]));
345
+ }
323
346
  /** One long poll of the session's event log: what happened after `since`, or nothing at timeout. */
324
347
  export async function pollEvents(port, summary, since, timeoutMs) {
325
348
  const path = `${summary.url}api/events?since=${since}&timeout=${timeoutMs}`;