@shiyuphan/follow-me 0.2.0 → 0.4.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 (154) hide show
  1. package/README.md +44 -55
  2. package/dist/src/cli.js +740 -194
  3. package/dist/src/comments.js +109 -6
  4. package/dist/src/diff.js +11 -0
  5. package/dist/src/evolve.js +123 -0
  6. package/dist/src/git.js +12 -0
  7. package/dist/src/host.js +39 -8
  8. package/dist/src/install.js +21 -0
  9. package/dist/src/plan.js +1 -1
  10. package/dist/src/prompt.js +384 -51
  11. package/dist/src/server.js +195 -67
  12. package/dist/src/session.js +53 -12
  13. package/dist/ui/dist/assets/abnfDiagram-WEYOIA3C-byISxePP.js +1 -0
  14. package/dist/ui/dist/assets/arc-CBzsAsP2.js +1 -0
  15. package/dist/ui/dist/assets/architecture-WOLXFQ4H-0OSkIwdB.js +1 -0
  16. package/dist/ui/dist/assets/architectureDiagram-ZHVYKT53-ixHybNqO.js +36 -0
  17. package/dist/ui/dist/assets/array-BifhSqXX.js +1 -0
  18. package/dist/ui/dist/assets/blockDiagram-RJAE7QA3-1F8XL_Ou.js +140 -0
  19. package/dist/ui/dist/assets/c4Diagram-XDPPPE54-D9PFBXnc.js +38 -0
  20. package/dist/ui/dist/assets/channel-B-fwsHf1.js +1 -0
  21. package/dist/ui/dist/assets/chunk-2E7WHVMD-zZdNfMkj.js +88 -0
  22. package/dist/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  23. package/dist/ui/dist/assets/chunk-4EMNKR2T-CTKoxSSj.js +1 -0
  24. package/dist/ui/dist/assets/chunk-55XCU76K-ifFnDlcS.js +1 -0
  25. package/dist/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  26. package/dist/ui/dist/assets/chunk-7S6XSTFO-BCMVdi_1.js +2 -0
  27. package/dist/ui/dist/assets/chunk-AQDTSKZB-DC-5Whju.js +72 -0
  28. package/dist/ui/dist/assets/chunk-DSNLSHJZ-DASbe_gb.js +127 -0
  29. package/dist/ui/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  30. package/dist/ui/dist/assets/chunk-HAGFOTED-CArytDjj.js +62 -0
  31. package/dist/ui/dist/assets/chunk-HNE4Q36P-Dnl-Wtud.js +1 -0
  32. package/dist/ui/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  33. package/dist/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  34. package/dist/ui/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  35. package/dist/ui/dist/assets/chunk-NQOKRVIF-wvSLaeaK.js +213 -0
  36. package/dist/ui/dist/assets/chunk-UHW7PFBP-CckaOGiQ.js +1 -0
  37. package/dist/ui/dist/assets/chunk-VQ5W62QI-kQBMDJ7V.js +1 -0
  38. package/dist/ui/dist/assets/chunk-WQQL4CDK-CMg6bbLU.js +10 -0
  39. package/dist/ui/dist/assets/chunk-XXDRQBXY-CYlFr19K.js +1 -0
  40. package/dist/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  41. package/dist/ui/dist/assets/chunk-YZO67HFG-CqkG1vVk.js +1 -0
  42. package/dist/ui/dist/assets/chunk-ZAUX2LTX-BUNEdx-e.js +1 -0
  43. package/dist/ui/dist/assets/chunk-ZCHMQSOR-CFTbvcrm.js +2 -0
  44. package/dist/ui/dist/assets/classDiagram-v2-4MYWR3UA-t2OdJ0LA.js +217 -0
  45. package/dist/ui/dist/assets/cose-bilkent-JH36ORCC-FGm030gc.js +1 -0
  46. package/dist/ui/dist/assets/cynefin-EF2NZ3EQ-BtU33Lsu.js +1 -0
  47. package/dist/ui/dist/assets/cynefinDiagram-WF24UDRP-DUr-I7hS.js +62 -0
  48. package/dist/ui/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  49. package/dist/ui/dist/assets/dagre-NK4GIZVX-BWTHwJKt.js +4 -0
  50. package/dist/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  51. package/dist/ui/dist/assets/diagram-CBK3MWTC-PcHrsEBn.js +24 -0
  52. package/dist/ui/dist/assets/diagram-I5XIIV2D-CkGtKjYj.js +24 -0
  53. package/dist/ui/dist/assets/diagram-ODIL2UCW-B_HdBB1w.js +30 -0
  54. package/dist/ui/dist/assets/diagram-PFBEJUXE-DL9zayE6.js +3 -0
  55. package/dist/ui/dist/assets/diagram-RJA3I4VV-By5wl8nK.js +41 -0
  56. package/dist/ui/dist/assets/diagram-TYUNFSNR-ziWPVayL.js +200 -0
  57. package/dist/ui/dist/assets/dist-NDVqCM7k.js +1 -0
  58. package/dist/ui/dist/assets/ebnfDiagram-V4P3TTPT-DH4YGkPc.js +1 -0
  59. package/dist/ui/dist/assets/elk-QONTBCTQ-BL2RlCEv.js +27 -0
  60. package/dist/ui/dist/assets/erDiagram-KYQIGQAA-BKbTMaRp.js +99 -0
  61. package/dist/ui/dist/assets/eventmodeling-K75KTNOO-Dk5RCWc-.js +1 -0
  62. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Dkjb3PCK.js +1 -0
  63. package/dist/ui/dist/assets/ganttDiagram-R7GPNTJY-3vM9gxeT.js +292 -0
  64. package/dist/ui/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  65. package/dist/ui/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  66. package/dist/ui/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  67. package/dist/ui/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  68. package/dist/ui/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  69. package/dist/ui/dist/assets/gitGraph-VSP46ZUC-CXGFhz54.js +1 -0
  70. package/dist/ui/dist/assets/gitGraphDiagram-PDYL7XHY-Cl4fwUuA.js +106 -0
  71. package/dist/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
  72. package/dist/ui/dist/assets/index-B8TYcxF8.css +1 -0
  73. package/dist/ui/dist/assets/index-CBbx_pJk.js +134 -0
  74. package/dist/ui/dist/assets/info-OHQRW6UA-DfSsZ8ib.js +1 -0
  75. package/dist/ui/dist/assets/infoDiagram-MVDRKAX5-B_7tyrL5.js +2 -0
  76. package/dist/ui/dist/assets/init-C-OQMol4.js +1 -0
  77. package/dist/ui/dist/assets/ishikawaDiagram-HPEHV2GE-DUvHljpg.js +70 -0
  78. package/dist/ui/dist/assets/journeyDiagram-BOSHW7O7-BCgqzrb8.js +139 -0
  79. package/dist/ui/dist/assets/kanban-definition-FNSA4HC5-DOzZ3NmE.js +89 -0
  80. package/dist/ui/dist/assets/katex-B_JcIwqj.js +257 -0
  81. package/dist/ui/dist/assets/line-B-mPFVll.js +1 -0
  82. package/dist/ui/dist/assets/linear-k7o0jout.js +1 -0
  83. package/dist/ui/dist/assets/mermaid-parser.core-CAyLSQpH.js +7 -0
  84. package/dist/ui/dist/assets/mermaid.core-vfVMjgx2.js +44 -0
  85. package/dist/ui/dist/assets/mindmap-definition-6OY57UG3-B6y-B_r4.js +98 -0
  86. package/dist/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  87. package/dist/ui/dist/assets/packet-JDAUHWVQ-C_r4ed30.js +1 -0
  88. package/dist/ui/dist/assets/path-fybaL0A-.js +1 -0
  89. package/dist/ui/dist/assets/pegDiagram-ECIUJNBX-B8VUm3Bh.js +1 -0
  90. package/dist/ui/dist/assets/pie-XZMESJXO-DcwtbH9y.js +1 -0
  91. package/dist/ui/dist/assets/pieDiagram-GNMPY74T-CvUNBi2F.js +39 -0
  92. package/dist/ui/dist/assets/prism-bash-D6zCJ74D.js +1 -0
  93. package/dist/ui/dist/assets/prism-c-04YixN25.js +1 -0
  94. package/dist/ui/dist/assets/prism-clike-DapgWyxx.js +1 -0
  95. package/dist/ui/dist/assets/prism-core-_2ju85Ya.js +1 -0
  96. package/dist/ui/dist/assets/prism-cpp-C-lJbC-6.js +1 -0
  97. package/dist/ui/dist/assets/prism-css-CPgOzxXp.js +1 -0
  98. package/dist/ui/dist/assets/prism-go-C_8qAH5n.js +1 -0
  99. package/dist/ui/dist/assets/prism-java-6wtusvIl.js +1 -0
  100. package/dist/ui/dist/assets/prism-javascript-BnO-swvr.js +1 -0
  101. package/dist/ui/dist/assets/prism-json-Dp_-W-Hv.js +1 -0
  102. package/dist/ui/dist/assets/prism-jsx-D-D0NWtC.js +1 -0
  103. package/dist/ui/dist/assets/prism-kotlin-BUlJgC-o.js +1 -0
  104. package/dist/ui/dist/assets/prism-markdown-Bc9-36OC.js +1 -0
  105. package/dist/ui/dist/assets/prism-markup-Cp04rIa1.js +1 -0
  106. package/dist/ui/dist/assets/prism-markup-templating-KkSMBj6Y.js +1 -0
  107. package/dist/ui/dist/assets/prism-php-Co4fM40K.js +1 -0
  108. package/dist/ui/dist/assets/prism-python-CfiSVtM6.js +1 -0
  109. package/dist/ui/dist/assets/prism-ruby-DFCASkuD.js +1 -0
  110. package/dist/ui/dist/assets/prism-rust-Bu9N8X3N.js +1 -0
  111. package/dist/ui/dist/assets/prism-sql-CYhVDMEE.js +1 -0
  112. package/dist/ui/dist/assets/prism-swift-B0kWc-ne.js +1 -0
  113. package/dist/ui/dist/assets/prism-toml-DKb54GAT.js +1 -0
  114. package/dist/ui/dist/assets/prism-tsx-p6J2Kc_4.js +1 -0
  115. package/dist/ui/dist/assets/prism-typescript-BMqONzxf.js +1 -0
  116. package/dist/ui/dist/assets/prism-yaml-C0JHu8gr.js +1 -0
  117. package/dist/ui/dist/assets/quadrantDiagram-D57K4MC3-Dg2eVS3j.js +7 -0
  118. package/dist/ui/dist/assets/radar-ABXABTNO-xCNTcZE9.js +1 -0
  119. package/dist/ui/dist/assets/railroad-I3PHUGI6-BI-sIL2Q.js +1 -0
  120. package/dist/ui/dist/assets/railroad-abnf-LNEFI6M7-Ddvk40Wx.js +1 -0
  121. package/dist/ui/dist/assets/railroad-ebnf-DWYD2UWJ-h35gE183.js +1 -0
  122. package/dist/ui/dist/assets/railroad-peg-JR7BVNR7-Zim9iPdp.js +1 -0
  123. package/dist/ui/dist/assets/railroadDiagram-Y72NRBL7-Cz6fdmOQ.js +1 -0
  124. package/dist/ui/dist/assets/requirementDiagram-EQYSW5RA-DTJkCrcD.js +84 -0
  125. package/dist/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  126. package/dist/ui/dist/assets/sankeyDiagram-SY24JHO5-Bg1iiVla.js +40 -0
  127. package/dist/ui/dist/assets/sequenceDiagram-BLEHNHQU-9XfetsRA.js +169 -0
  128. package/dist/ui/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  129. package/dist/ui/dist/assets/src-Dym5afQ5.js +1 -0
  130. package/dist/ui/dist/assets/stateDiagram-v2-A6IESKWZ-CMmcMV8z.js +291 -0
  131. package/dist/ui/dist/assets/swimlanes-B3ENRKFQ-B2PcshTY.js +1 -0
  132. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DfB3XTTn.js +8 -0
  133. package/dist/ui/dist/assets/timeline-definition-JXYKGLGB-Cr1hCNtW.js +120 -0
  134. package/dist/ui/dist/assets/treeView-D4JQ5SDB-jH5ksKnh.js +1 -0
  135. package/dist/ui/dist/assets/treemap-SAJKECNS-BV90e8QL.js +1 -0
  136. package/dist/ui/dist/assets/usecaseDiagram-WPOLBNNX-BNwq5aJV.js +328 -0
  137. package/dist/ui/dist/assets/vennDiagram-Q3Y3IDCW-D-lLyZCL.js +34 -0
  138. package/dist/ui/dist/assets/wardley-7MLQ67FV-CWfHQUGt.js +1 -0
  139. package/dist/ui/dist/assets/wardleyDiagram-IVAIDKWA-BL_qNKVd.js +78 -0
  140. package/dist/ui/dist/assets/xychartDiagram-4URNHDU4-BHPK5Snp.js +7 -0
  141. package/dist/ui/dist/index.html +15 -0
  142. package/dist/ui/dist/style.css +121 -0
  143. package/package.json +31 -10
  144. package/skills/follow-me/SKILL.md +19 -89
  145. package/dist/src/anthropic.js +0 -133
  146. package/dist/ui/app.js +0 -2201
  147. package/dist/ui/concerns.js +0 -165
  148. package/dist/ui/highlight.js +0 -221
  149. package/dist/ui/index.html +0 -94
  150. package/dist/ui/live.js +0 -184
  151. package/dist/ui/playback.js +0 -579
  152. package/dist/ui/questions.js +0 -189
  153. package/dist/ui/style.css +0 -1331
  154. package/dist/ui/summary.js +0 -45
@@ -6,7 +6,7 @@ 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
8
  import { isConcernId, isPlanVersion } from "./plan.js";
9
- import { renderReport } from "./prompt.js";
9
+ import { renderReport, reportItems } from "./prompt.js";
10
10
  import { isLockToken } from "./session.js";
11
11
  /** The requested port is taken; host.ts decides whether that is our server or a stranger. */
12
12
  export class PortInUseError extends Error {
@@ -23,19 +23,16 @@ export class RegistrationError extends Error {
23
23
  }
24
24
  /** GET /api/health answers with this, so a CLI can tell our server from another program. */
25
25
  export const HEALTH_APP = "follow-me";
26
- const UI_DIR = new URL("../ui/", import.meta.url);
27
- /** Every module and the stylesheet in ui/ is served flat, so a new module needs no route. */
28
- const STATIC_FILE = /^\/([a-z][a-z-]*\.(?:js|css))$/;
26
+ /** The page Vite builds from ui/ into ui/dist; `npm run build` makes it. */
27
+ const UI_DIR = new URL("../ui/dist/", import.meta.url);
28
+ /** Hashed bundles under /assets/ and the few flat files of ui/public at the root. */
29
+ const STATIC_FILE = /^\/((?:assets\/)?[\w-]+(?:\.[\w-]+)*\.(js|css|woff2|svg))$/;
29
30
  const STATIC_TYPES = new Map([
30
31
  ["js", "text/javascript; charset=utf-8"],
31
32
  ["css", "text/css; charset=utf-8"],
33
+ ["woff2", "font/woff2"],
34
+ ["svg", "image/svg+xml"],
32
35
  ]);
33
- /** Mermaid's ESM entry imports its chunks by relative path, so they are served beside it. */
34
- const MERMAID_ENTRY = new URL(import.meta.resolve("mermaid/dist/mermaid.esm.min.mjs"));
35
- const MERMAID_CHUNK = /^\/vendor\/chunks\/mermaid\.esm\.min\/([\w-]+\.mjs)$/;
36
- /** Prism is a plain script per component, loaded one at a time by the page as languages need. */
37
- const PRISM_CORE = new URL(import.meta.resolve("prismjs/components/prism-core.min.js"));
38
- const PRISM_PATH = /^\/vendor\/prism\/([a-z][a-z-]*)\.js$/;
39
36
  const MAX_BODY_BYTES = 1_000_000;
40
37
  const BASE32 = "abcdefghijklmnopqrstuvwxyz234567";
41
38
  const COMMENT_ID = /^c_[a-z2-7]{6}$/;
@@ -85,7 +82,13 @@ export async function loadDraft(reviewsDir, version) {
85
82
  }
86
83
  catch (error) {
87
84
  if (isRecord(error) && error["code"] === "ENOENT") {
88
- return { comments: [], concerns: [], resolved: [] };
85
+ return {
86
+ comments: [],
87
+ concerns: [],
88
+ resolved: [],
89
+ excluded: { comments: [], concerns: [] },
90
+ earlierReplies: [],
91
+ };
89
92
  }
90
93
  throw error;
91
94
  }
@@ -196,7 +199,7 @@ function indexPage(rows, now) {
196
199
  const body = items.length === 0
197
200
  ? ` <section class="empty">
198
201
  <h1>No sessions yet</h1>
199
- <p>Run <code>follow-me review</code> to review a plan, or <code>follow-me walk</code> to read a branch someone else wrote. Each one opens here.</p>
202
+ <p>Ask your coding agent to review a branch with follow-me. Each review opens here.</p>
200
203
  <p><a href="https://github.com/phanshiyu/follow-me#readme">How follow-me works</a></p>
201
204
  </section>`
202
205
  : ` <h1>Sessions</h1>
@@ -221,44 +224,18 @@ ${body}
221
224
  </html>
222
225
  `;
223
226
  }
224
- /** The file behind a /vendor/ path, or null. Chunk names are checked, so no path escapes. */
225
- function vendorFile(path) {
226
- const part = PRISM_PATH.exec(path)?.[1];
227
- if (part !== undefined) {
228
- return part === "core" ? PRISM_CORE : new URL(`prism-${part}.min.js`, PRISM_CORE);
229
- }
230
- if (path === "/vendor/mermaid.mjs")
231
- return MERMAID_ENTRY;
232
- const chunk = MERMAID_CHUNK.exec(path)?.[1];
233
- return chunk === undefined ? null : new URL(`chunks/mermaid.esm.min/${chunk}`, MERMAID_ENTRY);
234
- }
235
- async function readUi(name) {
227
+ async function readUi(dir, name) {
236
228
  try {
237
- return await readFile(fileURLToPath(new URL(name, UI_DIR)));
229
+ return await readFile(fileURLToPath(new URL(name, dir)));
238
230
  }
239
231
  catch (error) {
240
232
  if (isRecord(error) && error["code"] === "ENOENT")
241
- throw new HttpError(404, "not found");
233
+ return null;
242
234
  throw error;
243
235
  }
244
236
  }
245
- async function sendVendor(res, file) {
246
- let bytes;
247
- try {
248
- bytes = await readFile(fileURLToPath(file));
249
- }
250
- catch (error) {
251
- if (isRecord(error) && error["code"] === "ENOENT")
252
- throw new HttpError(404, "not found");
253
- throw error;
254
- }
255
- // The bundle only changes with the installed package, and it is several megabytes.
256
- res.writeHead(200, {
257
- "content-type": "text/javascript; charset=utf-8",
258
- "cache-control": "max-age=86400",
259
- });
260
- res.end(bytes);
261
- }
237
+ const NOT_BUILT = `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="color-scheme" content="light dark" /><title>follow-me</title></head><body style="font-family: system-ui; max-width: 40rem; margin: 4rem auto; padding: 0 1rem"><h1>The review page is not built</h1><p>This follow-me runs from a checkout without <code>ui/dist</code>. Run <code>npm run build</code> (or <code>npm run ui:build</code>) in the checkout, then reload.</p></body></html>
238
+ `;
262
239
  function viewOf(registration, loaded, draft) {
263
240
  const { state, reviewsDir } = loaded;
264
241
  return {
@@ -267,7 +244,11 @@ function viewOf(registration, loaded, draft) {
267
244
  comments: new Map(state.comments.map((c) => [c.id, c])),
268
245
  decisions: new Map(draft.concerns.map((d) => [d.concern, d])),
269
246
  resolved: new Set(draft.resolved),
247
+ excludedComments: new Set(draft.excluded.comments),
248
+ excludedConcerns: new Set(draft.excluded.concerns),
249
+ earlierReplies: new Map(draft.earlierReplies.map((e) => [e.comment, e.replies])),
270
250
  unitIds: new Set(state.units.map((u) => u.id)),
251
+ hunkRows: new Map(state.units.flatMap((u) => u.kind === "hunk" ? [[u.id, u.body.length]] : [])),
271
252
  concernIds: new Set(state.commits.flatMap((c) => (c.concerns ?? []).map((k) => k.id))),
272
253
  draftFile: draftPath(reviewsDir, state.version),
273
254
  reviewFile: join(reviewsDir, `v${state.version}.json`),
@@ -277,6 +258,47 @@ function withReplies(comment, replies) {
277
258
  const { replies: _dropped, ...rest } = comment;
278
259
  return replies === undefined || replies.length === 0 ? rest : { ...rest, replies };
279
260
  }
261
+ /** A comment of the previous version with the replies it got during this one. */
262
+ function withEarlier(view, comment) {
263
+ const later = view.earlierReplies.get(comment.id) ?? [];
264
+ return later.length === 0
265
+ ? comment
266
+ : withReplies(comment, [...(comment.replies ?? []), ...later]);
267
+ }
268
+ function previousOf(view) {
269
+ const { previous } = view.loaded;
270
+ if (previous === null)
271
+ return null;
272
+ return { ...previous, comments: previous.comments.map((c) => withEarlier(view, c)) };
273
+ }
274
+ function carriedOf(view) {
275
+ return view.loaded.carried.map((c) => ({ ...c, comment: withEarlier(view, c.comment) }));
276
+ }
277
+ function earlierOf(view) {
278
+ return [...view.earlierReplies].map(([comment, replies]) => ({ comment, replies }));
279
+ }
280
+ /**
281
+ * What a restarted server's log begins with: each saved comment and each reply as the event that
282
+ * first carried it, oldest first, so a client that reads from 0 sees the whole conversation.
283
+ */
284
+ function replayOf(view) {
285
+ const said = [];
286
+ for (const comment of view.comments.values()) {
287
+ said.push({
288
+ at: comment.createdAt,
289
+ event: { type: "comment", comment: withReplies(comment, undefined) },
290
+ });
291
+ for (const reply of comment.replies ?? []) {
292
+ said.push({ at: reply.createdAt, event: { type: "reply", commentId: comment.id, reply } });
293
+ }
294
+ }
295
+ for (const [commentId, replies] of view.earlierReplies) {
296
+ for (const reply of replies) {
297
+ said.push({ at: reply.createdAt, event: { type: "reply", commentId, reply } });
298
+ }
299
+ }
300
+ return said.sort((a, b) => a.at.localeCompare(b.at)).map((s) => s.event);
301
+ }
280
302
  function mintReplyId(taken) {
281
303
  for (;;) {
282
304
  let id = "r_";
@@ -308,17 +330,26 @@ function hostSession(id, registration, loaded, draft, load) {
308
330
  for (const listener of [...listeners])
309
331
  listener();
310
332
  };
333
+ for (const event of replayOf(view))
334
+ emit(event);
311
335
  const waitForEvents = (since, timeoutMs, signal) => new Promise((resolve) => {
312
- const answer = () => resolve({ seq: events.length, events: events.slice(since) });
313
- if (events.length > since || timeoutMs === 0 || signal.aborted)
336
+ const news = () => events.filter((e) => e.seq > since);
337
+ const answer = () => resolve({ seq: events.length, events: news() });
338
+ if (news().length > 0 || timeoutMs === 0 || signal.aborted)
314
339
  return answer();
315
340
  const wake = () => {
341
+ if (news().length === 0 && !signal.aborted)
342
+ return;
316
343
  clearTimeout(timer);
317
344
  waiters.delete(wake);
318
345
  signal.removeEventListener("abort", wake);
319
346
  answer();
320
347
  };
321
- const timer = setTimeout(wake, timeoutMs);
348
+ const timer = setTimeout(() => {
349
+ waiters.delete(wake);
350
+ signal.removeEventListener("abort", wake);
351
+ answer();
352
+ }, timeoutMs);
322
353
  waiters.add(wake);
323
354
  signal.addEventListener("abort", wake);
324
355
  });
@@ -330,6 +361,10 @@ function hostSession(id, registration, loaded, draft, load) {
330
361
  if (swapping)
331
362
  throw new HttpError(409, "the session is switching plan versions; try again");
332
363
  };
364
+ /** The exclusions as stored, or null when nothing is excluded. */
365
+ const excludedOf = () => view.excludedComments.size + view.excludedConcerns.size === 0
366
+ ? null
367
+ : { comments: [...view.excludedComments], concerns: [...view.excludedConcerns] };
333
368
  // Writes are chained so two rapid edits never race on the same file.
334
369
  let writes = Promise.resolve();
335
370
  const saveDraft = () => {
@@ -337,12 +372,16 @@ function hostSession(id, registration, loaded, draft, load) {
337
372
  const file = view.draftFile;
338
373
  const concerns = [...view.decisions.values()];
339
374
  const resolved = [...view.resolved];
375
+ const excluded = excludedOf();
376
+ const earlierReplies = earlierOf(view);
340
377
  const snapshot = JSON.stringify({
341
378
  schema: "follow-me/review-draft@1",
342
379
  planVersion: state.version,
343
380
  comments: [...view.comments.values()],
344
381
  ...(concerns.length === 0 ? {} : { concerns }),
345
382
  ...(resolved.length === 0 ? {} : { resolved }),
383
+ ...(excluded === null ? {} : { excluded }),
384
+ ...(earlierReplies.length === 0 ? {} : { earlierReplies }),
346
385
  }, null, 2);
347
386
  const dir = view.loaded.reviewsDir;
348
387
  writes = writes.then(async () => {
@@ -363,11 +402,21 @@ function hostSession(id, registration, loaded, draft, load) {
363
402
  if (anchor.kind === "hunk" && !view.unitIds.has(anchor.unit)) {
364
403
  throw new HttpError(400, "anchor names an unknown unit");
365
404
  }
405
+ if (anchor.kind === "hunk" && anchor.offsets !== undefined) {
406
+ const rows = view.hunkRows.get(anchor.unit);
407
+ if (rows === undefined)
408
+ throw new HttpError(400, "anchor.offsets names a unit without rows");
409
+ if (anchor.offsets.some((o) => o >= rows)) {
410
+ throw new HttpError(400, `anchor.offsets is out of range, the unit has ${rows} rows`);
411
+ }
412
+ }
366
413
  return parsed;
367
414
  };
368
415
  const review = () => {
369
416
  const concerns = [...view.decisions.values()];
370
417
  const resolved = [...view.resolved];
418
+ const excluded = excludedOf();
419
+ const earlierReplies = earlierOf(view);
371
420
  return {
372
421
  schema: "follow-me/review@1",
373
422
  mode: view.loaded.state.mode,
@@ -377,6 +426,8 @@ function hostSession(id, registration, loaded, draft, load) {
377
426
  comments: [...view.comments.values()],
378
427
  ...(concerns.length === 0 ? {} : { concerns }),
379
428
  ...(resolved.length === 0 ? {} : { resolved }),
429
+ ...(excluded === null ? {} : { excluded }),
430
+ ...(earlierReplies.length === 0 ? {} : { earlierReplies }),
380
431
  };
381
432
  };
382
433
  const report = (finished) => {
@@ -396,7 +447,7 @@ function hostSession(id, registration, loaded, draft, load) {
396
447
  await rm(view.draftFile, { force: true });
397
448
  return view.reviewFile;
398
449
  };
399
- const hasWork = () => view.comments.size > 0 || view.decisions.size > 0;
450
+ const hasWork = () => view.comments.size > 0 || view.decisions.size > 0 || view.earlierReplies.size > 0;
400
451
  return {
401
452
  id,
402
453
  done,
@@ -412,14 +463,17 @@ function hostSession(id, registration, loaded, draft, load) {
412
463
  mode: view.loaded.state.mode,
413
464
  version: view.loaded.state.version,
414
465
  comments: view.comments.size,
466
+ openedAt: new Date(openedAt).toISOString(),
415
467
  }),
416
468
  snapshot: () => ({
417
469
  ...view.loaded.state,
418
470
  comments: [...view.comments.values()],
419
471
  concerns: [...view.decisions.values()],
420
472
  resolved: [...view.resolved],
473
+ excluded: { comments: [...view.excludedComments], concerns: [...view.excludedConcerns] },
421
474
  seq: events.length,
422
- previous: view.loaded.previous,
475
+ previous: previousOf(view),
476
+ carried: carriedOf(view),
423
477
  }),
424
478
  /** Events after `since`, now and as they happen, until unfollowed or the review ends. */
425
479
  latest: () => events.length,
@@ -429,7 +483,7 @@ function hostSession(id, registration, loaded, draft, load) {
429
483
  }
430
484
  let last = since;
431
485
  const flush = () => {
432
- for (const event of events.slice(last)) {
486
+ for (const event of events.filter((e) => e.seq > last)) {
433
487
  send(event);
434
488
  last = event.seq;
435
489
  if (event.type === "ended") {
@@ -450,6 +504,14 @@ function hostSession(id, registration, loaded, draft, load) {
450
504
  return waitForEvents(since, timeoutMs, signal);
451
505
  },
452
506
  report: () => report(review()),
507
+ reportItems: () => {
508
+ const { state } = view.loaded;
509
+ const units = new Map(state.units.map((u) => [u.id, u]));
510
+ return reportItems(review(), state.commits, units, {
511
+ branch: state.anchor.branch,
512
+ responses: [],
513
+ });
514
+ },
453
515
  add: async (raw) => {
454
516
  open();
455
517
  if (!isRecord(raw))
@@ -479,25 +541,37 @@ function hostSession(id, registration, loaded, draft, load) {
479
541
  if (gone === undefined)
480
542
  throw new HttpError(404, "no such comment");
481
543
  view.comments.delete(target);
544
+ view.excludedComments.delete(gone.id);
482
545
  emit({ type: "comment-deleted", id: gone.id });
483
546
  await saveDraft();
484
547
  },
548
+ /** Replies under a comment of this version, or of the previous one, as after a swap. */
485
549
  reply: async (target, raw) => {
486
550
  open();
487
551
  const comment = view.comments.get(target);
488
- if (comment === undefined)
552
+ const earlier = view.loaded.previous?.comments.find((c) => c.id === target);
553
+ const owner = comment ?? earlier;
554
+ if (owner === undefined)
489
555
  throw new HttpError(404, "no such comment");
490
556
  const parsed = parseNewReply(raw);
491
557
  if (typeof parsed === "string")
492
558
  throw new HttpError(400, `malformed reply: ${parsed}`);
493
- const taken = new Set([...view.comments.values()].flatMap((c) => c.replies ?? []).map((r) => r.id));
559
+ const taken = new Set([...view.comments.values(), ...(previousOf(view)?.comments ?? [])]
560
+ .flatMap((c) => c.replies ?? [])
561
+ .map((r) => r.id));
494
562
  const reply = {
495
563
  id: mintReplyId(taken),
496
564
  ...parsed,
497
565
  createdAt: new Date().toISOString(),
498
566
  };
499
- view.comments.set(comment.id, { ...comment, replies: [...(comment.replies ?? []), reply] });
500
- emit({ type: "reply", commentId: comment.id, reply });
567
+ if (comment !== undefined) {
568
+ view.comments.set(comment.id, { ...comment, replies: [...(comment.replies ?? []), reply] });
569
+ }
570
+ else if (earlier !== undefined) {
571
+ const later = view.earlierReplies.get(earlier.id) ?? [];
572
+ view.earlierReplies.set(earlier.id, [...later, reply]);
573
+ }
574
+ emit({ type: "reply", commentId: owner.id, reply });
501
575
  await saveDraft();
502
576
  return reply.id;
503
577
  },
@@ -506,15 +580,54 @@ function hostSession(id, registration, loaded, draft, load) {
506
580
  if (!isConcernId(target) || !view.concernIds.has(target)) {
507
581
  throw new HttpError(404, "no such concern in this plan");
508
582
  }
509
- const decision = parseNewDecision(target, raw);
510
- if (typeof decision === "string")
511
- throw new HttpError(400, `malformed decision: ${decision}`);
583
+ const parsed = parseNewDecision(target, raw);
584
+ if (typeof parsed === "string")
585
+ throw new HttpError(400, `malformed decision: ${parsed}`);
586
+ const decision = { ...parsed, decidedAt: new Date().toISOString() };
512
587
  view.decisions.set(target, decision);
513
588
  const { concern: concernId, ...rest } = decision;
514
589
  emit({ type: "concern", concernId, ...rest });
515
590
  await saveDraft();
516
591
  return decision;
517
592
  },
593
+ /** Leaves a comment or an agent concern out of the hand-off, or puts it back. */
594
+ exclude: async (raw) => {
595
+ open();
596
+ const body = isRecord(raw) ? raw : {};
597
+ const { comment, concern, excluded } = body;
598
+ if (typeof excluded !== "boolean" || (comment === undefined) === (concern === undefined)) {
599
+ throw new HttpError(400, "expected { comment } or { concern }, and { excluded: boolean }");
600
+ }
601
+ if (comment !== undefined) {
602
+ if (!isCommentId(comment) || !view.comments.has(comment)) {
603
+ throw new HttpError(404, "no such comment");
604
+ }
605
+ if (excluded)
606
+ view.excludedComments.add(comment);
607
+ else
608
+ view.excludedComments.delete(comment);
609
+ }
610
+ else {
611
+ if (!isConcernId(concern) || !view.concernIds.has(concern)) {
612
+ throw new HttpError(404, "no such concern in this plan");
613
+ }
614
+ if (excluded)
615
+ view.excludedConcerns.add(concern);
616
+ else
617
+ view.excludedConcerns.delete(concern);
618
+ }
619
+ await saveDraft();
620
+ return { comments: [...view.excludedComments], concerns: [...view.excludedConcerns] };
621
+ },
622
+ /**
623
+ * The reviewer pressed Submit. Every comment already reached the agent, so this only tells it
624
+ * the reviewer is done; `released` stays for pages that still read it.
625
+ */
626
+ submit: () => {
627
+ open();
628
+ emit({ type: "submitted" });
629
+ return { released: 0 };
630
+ },
518
631
  resolve: async (raw) => {
519
632
  open();
520
633
  const body = isRecord(raw) ? raw : {};
@@ -556,7 +669,7 @@ function hostSession(id, registration, loaded, draft, load) {
556
669
  else
557
670
  await rm(view.draftFile, { force: true });
558
671
  view = viewOf({ ...view.registration, version, hash }, next, await loadDraft(next.reviewsDir, version));
559
- emit({ type: "version", version, hash });
672
+ emit({ type: "version", version, hash, carried: carriedOf(view) });
560
673
  }
561
674
  finally {
562
675
  swapping = false;
@@ -588,6 +701,7 @@ function sameRegistration(a, b) {
588
701
  * itself once it has had no session for idleMs.
589
702
  */
590
703
  export function startServer(opts) {
704
+ const uiDir = opts.uiDir ?? UI_DIR;
591
705
  const registry = new Map();
592
706
  const pending = new Map();
593
707
  let boundPort = 0;
@@ -671,17 +785,21 @@ export function startServer(opts) {
671
785
  const method = req.method ?? "GET";
672
786
  const url = new URL(req.url ?? "/", "http://localhost");
673
787
  const path = url.pathname;
674
- const asset = method === "GET" ? STATIC_FILE.exec(path)?.[1] : undefined;
675
- if (asset !== undefined) {
676
- const bytes = await readUi(asset);
677
- const type = STATIC_TYPES.get(asset.slice(asset.lastIndexOf(".") + 1));
678
- res.writeHead(200, { "content-type": type ?? "text/plain", "cache-control": "no-store" });
788
+ const asset = method === "GET" ? STATIC_FILE.exec(path) : null;
789
+ if (asset !== null) {
790
+ const [, name = "", ext = ""] = asset;
791
+ const bytes = await readUi(uiDir, name);
792
+ if (bytes === null)
793
+ throw new HttpError(404, "not found");
794
+ // Vite hashes every name under assets/, so those never change under the same URL.
795
+ const cache = name.startsWith("assets/") ? "max-age=31536000, immutable" : "no-store";
796
+ res.writeHead(200, {
797
+ "content-type": STATIC_TYPES.get(ext) ?? "text/plain",
798
+ "cache-control": cache,
799
+ });
679
800
  res.end(bytes);
680
801
  return;
681
802
  }
682
- const vendor = method === "GET" ? vendorFile(path) : null;
683
- if (vendor)
684
- return sendVendor(res, vendor);
685
803
  if (method === "GET" && path === "/") {
686
804
  const rows = [...registry.values()].map((h) => ({
687
805
  summary: h.summary(),
@@ -711,11 +829,12 @@ export function startServer(opts) {
711
829
  res.end();
712
830
  return;
713
831
  }
714
- if (method === "GET" && rest === "/") {
832
+ if (method === "GET" && (rest === "/" || rest === "/report")) {
715
833
  if (!isSessionId(rawId) || !registry.has(rawId)) {
716
834
  return sendHtml(res, 404, '<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="color-scheme" content="light dark" /><title>follow-me</title><link rel="stylesheet" href="/style.css" /></head><body class="index"><header class="topbar"><strong>follow-me</strong></header><main class="index-main"><section class="empty"><h1>This session has ended</h1><p>It finished, or it never existed. <a href="/">All sessions</a></p></section></main></body></html>\n');
717
835
  }
718
- return sendHtml(res, 200, await readFile(fileURLToPath(new URL("index.html", UI_DIR)), "utf8"));
836
+ const page = await readUi(uiDir, "index.html");
837
+ return page === null ? sendHtml(res, 503, NOT_BUILT) : sendHtml(res, 200, page.toString());
719
838
  }
720
839
  const session = hosted(rawId);
721
840
  if (method === "GET" && rest === "/api/state")
@@ -747,6 +866,9 @@ export function startServer(opts) {
747
866
  const timeout = queryInt(url, "timeout", 0, MAX_POLL_MS, DEFAULT_POLL_MS);
748
867
  return sendJson(res, 200, await session.poll(since, timeout, abort.signal));
749
868
  }
869
+ if (method === "GET" && rest === "/api/report" && url.searchParams.get("format") === "json") {
870
+ return sendJson(res, 200, session.reportItems());
871
+ }
750
872
  if (method === "GET" && rest === "/api/report") {
751
873
  res.writeHead(200, { "content-type": "text/markdown; charset=utf-8" });
752
874
  res.end(session.report());
@@ -762,6 +884,12 @@ export function startServer(opts) {
762
884
  await session.swap(version, hash);
763
885
  return sendJson(res, 200, session.summary());
764
886
  }
887
+ if (method === "POST" && rest === "/api/submit") {
888
+ return sendJson(res, 200, session.submit());
889
+ }
890
+ if (method === "POST" && rest === "/api/excluded") {
891
+ return sendJson(res, 200, await session.exclude(await readJson(req)));
892
+ }
765
893
  if (method === "POST" && rest === "/api/resolved") {
766
894
  return sendJson(res, 200, { resolved: await session.resolve(await readJson(req)) });
767
895
  }
@@ -3,6 +3,7 @@ import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, renameSync,
3
3
  import { basename, dirname, join } from "node:path";
4
4
  import { parseDraft, parseReview } from "./comments.js";
5
5
  import { isSha } from "./diff.js";
6
+ import { evolvePlan } from "./evolve.js";
6
7
  import { branchExists, currentBranch, defaultBaseRef, GitError, loadDiff, objectsDir, QUARANTINE_PREFIX, repoRoot, resolveCommit, } from "./git.js";
7
8
  import { isFields, isPlanVersion, parsePlanFile, planHash, serializePlanFile, } from "./plan.js";
8
9
  export class BranchMovedError extends Error {
@@ -202,8 +203,9 @@ function parseApplied(v) {
202
203
  function makeSession(root, dir, anchor, diff) {
203
204
  const plansDir = join(dir, "plans");
204
205
  const splitReviews = join(dir, "reviews");
205
- const walkReviewsDir = join(splitReviews, "walk");
206
- const reviewsDir = (mode) => mode === "walk" ? walkReviewsDir : splitReviews;
206
+ // Older builds wrote reviewer reviews to reviews/walk/; they stay readable, nothing new goes there.
207
+ const legacyWalkDir = join(splitReviews, "walk");
208
+ const reviewsDir = () => splitReviews;
207
209
  const draftPath = join(dir, "draft.json");
208
210
  const promptPath = join(dir, "prompt.md");
209
211
  const appliedPath = join(dir, "applied.json");
@@ -216,16 +218,16 @@ function makeSession(root, dir, anchor, diff) {
216
218
  }
217
219
  return { version, file, hash: planHash(file) };
218
220
  });
219
- const reviewsIn = (mode) => versions(reviewsDir(mode)).map(({ version, path }) => {
221
+ const reviewsIn = (dir) => versions(dir).map(({ version, path }) => {
220
222
  const review = parseReview(readJson(path));
221
223
  if (typeof review === "string")
222
224
  throw new SessionFileError(`${path}: ${review}`);
223
225
  return { version, review };
224
226
  });
225
- const reviews = () => reviewsIn("split");
226
- const walkReviews = () => [...reviews().filter((r) => r.review.mode === "walk"), ...reviewsIn("walk")].sort((a, b) => a.version - b.version);
227
- const draftVersions = (mode) => {
228
- const dir = reviewsDir(mode);
227
+ const reviews = () => reviewsIn(splitReviews);
228
+ const walkReviews = () => [...reviews().filter((r) => r.review.mode === "walk"), ...reviewsIn(legacyWalkDir)].sort((a, b) => a.version - b.version);
229
+ const draftVersions = () => {
230
+ const dir = reviewsDir();
229
231
  if (!existsSync(dir))
230
232
  return [];
231
233
  return readdirSync(dir).flatMap((name) => {
@@ -234,11 +236,11 @@ function makeSession(root, dir, anchor, diff) {
234
236
  });
235
237
  };
236
238
  const draftReview = (mode, version) => {
237
- const path = join(reviewsDir(mode), `v${version}.draft.json`);
239
+ const path = join(reviewsDir(), `v${version}.draft.json`);
238
240
  const draft = parseDraft(readJson(path));
239
241
  if (typeof draft === "string")
240
242
  throw new SessionFileError(`${path}: ${draft}`);
241
- const { comments, concerns, resolved } = draft;
243
+ const { comments, concerns, resolved, excluded } = draft;
242
244
  return {
243
245
  schema: "follow-me/review@1",
244
246
  mode,
@@ -248,6 +250,7 @@ function makeSession(root, dir, anchor, diff) {
248
250
  comments,
249
251
  ...(concerns.length === 0 ? {} : { concerns }),
250
252
  ...(resolved.length === 0 ? {} : { resolved }),
253
+ ...(excluded.comments.length + excluded.concerns.length === 0 ? {} : { excluded }),
251
254
  };
252
255
  };
253
256
  const priorReview = (mode, before) => {
@@ -255,11 +258,20 @@ function makeSession(root, dir, anchor, diff) {
255
258
  const done = (mode === "walk" ? walkReviews() : reviews().filter((r) => r.review.mode === mode))
256
259
  .filter((r) => below(r.version))
257
260
  .at(-1);
258
- const unfinished = draftVersions(mode)
261
+ const unfinished = draftVersions()
259
262
  .filter((v) => below(v) && v > (done?.version ?? 0))
260
263
  .sort((a, b) => a - b)
261
264
  .at(-1);
262
- return unfinished === undefined ? (done?.review ?? null) : draftReview(mode, unfinished);
265
+ if (unfinished === undefined)
266
+ return done?.review ?? null;
267
+ // A draft answers comments from every earlier version, so the finished review's
268
+ // comments stay visible beside the newer autosave's.
269
+ const current = draftReview(mode, unfinished);
270
+ if (done === undefined)
271
+ return current;
272
+ const seen = new Set(current.comments.map((c) => c.id));
273
+ const earlier = done.review.comments.filter((c) => !seen.has(c.id));
274
+ return { ...current, comments: [...earlier, ...current.comments] };
263
275
  };
264
276
  return {
265
277
  root,
@@ -283,7 +295,7 @@ function makeSession(root, dir, anchor, diff) {
283
295
  writeDraft: (draft) => writeJson(draftPath, draft),
284
296
  writePrompt: (text) => writeText(promptPath, text),
285
297
  writeReview: (review) => {
286
- const path = join(reviewsDir(review.mode), `v${review.planVersion}.json`);
298
+ const path = join(reviewsDir(), `v${review.planVersion}.json`);
287
299
  if (existsSync(path))
288
300
  throw new SessionFileError(`${path} is already written`);
289
301
  writeJson(path, review);
@@ -408,6 +420,35 @@ export async function openSessionAt(dir) {
408
420
  }
409
421
  return openSession(dirname(top), { branch: anchor.branch });
410
422
  }
423
+ /**
424
+ * The diff a stored plan was made on: the session's own when the plan is for this tip, else the
425
+ * plan's base and tip loaded again. Null when those commits are gone from the repo.
426
+ */
427
+ export async function diffOfPlan(session, plan) {
428
+ const { base, tip } = plan.file;
429
+ if (base === session.anchor.base && tip === session.anchor.tip)
430
+ return session.diff;
431
+ try {
432
+ return await loadDiff(session.root, base, tip);
433
+ }
434
+ catch (error) {
435
+ if (error instanceof GitError)
436
+ return null;
437
+ throw error;
438
+ }
439
+ }
440
+ /**
441
+ * How the latest plan carries over to the branch as it is now, or null when that plan was made
442
+ * for this tip. The old diff is loaded from the plan's own base and tip, so this gives the same
443
+ * answer however many times it runs after the move.
444
+ */
445
+ export async function sinceLastPlan(session) {
446
+ const latest = session.plans().at(-1);
447
+ if (latest === undefined || latest.file.tip === session.anchor.tip)
448
+ return null;
449
+ const oldDiff = await loadDiff(session.root, latest.file.base, latest.file.tip);
450
+ return { since: latest.version, ...evolvePlan(latest.file, oldDiff, session.diff) };
451
+ }
411
452
  /** "<name>.<pid>.<random>.tmp" as written by writeJson, the lock and the review server. */
412
453
  function abandonedTemps(dir) {
413
454
  if (!existsSync(dir))
@@ -0,0 +1 @@
1
+ import{m as e,t}from"./mermaid-parser.core-CAyLSQpH.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-Dym5afQ5.js";import"./chunk-DSNLSHJZ-DASbe_gb.js";import{n as i,r as a,t as o}from"./chunk-2E7WHVMD-zZdNfMkj.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-vfVMjgx2.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};
@@ -0,0 +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-NDVqCM7k.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};
@@ -0,0 +1 @@
1
+ import"./chunk-NGNAAXSQ-C2Wy9Xmz.js";export{M as createArchitectureServices}from"./mermaid-parser.core-CAyLSQpH.js";