brane 0.1.1 → 0.3.1

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 (352) hide show
  1. package/AGENTS.md +367 -0
  2. package/CHANGELOG.md +66 -0
  3. package/CONTRIBUTING.md +101 -0
  4. package/DESIGN.md +405 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +213 -0
  9. package/dist/absorb/brane-id.js +11 -0
  10. package/dist/absorb/chatgpt.js +161 -0
  11. package/dist/absorb/claude-code.js +504 -0
  12. package/dist/absorb/index.js +543 -0
  13. package/dist/absorb/organelle.js +345 -0
  14. package/dist/absorb/run.js +520 -0
  15. package/dist/absorb/sources.js +168 -0
  16. package/dist/absorb/types.js +3 -0
  17. package/dist/agent-env.js +45 -0
  18. package/dist/agents.js +1168 -0
  19. package/dist/claude-live-session.js +124 -0
  20. package/dist/cli.js +50 -0
  21. package/dist/codex-tool-log.js +144 -0
  22. package/dist/commands/absorb.js +291 -0
  23. package/dist/commands/attach.js +31 -0
  24. package/dist/commands/certify.js +62 -0
  25. package/dist/commands/config.js +133 -0
  26. package/dist/commands/cost.js +126 -0
  27. package/dist/commands/daemon.js +350 -0
  28. package/dist/commands/doctor.js +262 -0
  29. package/dist/commands/fold.js +95 -0
  30. package/dist/commands/list.js +89 -0
  31. package/dist/commands/mcp.js +84 -0
  32. package/dist/commands/meld.js +360 -0
  33. package/dist/commands/memory.js +215 -0
  34. package/dist/commands/revive.js +74 -0
  35. package/dist/commands/run.js +85 -0
  36. package/dist/commands/spawn.js +1101 -0
  37. package/dist/commands/token.js +154 -0
  38. package/dist/copilot-tool-log.js +145 -0
  39. package/dist/cost/pricing.js +116 -0
  40. package/dist/cost/reprice.js +136 -0
  41. package/dist/crush-tool-log.js +293 -0
  42. package/dist/daemon/agent-scope.js +116 -0
  43. package/dist/daemon/awaiting.js +54 -0
  44. package/dist/daemon/cert-manager.js +349 -0
  45. package/dist/daemon/client.js +262 -0
  46. package/dist/daemon/gated-routes.js +112 -0
  47. package/dist/daemon/keep-awake.js +103 -0
  48. package/dist/daemon/local-socket.js +145 -0
  49. package/dist/daemon/presence-gate.js +206 -0
  50. package/dist/daemon/server.js +6353 -0
  51. package/dist/daemon/signal-source.js +35 -0
  52. package/dist/daemon/tailscale-rebind.js +58 -0
  53. package/dist/daemon/token-ring.js +263 -0
  54. package/dist/daemon/token-routes.js +190 -0
  55. package/dist/db/events.js +152 -0
  56. package/dist/db/mentions.js +179 -0
  57. package/dist/db/schema.js +643 -0
  58. package/dist/db/tokens.js +624 -0
  59. package/dist/db/tool-uses.js +647 -0
  60. package/dist/diagram/index.js +85 -0
  61. package/dist/diagram/label.js +455 -0
  62. package/dist/diagram/layout.js +476 -0
  63. package/dist/diagram/links.js +50 -0
  64. package/dist/diagram/metrics.js +152 -0
  65. package/dist/diagram/parse.js +487 -0
  66. package/dist/diagram/svg.js +389 -0
  67. package/dist/diagram/types.js +9 -0
  68. package/dist/export.js +629 -0
  69. package/dist/files/archive-types.js +2 -0
  70. package/dist/files/archive.js +182 -0
  71. package/dist/files/browse.js +327 -0
  72. package/dist/files/bytes.js +162 -0
  73. package/dist/files/codecs.js +33 -0
  74. package/dist/files/find.js +212 -0
  75. package/dist/files/tar.js +223 -0
  76. package/dist/files/zip.js +209 -0
  77. package/dist/folds.js +72 -0
  78. package/dist/grammars/ebnf.js +92 -0
  79. package/dist/highlight.js +250 -0
  80. package/dist/ids.js +24 -0
  81. package/dist/markdown-diagnostics.js +233 -0
  82. package/dist/math.js +168 -0
  83. package/dist/mcp/server.js +886 -0
  84. package/dist/memory/embedder.js +145 -0
  85. package/dist/memory/embedding-worker.js +71 -0
  86. package/dist/memory/embeddings.js +91 -0
  87. package/dist/memory/index.js +232 -0
  88. package/dist/memory/storage.js +587 -0
  89. package/dist/memory/wiki.js +688 -0
  90. package/dist/memory/wordpiece.js +247 -0
  91. package/dist/mentions-scan.js +40 -0
  92. package/dist/presence.js +259 -0
  93. package/dist/session-affinity.js +185 -0
  94. package/dist/shared/api-request.js +124 -0
  95. package/dist/shared/app-origin.js +26 -0
  96. package/dist/shared/block-glyphs.js +114 -0
  97. package/dist/shared/diagram-css.js +17 -0
  98. package/dist/shared/export-selection.js +101 -0
  99. package/dist/shared/find-in-text.js +110 -0
  100. package/dist/shared/glow-hues.js +225 -0
  101. package/dist/shared/hex-dump.js +62 -0
  102. package/dist/shared/hex-pages.js +52 -0
  103. package/dist/shared/hit-drum.js +79 -0
  104. package/dist/shared/known-nids.js +58 -0
  105. package/dist/shared/list-scroll-keys.js +56 -0
  106. package/dist/shared/markdown-lists.js +72 -0
  107. package/dist/shared/markdown-math.js +485 -0
  108. package/dist/shared/markdown-mentions.js +79 -0
  109. package/dist/shared/markdown-nids.js +41 -0
  110. package/dist/shared/markdown-protocols.js +53 -0
  111. package/dist/shared/markdown-refs.js +123 -0
  112. package/dist/shared/membrane-invasion.js +342 -0
  113. package/dist/shared/membrane-motion.js +83 -0
  114. package/dist/shared/membrane-palette.js +253 -0
  115. package/dist/shared/membrane-terrain.js +212 -0
  116. package/dist/shared/message-selection.js +123 -0
  117. package/dist/shared/mo-glyph.js +57 -0
  118. package/dist/shared/narrative-format.js +237 -0
  119. package/dist/shared/narrative-menu.js +84 -0
  120. package/dist/shared/narrative-sort.js +47 -0
  121. package/dist/shared/narrative-status.js +52 -0
  122. package/dist/shared/no-pan.js +85 -0
  123. package/dist/shared/pane-layout.js +50 -0
  124. package/dist/shared/payload-inline.js +83 -0
  125. package/dist/shared/speaker-color.js +61 -0
  126. package/dist/shared/token-rotation.js +14 -0
  127. package/dist/shared/view-url.js +216 -0
  128. package/dist/shared/wiki-links.js +70 -0
  129. package/dist/shared/wiki-window.js +4 -0
  130. package/dist/tailscale.js +55 -0
  131. package/dist/test/absorb-claude.test.js +892 -0
  132. package/dist/test/agent-scope.test.js +193 -0
  133. package/dist/test/agents.test.js +55 -0
  134. package/dist/test/api-request.test.js +122 -0
  135. package/dist/test/app-host-literal.test.js +53 -0
  136. package/dist/test/awaiting-glow.test.js +54 -0
  137. package/dist/test/awaiting.test.js +122 -0
  138. package/dist/test/bind-token.test.js +88 -0
  139. package/dist/test/block-glyphs.test.js +68 -0
  140. package/dist/test/cert-manager.test.js +262 -0
  141. package/dist/test/claude-live-session.test.js +110 -0
  142. package/dist/test/codex-tool-log.test.js +568 -0
  143. package/dist/test/config.test.js +71 -0
  144. package/dist/test/copilot-tool-log.test.js +93 -0
  145. package/dist/test/cors-preflight.test.js +56 -0
  146. package/dist/test/cost-linger.test.js +31 -0
  147. package/dist/test/cost-pricing.test.js +72 -0
  148. package/dist/test/cost-reprice.test.js +139 -0
  149. package/dist/test/cost-sessions.test.js +301 -0
  150. package/dist/test/crush-tool-log.test.js +289 -0
  151. package/dist/test/daemon-client.test.js +109 -0
  152. package/dist/test/daemon-socket.test.js +293 -0
  153. package/dist/test/diagram-arrows.test.js +205 -0
  154. package/dist/test/diagram-export.test.js +210 -0
  155. package/dist/test/diagram-labels.test.js +313 -0
  156. package/dist/test/diagram-parse.test.js +191 -0
  157. package/dist/test/diagram-render.test.js +175 -0
  158. package/dist/test/diagram-review.test.js +288 -0
  159. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  160. package/dist/test/embedder.test.js +84 -0
  161. package/dist/test/events.test.js +123 -0
  162. package/dist/test/export-selection.test.js +102 -0
  163. package/dist/test/export.test.js +505 -0
  164. package/dist/test/files-browse.test.js +393 -0
  165. package/dist/test/files-find.test.js +272 -0
  166. package/dist/test/find-in-text.test.js +86 -0
  167. package/dist/test/first-paint-viewport.test.js +59 -0
  168. package/dist/test/fixtures/embedding-golden.js +23 -0
  169. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  170. package/dist/test/glow-hues.test.js +202 -0
  171. package/dist/test/harness-cost.test.js +423 -0
  172. package/dist/test/helpers.js +21 -0
  173. package/dist/test/hex-pages.test.js +68 -0
  174. package/dist/test/highlight.test.js +69 -0
  175. package/dist/test/hit-drum.test.js +71 -0
  176. package/dist/test/hook-config.test.js +299 -0
  177. package/dist/test/json-gzip.test.js +70 -0
  178. package/dist/test/key-routing.test.js +58 -0
  179. package/dist/test/known-nids.test.js +106 -0
  180. package/dist/test/link-preview.test.js +67 -0
  181. package/dist/test/list-scroll-keys.test.js +66 -0
  182. package/dist/test/listen-own-words.test.js +147 -0
  183. package/dist/test/listen-timeout.test.js +33 -0
  184. package/dist/test/markdown-diagnostics.test.js +121 -0
  185. package/dist/test/markdown-lists.test.js +182 -0
  186. package/dist/test/markdown-math.test.js +256 -0
  187. package/dist/test/markdown-nids.test.js +146 -0
  188. package/dist/test/markdown-protocols.test.js +139 -0
  189. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  190. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  191. package/dist/test/mcp-narrative.test.js +193 -0
  192. package/dist/test/mcp.test.js +548 -0
  193. package/dist/test/meld.test.js +143 -0
  194. package/dist/test/membrane-invasion.test.js +482 -0
  195. package/dist/test/membrane-motion.test.js +101 -0
  196. package/dist/test/membrane-palette.test.js +166 -0
  197. package/dist/test/membrane-terrain.test.js +170 -0
  198. package/dist/test/memory-log.test.js +202 -0
  199. package/dist/test/mentions-e2e.test.js +261 -0
  200. package/dist/test/mentions-ledger.test.js +265 -0
  201. package/dist/test/mentions-listen.test.js +265 -0
  202. package/dist/test/mentions.test.js +238 -0
  203. package/dist/test/message-selection.test.js +114 -0
  204. package/dist/test/mo-glyph.test.js +22 -0
  205. package/dist/test/model-resolution.test.js +166 -0
  206. package/dist/test/narrate-validate.test.js +77 -0
  207. package/dist/test/narrate-wait.test.js +160 -0
  208. package/dist/test/narrative-format.test.js +219 -0
  209. package/dist/test/narrative-menu.test.js +150 -0
  210. package/dist/test/narrative-sort.test.js +75 -0
  211. package/dist/test/no-pan.test.js +60 -0
  212. package/dist/test/pane-layout.test.js +153 -0
  213. package/dist/test/paths.test.js +37 -0
  214. package/dist/test/payload-inline.test.js +91 -0
  215. package/dist/test/precache-fonts.test.js +72 -0
  216. package/dist/test/presence-gate.test.js +440 -0
  217. package/dist/test/presence.test.js +160 -0
  218. package/dist/test/read-events.test.js +220 -0
  219. package/dist/test/remote-meld.test.js +128 -0
  220. package/dist/test/revive-cli.test.js +93 -0
  221. package/dist/test/search-floor.test.js +206 -0
  222. package/dist/test/search-fusion.test.js +168 -0
  223. package/dist/test/search.test.js +205 -0
  224. package/dist/test/security.test.js +48 -0
  225. package/dist/test/selection-wiring.test.js +61 -0
  226. package/dist/test/session-affinity.test.js +295 -0
  227. package/dist/test/signal-source.test.js +32 -0
  228. package/dist/test/socket-account.test.js +268 -0
  229. package/dist/test/spawn-toml.test.js +72 -0
  230. package/dist/test/speaker-color.test.js +90 -0
  231. package/dist/test/tailscale-rebind.test.js +79 -0
  232. package/dist/test/terminal-cursor.test.js +61 -0
  233. package/dist/test/terminal-resync.test.js +23 -0
  234. package/dist/test/terminal-size.test.js +36 -0
  235. package/dist/test/thin-cli.test.js +382 -0
  236. package/dist/test/token-bootstrap.test.js +272 -0
  237. package/dist/test/token-devices.test.js +646 -0
  238. package/dist/test/token-lapse.test.js +94 -0
  239. package/dist/test/token-ring.test.js +188 -0
  240. package/dist/test/token-rotate-cli.test.js +58 -0
  241. package/dist/test/token-routes.test.js +95 -0
  242. package/dist/test/tool-uses.test.js +1006 -0
  243. package/dist/test/usage-ledger.test.js +126 -0
  244. package/dist/test/view-url.test.js +627 -0
  245. package/dist/test/wiki-links.test.js +57 -0
  246. package/dist/test/wiki-sources.test.js +85 -0
  247. package/dist/test/wiki-window.test.js +31 -0
  248. package/dist/test/wiki.test.js +399 -0
  249. package/dist/test/wordpiece.test.js +68 -0
  250. package/dist/test/wrangler-route.test.js +27 -0
  251. package/dist/tls.js +148 -0
  252. package/dist/tmux.js +213 -0
  253. package/dist/user-agent.js +46 -0
  254. package/docker-entrypoint.sh +154 -0
  255. package/docs/README.md +3 -0
  256. package/docs/absorb.md +231 -0
  257. package/docs/cost-ledger.md +125 -0
  258. package/docs/daemon-api.md +189 -0
  259. package/docs/diagrams.md +163 -0
  260. package/docs/doctor.md +36 -0
  261. package/docs/export-scope.md +69 -0
  262. package/docs/files-view.md +133 -0
  263. package/docs/link-previews.md +38 -0
  264. package/docs/math.md +32 -0
  265. package/docs/mcp.md +69 -0
  266. package/docs/memory-search.md +75 -0
  267. package/docs/mentions.md +101 -0
  268. package/docs/multi-brane.md +14 -0
  269. package/docs/passkeys.md +165 -0
  270. package/docs/presence.md +202 -0
  271. package/docs/releasing.md +104 -0
  272. package/docs/rendering-warnings.md +22 -0
  273. package/docs/request-layer.md +72 -0
  274. package/docs/selection-store.md +50 -0
  275. package/docs/session-affinity.md +61 -0
  276. package/docs/terminal-mirroring.md +243 -0
  277. package/docs/token-rotation.md +431 -0
  278. package/docs/view-urls.md +134 -0
  279. package/docs/web-app.md +23 -0
  280. package/docs/wiki.md +43 -0
  281. package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
  282. package/expo/dist/apple-touch-icon.png +0 -0
  283. package/expo/dist/favicon.svg +3 -0
  284. package/expo/dist/index.html +177 -0
  285. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  301. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  302. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  303. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  304. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  305. package/expo/dist/katex/katex.min.css +1 -0
  306. package/expo/dist/manifest.json +16 -0
  307. package/expo/dist/metadata.json +1 -0
  308. package/expo/dist/og/membrane.v1.png +0 -0
  309. package/expo/dist/pdf.min.mjs +28 -0
  310. package/expo/dist/pdf.worker.min.mjs +28 -0
  311. package/expo/dist/sw.js +154 -0
  312. package/package.json +79 -27
  313. package/protocols/LIVE_loop.md +407 -0
  314. package/protocols/bebrief.md +41 -0
  315. package/protocols/digest.md +231 -0
  316. package/protocols/fly.md +266 -0
  317. package/protocols/idle.md +81 -0
  318. package/protocols/keeper.md +829 -0
  319. package/protocols/markdown.md +80 -0
  320. package/protocols/memory.md +419 -0
  321. package/protocols/mentions.md +93 -0
  322. package/protocols/payloads.md +53 -0
  323. package/protocols/prelude.md +91 -0
  324. package/protocols/ramify.md +105 -0
  325. package/protocols/review.md +356 -0
  326. package/protocols/rrr.md +232 -0
  327. package/protocols/scribe.md +429 -0
  328. package/protocols/summarize.md +203 -0
  329. package/protocols/twin.md +174 -0
  330. package/protocols/wiki.md +438 -0
  331. package/protocols/worker.md +38 -0
  332. package/wiki/shared/append-only-event-log.md +148 -0
  333. package/wiki/shared/index.md +53 -0
  334. package/wiki/shared/owning-your-narratives.md +161 -0
  335. package/wiki/shared/premature-ontologies.md +435 -0
  336. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  337. package/wiki/shared/sequestering-the-narrative.md +143 -0
  338. package/wiki/shared/why-wikis-work.md +216 -0
  339. package/.travis.yml +0 -7
  340. package/lib/brane.cjs.js +0 -12
  341. package/lib/brane.cjs.js.map +0 -1
  342. package/lib/brane.d.ts +0 -11
  343. package/lib/brane.js +0 -6
  344. package/lib/brane.js.map +0 -1
  345. package/lib/tests.d.ts +0 -1
  346. package/lib/tests.js +0 -8
  347. package/lib/tests.js.map +0 -1
  348. package/rollup.config.js +0 -21
  349. package/scripts/test.sh +0 -8
  350. package/src/brane.ts +0 -21
  351. package/src/tests.ts +0 -8
  352. package/tsconfig.json +0 -16
@@ -0,0 +1,85 @@
1
+ /** ```mermaid source → SVG, or a refusal.
2
+ *
3
+ * The one entry point, and deliberately the *same* one for both targets: the
4
+ * app fetches it through `POST /api/diagram`, the HTML export calls it
5
+ * directly in its fence loop. That is the "one renderer, two targets" rule
6
+ * the KaTeX path already follows — an export is a snapshot of the live view
7
+ * rather than a lookalike of it.
8
+ *
9
+ * Unlike KaTeX it runs only on the daemon. Inline math is ubiquitous and
10
+ * light, so it is worth bundling; diagrams are rare and heavy, like the
11
+ * syntax highlighting that already lives behind `POST /api/highlight`.
12
+ *
13
+ * `null` is the fallback signal, and both call sites already handle it: the
14
+ * export substitutes only non-null results, and the app's code view renders
15
+ * plain when its fetch comes back empty. So a refused diagram is the
16
+ * highlighted code block a mermaid fence renders as today — no new path.
17
+ */
18
+ import { createHash } from "node:crypto";
19
+ import { parseDiagram } from "./parse.js";
20
+ import { layoutDiagram } from "./layout.js";
21
+ import { renderSvg } from "./svg.js";
22
+ export { parseLabel, wrapRuns, MARKDOWN_WRAP_WIDTH } from "./label.js";
23
+ export { parseDiagram } from "./parse.js";
24
+ export { layoutDiagram, projectAttachment, faceOffset } from "./layout.js";
25
+ export { basisPath, renderSvg, esc } from "./svg.js";
26
+ export { textWidth, charWidth, runWidth, measureNode, measureEdgeLabel } from "./metrics.js";
27
+ /** A source larger than this is not a diagram anyone will read, and layout is
28
+ * synchronous on the daemon's only thread. */
29
+ export const MAX_SOURCE_BYTES = 24_000;
30
+ /** Likewise: dagre is O(V·E)-ish and a pathological graph must not stall the
31
+ * daemon. Both bounds refuse rather than truncate. */
32
+ export const MAX_NODES = 300;
33
+ export const MAX_EDGES = 600;
34
+ /** Parse once, and check the bounds against the result. `diagnoseDiagram` and
35
+ * `renderDiagram` both need exactly this, and calling the former from the
36
+ * latter parsed the same source twice on the rendering path. */
37
+ function parseWithin(source) {
38
+ if (source.length > MAX_SOURCE_BYTES) {
39
+ return { ok: false, refusal: { kind: "unsupported", message: `diagram source is over ${MAX_SOURCE_BYTES} bytes` } };
40
+ }
41
+ const parsed = parseDiagram(source);
42
+ if (!parsed.ok)
43
+ return { ok: false, refusal: parsed.refusal };
44
+ if (parsed.ir.nodes.length > MAX_NODES) {
45
+ return { ok: false, refusal: { kind: "unsupported", message: `diagram has more than ${MAX_NODES} nodes` } };
46
+ }
47
+ if (parsed.ir.edges.length > MAX_EDGES) {
48
+ return { ok: false, refusal: { kind: "unsupported", message: `diagram has more than ${MAX_EDGES} edges` } };
49
+ }
50
+ return { ok: true, ir: parsed.ir };
51
+ }
52
+ export function diagnoseDiagram(source) {
53
+ return inspectDiagram(source).refusal;
54
+ }
55
+ /** Everything `narrate` reports about one fence, from one parse: whether it
56
+ * draws at all, and if it does, which label markup draws as typed. */
57
+ export function inspectDiagram(source) {
58
+ const checked = parseWithin(source);
59
+ return checked.ok
60
+ ? { refusal: null, literal: checked.ir.literal }
61
+ : { refusal: checked.refusal, literal: [] };
62
+ }
63
+ export function renderDiagram(source, opts = {}) {
64
+ const parsed = parseWithin(source);
65
+ if (!parsed.ok)
66
+ return null;
67
+ try {
68
+ const placed = layoutDiagram(parsed.ir);
69
+ if (!Number.isFinite(placed.width) || !Number.isFinite(placed.height) ||
70
+ placed.width <= 0 || placed.height <= 0) {
71
+ return null;
72
+ }
73
+ const idSuffix = createHash("sha256").update(source).digest("hex").slice(0, 8);
74
+ return {
75
+ svg: renderSvg(placed, { hrefFor: opts.hrefFor, idSuffix, edgeHighlight: opts.edgeHighlight }),
76
+ labels: parsed.ir.nodes.map((n) => n.lines.join(" ")),
77
+ };
78
+ }
79
+ catch {
80
+ // dagre throws on graph shapes it cannot rank. A thrown layout is a
81
+ // fallback, never a 500 — the reader still gets the source.
82
+ return null;
83
+ }
84
+ }
85
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,455 @@
1
+ /** Label text → styled lines.
2
+ *
3
+ * Mermaid has two label grammars, and agents write both:
4
+ *
5
+ * - **Plain** (`A[text]`, `A["text"]`). Mermaid's HTML labels honour a few
6
+ * tags in here — `<br/>`, `<b>`, `<i>` and their kin — and nothing else:
7
+ * `**x**` in a plain label is two asterisks on GitHub too.
8
+ * - **Markdown strings** (``A["`text`"]``, mermaid ≥10). The backticks inside
9
+ * the quotes are the marker, not over-quoting: `**bold**` and `*italic*`
10
+ * render, a newline in the source is a line break, and mermaid wraps long
11
+ * lines to a fixed width.
12
+ *
13
+ * Before this module brane read only `<br/>` from either, so ``"`**Plan**`"``
14
+ * drew its backticks and asterisks as text (claude-cdouqlgt:19 on another
15
+ * daemon, where an agent tried three spellings of bold and got none).
16
+ *
17
+ * **Nothing here builds markup.** A label becomes runs of plain text with
18
+ * flags, and `svg.ts` escapes each run into a `<tspan>` whose attributes it
19
+ * writes itself. A `<script>` in a label is text, exactly as before.
20
+ *
21
+ * **One parse, never a second scanner.** A markdown string goes through
22
+ * `fromMarkdown`, the parser `markdown-diagnostics.ts` already uses, so what
23
+ * counts as bold here is what counts as bold everywhere else in brane. The
24
+ * plain grammar is a fixed set of tags, so a linear scanner over that set
25
+ * is its whole definition.
26
+ *
27
+ * **Tables are brane's own.** A node's markdown string may hold a GFM table,
28
+ * which mermaid shows as "Unsupported markdown". Brane draws it as a grid,
29
+ * as static SVG, so the HTML export carries no logic for it. In an edge
30
+ * label or a subgraph title there is no box for a grid, so there a table
31
+ * shows as its source and is reported.
32
+ *
33
+ * **What still shows as source is reported.** Each label returns `literal`:
34
+ * markup that will be drawn as the characters it is made of. `narrate`
35
+ * turns those into `diagram_label_literal` warnings, so an agent that writes
36
+ * `**x**` in a plain label learns to write a markdown string, instead of
37
+ * getting a clean receipt for a diagram showing asterisks.
38
+ */
39
+ import { fromMarkdown } from "mdast-util-from-markdown";
40
+ import { gfmTable } from "micromark-extension-gfm-table";
41
+ import { gfmTableFromMarkdown } from "mdast-util-gfm-table";
42
+ import { FONT_SIZE, runWidth } from "./metrics.js";
43
+ export const isTable = (l) => !Array.isArray(l);
44
+ /** Line slots a label line takes up. */
45
+ export const slotsOf = (l) => (isTable(l) ? 1 + l.rows.length : 1);
46
+ /** A table as plain lines, for the places that can only hold text. */
47
+ export function tableText(t) {
48
+ return [t.header, ...t.rows].map((cells) => cells.map(lineText).join(" | "));
49
+ }
50
+ /** Label lines with every table flattened to text lines: for edge labels and
51
+ * subgraph titles, where a table is never drawn as one. */
52
+ export function textLines(lines) {
53
+ return lines.flatMap((l) => (isTable(l) ? tableText(l).map((text) => [{ text }]) : [l]));
54
+ }
55
+ /** Mermaid's `flowchart.wrappingWidth` (200px) at mermaid's 16px, scaled to
56
+ * brane's font size. Markdown strings wrap to it; plain labels never wrap,
57
+ * also as mermaid does. Scaled because it is the *em* width that decides
58
+ * where a line breaks: 200px at 14px would keep words together that GitHub
59
+ * splits (`planner picks the operations` is one line at 200px/14px and two
60
+ * on GitHub). */
61
+ export const MARKDOWN_WRAP_WIDTH = (200 * FONT_SIZE) / 16;
62
+ const TAGS = {
63
+ b: "bold", strong: "bold",
64
+ i: "italic", em: "italic",
65
+ code: "code",
66
+ s: "strike", del: "strike", strike: "strike",
67
+ u: "underline",
68
+ };
69
+ /** Every tag-shaped token: `<b>`, `</em>`, `<br/>`, `<br />`, `<foo bar>`. */
70
+ const TAG = /<(\/?)([A-Za-z][A-Za-z0-9]*)\b[^<>]*?(\/?)>/g;
71
+ /** A markdown string: ``"`…`"``, quotes and backticks wrapping the whole. */
72
+ const MD_STRING = /^"`([\s\S]*)`"$/;
73
+ /** `**x**`, `__x__` and `` `x` `` in a plain label are drawn as typed, here
74
+ * and in mermaid. Worth saying, because the fix is one character pair away. */
75
+ const MARKDOWN_IN_PLAIN = /\*\*[^*\s][^*]*\*\*|__[^_\s][^_]*__|`[^`\s][^`]*`/;
76
+ export function parseLabel(raw, opts = {}) {
77
+ const t = raw.trim();
78
+ const md = MD_STRING.exec(t);
79
+ return md ? parseMarkdownLabel(md[1], opts) : parsePlainLabel(t);
80
+ }
81
+ function plainLines(runs) {
82
+ return runs.flatMap((l) => (isTable(l) ? tableText(l) : [lineText(l)]));
83
+ }
84
+ /** A label that is just text — the node id, for one written without a label. */
85
+ export function plainLabel(text) {
86
+ return { lines: [text], runs: [[{ text }]], literal: [] };
87
+ }
88
+ // ── Plain labels ─────────────────────────────────────────────────────────────
89
+ function parsePlainLabel(raw) {
90
+ // One pair of wrapping quotes is label quoting, not text. The old splitter
91
+ // stripped a backtick pair as well; kept, since dropping it would put
92
+ // backticks into diagrams that drew without them yesterday.
93
+ const s = raw.replace(/^(["'`])([\s\S]*)\1$/, "$2");
94
+ const literal = [];
95
+ const b = new LineBuilder();
96
+ const style = {};
97
+ const depth = {};
98
+ const unknown = new Set();
99
+ let at = 0;
100
+ for (const m of s.matchAll(TAG)) {
101
+ b.text(decodeEntities(s.slice(at, m.index)), style);
102
+ at = m.index + m[0].length;
103
+ const [, close, nameRaw, selfClose] = m;
104
+ const name = nameRaw.toLowerCase();
105
+ if (name === "br") {
106
+ b.newline();
107
+ continue;
108
+ }
109
+ const flag = TAGS[name];
110
+ if (!flag || selfClose) {
111
+ b.text(m[0], style);
112
+ // Once per tag name: `<span>…</span>` is one mistake, not two.
113
+ if (!unknown.has(name)) {
114
+ unknown.add(name);
115
+ literal.push(`\`<${name}>\` is not a tag diagrams draw, so it shows as text`);
116
+ }
117
+ continue;
118
+ }
119
+ // Counted, so `<b><b>x</b> y</b>` keeps `y` bold, and a stray `</b>`
120
+ // closes nothing rather than going negative.
121
+ const d = (depth[flag] ?? 0) + (close ? -1 : 1);
122
+ depth[flag] = Math.max(0, d);
123
+ if (depth[flag])
124
+ style[flag] = true;
125
+ else
126
+ delete style[flag];
127
+ }
128
+ b.text(decodeEntities(s.slice(at)), style);
129
+ const out = b.finish(/* dropBlank */ true);
130
+ // Only text outside `<code>`: inside it, `**` is meant literally, and saying
131
+ // otherwise was a false warning.
132
+ const prose = out.runs.map((l) => (isTable(l) ? "" : l.filter((r) => !r.code).map((r) => r.text).join(""))).join("\n");
133
+ const md = MARKDOWN_IN_PLAIN.exec(prose);
134
+ if (md) {
135
+ literal.push(`\`${md[0]}\` shows as typed in a plain label. To style it, make the label a markdown string: backticks just inside the quotes, as in \`A["\`**bold**\`"]\``);
136
+ }
137
+ return { ...out, literal };
138
+ }
139
+ // ── Markdown strings ─────────────────────────────────────────────────────────
140
+ function parseMarkdownLabel(inner, opts) {
141
+ // Continuation lines carry the diagram's indentation, which in markdown
142
+ // would read as an indented code block. Mermaid dedents; trimming each line
143
+ // is the same thing for text that is never meant to be indented.
144
+ const src = inner.split("\n").map((l) => l.trim()).join("\n");
145
+ const literal = [];
146
+ // Swapped while a table cell is read, so the inline walk below fills the
147
+ // cell rather than the label.
148
+ let b = new LineBuilder();
149
+ let tree;
150
+ try {
151
+ // The table extension is loaded whether or not tables are drawn: without
152
+ // it a table parses as paragraphs of pipes, which draw as typed and could
153
+ // not be reported, since nothing would know they were a table.
154
+ tree = fromMarkdown(src, { extensions: [gfmTable()], mdastExtensions: [gfmTableFromMarkdown()] });
155
+ }
156
+ catch {
157
+ b.text(src, {});
158
+ return { ...b.finish(true), literal: ["the markdown string could not be parsed, so it shows as typed"] };
159
+ }
160
+ const sourceOf = (node) => {
161
+ const s = node?.position?.start?.offset, e = node?.position?.end?.offset;
162
+ return typeof s === "number" && typeof e === "number" ? src.slice(s, e) : "";
163
+ };
164
+ const tagStyle = {};
165
+ const inline = (node, style) => {
166
+ const eff = { ...style, ...tagStyle };
167
+ switch (node.type) {
168
+ case "text":
169
+ b.text(node.value, eff);
170
+ return;
171
+ case "break":
172
+ b.newline();
173
+ return;
174
+ case "strong":
175
+ for (const c of node.children)
176
+ inline(c, { ...style, bold: true });
177
+ return;
178
+ case "emphasis":
179
+ for (const c of node.children)
180
+ inline(c, { ...style, italic: true });
181
+ return;
182
+ case "inlineCode":
183
+ b.text(node.value, { ...eff, code: true });
184
+ return;
185
+ case "html": {
186
+ // Inline HTML arrives one tag per node. The tags plain labels honour
187
+ // mean the same here; anything else is shown and reported.
188
+ const m = /^<(\/?)([A-Za-z][A-Za-z0-9]*)\b[^<>]*?(\/?)>$/.exec(node.value);
189
+ const name = m?.[2].toLowerCase();
190
+ if (name === "br") {
191
+ b.newline();
192
+ return;
193
+ }
194
+ const flag = name ? TAGS[name] : undefined;
195
+ if (m && flag && !m[3]) {
196
+ if (m[1])
197
+ delete tagStyle[flag];
198
+ else
199
+ tagStyle[flag] = true;
200
+ return;
201
+ }
202
+ b.text(node.value, eff);
203
+ literal.push(`\`${clip(node.value)}\` is not something diagrams draw, so it shows as text`);
204
+ return;
205
+ }
206
+ case "link":
207
+ case "linkReference":
208
+ for (const c of node.children ?? [])
209
+ inline(c, style);
210
+ literal.push(`links in diagram labels are drawn as their text and are not followed`);
211
+ return;
212
+ default: {
213
+ const text = sourceOf(node);
214
+ if (text) {
215
+ b.text(text, eff);
216
+ literal.push(`\`${clip(text)}\` is markdown diagrams do not draw, so it shows as typed`);
217
+ }
218
+ }
219
+ }
220
+ };
221
+ const block = (node, prefix) => {
222
+ switch (node.type) {
223
+ case "root":
224
+ case "blockquote":
225
+ node.children.forEach((c) => block(c, prefix));
226
+ return;
227
+ case "paragraph":
228
+ case "heading":
229
+ b.startBlock();
230
+ if (prefix)
231
+ b.text(prefix, {});
232
+ for (const c of node.children)
233
+ inline(c, node.type === "heading" ? { bold: true } : {});
234
+ return;
235
+ case "list":
236
+ node.children.forEach((item, i) => {
237
+ const marker = node.ordered ? `${(node.start ?? 1) + i}. ` : "• ";
238
+ (item.children ?? []).forEach((c, j) => block(c, j === 0 ? marker : ""));
239
+ });
240
+ return;
241
+ case "table": {
242
+ if (!opts.tables) {
243
+ literalBlock(node, "tables are drawn only in node labels, so this one");
244
+ return;
245
+ }
246
+ const cellRuns = (cell, header) => {
247
+ const outer = b;
248
+ b = new LineBuilder();
249
+ for (const c of cell.children ?? [])
250
+ inline(c, header ? { bold: true } : {});
251
+ // A cell is one line: a `<br>` inside one becomes a space.
252
+ const line = textLines(b.finish(true).runs).reduce((acc, l, i) => (i ? [...acc, { text: " " }, ...l] : l), []);
253
+ b = outer;
254
+ return line;
255
+ };
256
+ const [head, ...body] = node.children ?? [];
257
+ const width = head?.children?.length ?? 0;
258
+ // GFM pads short rows with empty cells and drops extra ones.
259
+ const row = (r, header) => Array.from({ length: width }, (_, i) => (r.children?.[i] ? cellRuns(r.children[i], header) : []));
260
+ b.startBlock();
261
+ b.table({
262
+ table: true,
263
+ align: Array.from({ length: width }, (_, i) => node.align?.[i] ?? null),
264
+ header: row(head, true),
265
+ rows: body.map((r) => row(r, false)),
266
+ });
267
+ return;
268
+ }
269
+ case "code":
270
+ b.startBlock();
271
+ node.value.split("\n").forEach((l, i) => {
272
+ if (i)
273
+ b.newline();
274
+ b.text(l, { code: true });
275
+ });
276
+ return;
277
+ default:
278
+ literalBlock(node);
279
+ }
280
+ };
281
+ const literalBlock = (node, what) => {
282
+ const text = sourceOf(node);
283
+ if (!text)
284
+ return;
285
+ b.startBlock();
286
+ b.text(text, {});
287
+ literal.push(what
288
+ ? `${what} shows as typed`
289
+ : `\`${clip(text)}\` is markdown diagrams do not draw, so it shows as typed`);
290
+ };
291
+ block(tree, "");
292
+ const out = b.finish(/* dropBlank */ true);
293
+ const runs = out.runs.flatMap((line) => (isTable(line) ? [line] : wrapRuns(line, MARKDOWN_WRAP_WIDTH)));
294
+ return { lines: plainLines(runs), runs, literal };
295
+ }
296
+ // ── Lines ────────────────────────────────────────────────────────────────────
297
+ /** Accumulates runs into lines, merging adjacent runs of the same style and
298
+ * splitting text on its own newlines (a markdown soft break is a `\n` inside
299
+ * a text node, and in a markdown string it is a line break). */
300
+ class LineBuilder {
301
+ lines = [[]];
302
+ blocks = 0;
303
+ text(s, style) {
304
+ if (!s)
305
+ return;
306
+ s.split("\n").forEach((part, i) => {
307
+ if (i)
308
+ this.newline();
309
+ if (part)
310
+ this.push({ text: part, ...clean(style) });
311
+ });
312
+ }
313
+ newline() { this.lines.push([]); }
314
+ /** A new paragraph, heading or list item starts on its own line. */
315
+ startBlock() {
316
+ if (this.blocks++ > 0)
317
+ this.newline();
318
+ }
319
+ /** A table takes the current line if it is empty, and nothing after it
320
+ * shares its slots. */
321
+ table(t) {
322
+ const cur = this.lines[this.lines.length - 1];
323
+ if (!isTable(cur) && cur.length === 0)
324
+ this.lines[this.lines.length - 1] = t;
325
+ else
326
+ this.lines.push(t);
327
+ this.newline();
328
+ }
329
+ push(run) {
330
+ let line = this.lines[this.lines.length - 1];
331
+ if (isTable(line)) {
332
+ line = [];
333
+ this.lines.push(line);
334
+ }
335
+ const last = line[line.length - 1];
336
+ if (last && sameStyle(last, run))
337
+ last.text += run.text;
338
+ else
339
+ line.push(run);
340
+ }
341
+ finish(dropBlank) {
342
+ let runs = this.lines.map((l) => (isTable(l) ? l : trimLine(l)));
343
+ // `A<br/><br/>B` drew as two lines before, not three; an empty label
344
+ // still draws as one empty line.
345
+ if (dropBlank) {
346
+ const kept = runs.filter((l) => isTable(l) || l.length > 0);
347
+ runs = kept.length ? kept : [[]];
348
+ }
349
+ return { lines: plainLines(runs), runs };
350
+ }
351
+ }
352
+ const FLAGS = ["bold", "italic", "code", "strike", "underline"];
353
+ function clean(style) {
354
+ const out = {};
355
+ for (const f of FLAGS)
356
+ if (style[f])
357
+ out[f] = true;
358
+ return out;
359
+ }
360
+ function sameStyle(a, b) {
361
+ return FLAGS.every((f) => !!a[f] === !!b[f]);
362
+ }
363
+ /** Leading and trailing whitespace of the *line*, not of each run: the space
364
+ * in `**Plan** mixin` belongs to the second run and must stay. */
365
+ function trimLine(line) {
366
+ const out = line.map((r) => ({ ...r }));
367
+ while (out.length && !(out[0].text = out[0].text.replace(/^\s+/, "")))
368
+ out.shift();
369
+ while (out.length && !(out[out.length - 1].text = out[out.length - 1].text.replace(/\s+$/, "")))
370
+ out.pop();
371
+ return out;
372
+ }
373
+ export function lineText(line) {
374
+ return line.map((r) => r.text).join("");
375
+ }
376
+ /** Greedy word wrap of one line of runs to `max` px.
377
+ *
378
+ * Words are split at spaces and never broken, so a word longer than the
379
+ * width overflows as it does in mermaid. A run boundary inside a word
380
+ * (`**Plan**s`) is not a break opportunity. */
381
+ export function wrapRuns(line, max, fontSize = FONT_SIZE) {
382
+ const pieces = [];
383
+ for (const r of line) {
384
+ for (const m of r.text.matchAll(/\s+|\S+/g)) {
385
+ pieces.push({ run: { ...r, text: m[0] }, space: /^\s/.test(m[0]) });
386
+ }
387
+ }
388
+ const words = [];
389
+ for (const p of pieces) {
390
+ const prev = words[words.length - 1];
391
+ if (!prev || p.space || prev[0].space)
392
+ words.push([p]);
393
+ else
394
+ prev.push(p);
395
+ }
396
+ const width = (ps) => ps.reduce((w, p) => w + runWidth(p.run, fontSize), 0);
397
+ const out = [[]];
398
+ let w = 0;
399
+ for (const word of words) {
400
+ const cur = out[out.length - 1];
401
+ if (word[0].space) {
402
+ if (cur.length) {
403
+ cur.push(...word);
404
+ w += width(word);
405
+ }
406
+ continue;
407
+ }
408
+ const ww = width(word);
409
+ if (cur.length && w + ww > max) {
410
+ if (cur[cur.length - 1].space)
411
+ cur.pop();
412
+ out.push([...word]);
413
+ w = ww;
414
+ }
415
+ else {
416
+ cur.push(...word);
417
+ w += ww;
418
+ }
419
+ }
420
+ return out.map((ps) => {
421
+ const merged = [];
422
+ for (const p of ps) {
423
+ const last = merged[merged.length - 1];
424
+ if (last && sameStyle(last, p.run))
425
+ last.text += p.run.text;
426
+ else
427
+ merged.push({ ...p.run });
428
+ }
429
+ return trimLine(merged);
430
+ });
431
+ }
432
+ function clip(s, max = 40) {
433
+ const one = s.replace(/\s+/g, " ");
434
+ return one.length > max ? one.slice(0, max - 1) + "…" : one;
435
+ }
436
+ /** The character references a label is likely to hold. Mermaid's HTML labels
437
+ * decode them, so `A[x &lt; y]` reads `x < y` there; brane drew the entity.
438
+ * Decoded after tags are scanned, so a decoded `<` is never read as one. */
439
+ const NAMED = {
440
+ amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\u00a0",
441
+ ndash: "–", mdash: "—", hellip: "…", rarr: "→", larr: "←", harr: "↔",
442
+ le: "≤", ge: "≥", ne: "≠", times: "×", middot: "·", bull: "•", deg: "°",
443
+ };
444
+ function decodeEntities(s) {
445
+ if (!s.includes("&"))
446
+ return s;
447
+ return s.replace(/&(#x[0-9a-f]{1,6}|#[0-9]{1,7}|[a-z]{2,8});/gi, (whole, ref) => {
448
+ if (ref[0] === "#") {
449
+ const cp = ref[1] === "x" || ref[1] === "X" ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10);
450
+ return cp > 0 && cp <= 0x10ffff && !(cp >= 0xd800 && cp <= 0xdfff) ? String.fromCodePoint(cp) : whole;
451
+ }
452
+ return NAMED[ref.toLowerCase()] ?? whole;
453
+ });
454
+ }
455
+ //# sourceMappingURL=label.js.map