brane 0.0.2 → 0.3.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 (338) hide show
  1. package/AGENTS.md +362 -0
  2. package/CHANGELOG.md +33 -0
  3. package/CONTRIBUTING.md +91 -0
  4. package/DESIGN.md +402 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +205 -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 +116 -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 +332 -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 +1094 -0
  37. package/dist/commands/token.js +153 -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 +6349 -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 +633 -0
  58. package/dist/db/tokens.js +583 -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/block-glyphs.js +114 -0
  96. package/dist/shared/diagram-css.js +17 -0
  97. package/dist/shared/export-selection.js +101 -0
  98. package/dist/shared/find-in-text.js +110 -0
  99. package/dist/shared/glow-hues.js +225 -0
  100. package/dist/shared/hex-dump.js +62 -0
  101. package/dist/shared/hex-pages.js +52 -0
  102. package/dist/shared/hit-drum.js +79 -0
  103. package/dist/shared/known-nids.js +58 -0
  104. package/dist/shared/list-scroll-keys.js +56 -0
  105. package/dist/shared/markdown-lists.js +72 -0
  106. package/dist/shared/markdown-math.js +485 -0
  107. package/dist/shared/markdown-mentions.js +79 -0
  108. package/dist/shared/markdown-nids.js +41 -0
  109. package/dist/shared/markdown-protocols.js +53 -0
  110. package/dist/shared/markdown-refs.js +123 -0
  111. package/dist/shared/membrane-invasion.js +342 -0
  112. package/dist/shared/membrane-motion.js +83 -0
  113. package/dist/shared/membrane-palette.js +253 -0
  114. package/dist/shared/membrane-terrain.js +212 -0
  115. package/dist/shared/message-selection.js +123 -0
  116. package/dist/shared/mo-glyph.js +57 -0
  117. package/dist/shared/narrative-format.js +237 -0
  118. package/dist/shared/narrative-menu.js +82 -0
  119. package/dist/shared/narrative-sort.js +47 -0
  120. package/dist/shared/narrative-status.js +52 -0
  121. package/dist/shared/no-pan.js +85 -0
  122. package/dist/shared/pane-layout.js +50 -0
  123. package/dist/shared/payload-inline.js +83 -0
  124. package/dist/shared/speaker-color.js +61 -0
  125. package/dist/shared/token-rotation.js +14 -0
  126. package/dist/shared/view-url.js +197 -0
  127. package/dist/shared/wiki-links.js +70 -0
  128. package/dist/shared/wiki-window.js +4 -0
  129. package/dist/tailscale.js +55 -0
  130. package/dist/test/absorb-claude.test.js +892 -0
  131. package/dist/test/agent-scope.test.js +193 -0
  132. package/dist/test/agents.test.js +55 -0
  133. package/dist/test/api-request.test.js +122 -0
  134. package/dist/test/awaiting-glow.test.js +54 -0
  135. package/dist/test/awaiting.test.js +122 -0
  136. package/dist/test/bind-token.test.js +88 -0
  137. package/dist/test/block-glyphs.test.js +68 -0
  138. package/dist/test/cert-manager.test.js +262 -0
  139. package/dist/test/claude-live-session.test.js +110 -0
  140. package/dist/test/codex-tool-log.test.js +568 -0
  141. package/dist/test/config.test.js +71 -0
  142. package/dist/test/copilot-tool-log.test.js +93 -0
  143. package/dist/test/cors-preflight.test.js +49 -0
  144. package/dist/test/cost-linger.test.js +31 -0
  145. package/dist/test/cost-pricing.test.js +72 -0
  146. package/dist/test/cost-reprice.test.js +139 -0
  147. package/dist/test/cost-sessions.test.js +301 -0
  148. package/dist/test/crush-tool-log.test.js +289 -0
  149. package/dist/test/daemon-client.test.js +109 -0
  150. package/dist/test/daemon-socket.test.js +293 -0
  151. package/dist/test/diagram-arrows.test.js +205 -0
  152. package/dist/test/diagram-export.test.js +210 -0
  153. package/dist/test/diagram-labels.test.js +313 -0
  154. package/dist/test/diagram-parse.test.js +191 -0
  155. package/dist/test/diagram-render.test.js +175 -0
  156. package/dist/test/diagram-review.test.js +288 -0
  157. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  158. package/dist/test/embedder.test.js +84 -0
  159. package/dist/test/events.test.js +123 -0
  160. package/dist/test/export-selection.test.js +102 -0
  161. package/dist/test/export.test.js +505 -0
  162. package/dist/test/files-browse.test.js +393 -0
  163. package/dist/test/files-find.test.js +272 -0
  164. package/dist/test/find-in-text.test.js +86 -0
  165. package/dist/test/first-paint-viewport.test.js +59 -0
  166. package/dist/test/fixtures/embedding-golden.js +23 -0
  167. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  168. package/dist/test/glow-hues.test.js +202 -0
  169. package/dist/test/harness-cost.test.js +423 -0
  170. package/dist/test/helpers.js +21 -0
  171. package/dist/test/hex-pages.test.js +68 -0
  172. package/dist/test/highlight.test.js +69 -0
  173. package/dist/test/hit-drum.test.js +71 -0
  174. package/dist/test/hook-config.test.js +262 -0
  175. package/dist/test/json-gzip.test.js +70 -0
  176. package/dist/test/key-routing.test.js +58 -0
  177. package/dist/test/known-nids.test.js +106 -0
  178. package/dist/test/link-preview.test.js +67 -0
  179. package/dist/test/list-scroll-keys.test.js +66 -0
  180. package/dist/test/listen-own-words.test.js +147 -0
  181. package/dist/test/listen-timeout.test.js +33 -0
  182. package/dist/test/markdown-diagnostics.test.js +121 -0
  183. package/dist/test/markdown-lists.test.js +182 -0
  184. package/dist/test/markdown-math.test.js +256 -0
  185. package/dist/test/markdown-nids.test.js +146 -0
  186. package/dist/test/markdown-protocols.test.js +139 -0
  187. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  188. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  189. package/dist/test/mcp-narrative.test.js +193 -0
  190. package/dist/test/mcp.test.js +548 -0
  191. package/dist/test/meld.test.js +85 -0
  192. package/dist/test/membrane-invasion.test.js +482 -0
  193. package/dist/test/membrane-motion.test.js +101 -0
  194. package/dist/test/membrane-palette.test.js +166 -0
  195. package/dist/test/membrane-terrain.test.js +170 -0
  196. package/dist/test/memory-log.test.js +202 -0
  197. package/dist/test/mentions-e2e.test.js +261 -0
  198. package/dist/test/mentions-ledger.test.js +265 -0
  199. package/dist/test/mentions-listen.test.js +265 -0
  200. package/dist/test/mentions.test.js +238 -0
  201. package/dist/test/message-selection.test.js +114 -0
  202. package/dist/test/mo-glyph.test.js +22 -0
  203. package/dist/test/model-resolution.test.js +166 -0
  204. package/dist/test/narrate-validate.test.js +77 -0
  205. package/dist/test/narrate-wait.test.js +160 -0
  206. package/dist/test/narrative-format.test.js +219 -0
  207. package/dist/test/narrative-menu.test.js +137 -0
  208. package/dist/test/narrative-sort.test.js +75 -0
  209. package/dist/test/no-pan.test.js +60 -0
  210. package/dist/test/pane-layout.test.js +153 -0
  211. package/dist/test/paths.test.js +37 -0
  212. package/dist/test/payload-inline.test.js +91 -0
  213. package/dist/test/precache-fonts.test.js +72 -0
  214. package/dist/test/presence-gate.test.js +440 -0
  215. package/dist/test/presence.test.js +160 -0
  216. package/dist/test/read-events.test.js +220 -0
  217. package/dist/test/remote-meld.test.js +115 -0
  218. package/dist/test/revive-cli.test.js +93 -0
  219. package/dist/test/search-floor.test.js +206 -0
  220. package/dist/test/search-fusion.test.js +168 -0
  221. package/dist/test/search.test.js +205 -0
  222. package/dist/test/security.test.js +48 -0
  223. package/dist/test/selection-wiring.test.js +61 -0
  224. package/dist/test/session-affinity.test.js +295 -0
  225. package/dist/test/signal-source.test.js +32 -0
  226. package/dist/test/socket-account.test.js +268 -0
  227. package/dist/test/spawn-toml.test.js +72 -0
  228. package/dist/test/speaker-color.test.js +90 -0
  229. package/dist/test/tailscale-rebind.test.js +79 -0
  230. package/dist/test/terminal-cursor.test.js +61 -0
  231. package/dist/test/terminal-resync.test.js +23 -0
  232. package/dist/test/thin-cli.test.js +382 -0
  233. package/dist/test/token-bootstrap.test.js +272 -0
  234. package/dist/test/token-devices.test.js +587 -0
  235. package/dist/test/token-lapse.test.js +94 -0
  236. package/dist/test/token-ring.test.js +188 -0
  237. package/dist/test/token-rotate-cli.test.js +58 -0
  238. package/dist/test/token-routes.test.js +95 -0
  239. package/dist/test/tool-uses.test.js +1006 -0
  240. package/dist/test/usage-ledger.test.js +126 -0
  241. package/dist/test/view-url.test.js +581 -0
  242. package/dist/test/wiki-links.test.js +57 -0
  243. package/dist/test/wiki-sources.test.js +85 -0
  244. package/dist/test/wiki-window.test.js +31 -0
  245. package/dist/test/wiki.test.js +399 -0
  246. package/dist/test/wordpiece.test.js +68 -0
  247. package/dist/tls.js +148 -0
  248. package/dist/tmux.js +202 -0
  249. package/dist/user-agent.js +46 -0
  250. package/docker-entrypoint.sh +141 -0
  251. package/docs/README.md +3 -0
  252. package/docs/absorb.md +231 -0
  253. package/docs/cost-ledger.md +125 -0
  254. package/docs/daemon-api.md +189 -0
  255. package/docs/diagrams.md +163 -0
  256. package/docs/doctor.md +36 -0
  257. package/docs/export-scope.md +69 -0
  258. package/docs/files-view.md +133 -0
  259. package/docs/link-previews.md +38 -0
  260. package/docs/math.md +32 -0
  261. package/docs/mcp.md +69 -0
  262. package/docs/memory-search.md +75 -0
  263. package/docs/mentions.md +101 -0
  264. package/docs/multi-brane.md +14 -0
  265. package/docs/passkeys.md +165 -0
  266. package/docs/presence.md +202 -0
  267. package/docs/releasing.md +53 -0
  268. package/docs/rendering-warnings.md +22 -0
  269. package/docs/request-layer.md +72 -0
  270. package/docs/selection-store.md +50 -0
  271. package/docs/session-affinity.md +61 -0
  272. package/docs/terminal-mirroring.md +211 -0
  273. package/docs/token-rotation.md +413 -0
  274. package/docs/view-urls.md +114 -0
  275. package/docs/web-app.md +23 -0
  276. package/docs/wiki.md +43 -0
  277. package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -0
  278. package/expo/dist/apple-touch-icon.png +0 -0
  279. package/expo/dist/favicon.svg +3 -0
  280. package/expo/dist/index.html +177 -0
  281. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  282. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  283. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  284. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  285. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  301. package/expo/dist/katex/katex.min.css +1 -0
  302. package/expo/dist/manifest.json +16 -0
  303. package/expo/dist/metadata.json +1 -0
  304. package/expo/dist/og/membrane.v1.png +0 -0
  305. package/expo/dist/pdf.min.mjs +28 -0
  306. package/expo/dist/pdf.worker.min.mjs +28 -0
  307. package/expo/dist/sw.js +154 -0
  308. package/package.json +81 -23
  309. package/protocols/LIVE_loop.md +407 -0
  310. package/protocols/bebrief.md +41 -0
  311. package/protocols/digest.md +231 -0
  312. package/protocols/fly.md +266 -0
  313. package/protocols/idle.md +81 -0
  314. package/protocols/keeper.md +829 -0
  315. package/protocols/markdown.md +80 -0
  316. package/protocols/memory.md +419 -0
  317. package/protocols/mentions.md +93 -0
  318. package/protocols/payloads.md +53 -0
  319. package/protocols/prelude.md +91 -0
  320. package/protocols/ramify.md +105 -0
  321. package/protocols/review.md +356 -0
  322. package/protocols/rrr.md +232 -0
  323. package/protocols/scribe.md +429 -0
  324. package/protocols/summarize.md +203 -0
  325. package/protocols/twin.md +174 -0
  326. package/protocols/wiki.md +438 -0
  327. package/protocols/worker.md +38 -0
  328. package/wiki/shared/append-only-event-log.md +148 -0
  329. package/wiki/shared/index.md +48 -0
  330. package/wiki/shared/owning-your-narratives.md +161 -0
  331. package/wiki/shared/premature-ontologies.md +435 -0
  332. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  333. package/wiki/shared/sequestering-the-narrative.md +143 -0
  334. package/wiki/shared/why-wikis-work.md +216 -0
  335. package/.npmignore +0 -2
  336. package/.travis.yml +0 -5
  337. package/index.js +0 -8
  338. package/index.js~ +0 -12
package/dist/folds.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Registering a fold — the one implementation behind `POST /api/folds`,
3
+ * `brane fold`, and `brane absorb --fold`.
4
+ *
5
+ * Two callers, one rule with one switch. A phone on the tailnet must not be
6
+ * able to make `/` or `/etc` a spawn point, so a remote client's folds are
7
+ * confined to the operator's home directory. The operator's own shell is
8
+ * not so limited — `brane fold /Volumes/scratch/project` has always worked —
9
+ * and now that the CLI reaches this through the daemon, "the operator's own
10
+ * shell" has to be recognized on the wire. It is recognized as **loopback**:
11
+ * the CLI always talks to `127.0.0.1` (`buildDaemonBaseUrl`), and nothing
12
+ * outside this machine can. The route decides `confineToHome` from
13
+ * `req.socket.remoteAddress`; this module only applies it.
14
+ */
15
+ import { existsSync, statSync } from "node:fs";
16
+ import { resolve } from "node:path";
17
+ import { homedir } from "node:os";
18
+ import { fromPortablePath, toPortablePath, getBraneId } from "./db/schema.js";
19
+ import { getConfig } from "./commands/config.js";
20
+ /** A registration the daemon refuses. `status` is the HTTP status the route
21
+ * answers with — 400 for a bad path, 403 for one outside the caller's reach. */
22
+ export class FoldRegistrationError extends Error {
23
+ status;
24
+ constructor(message, status) {
25
+ super(message);
26
+ this.status = status;
27
+ }
28
+ }
29
+ /** The pure decision: is `absolute` a path this caller may register? Exported
30
+ * so the rule can be pinned without a database. Returns the refusal, or
31
+ * `null` when the path is allowed. */
32
+ export function foldPathRefusal(absolute, opts) {
33
+ const home = opts.home ?? homedir();
34
+ if (opts.confineToHome) {
35
+ if (absolute !== home && !absolute.startsWith(home + "/")) {
36
+ return new FoldRegistrationError(`Folds must live under your home directory (${home})`, 403);
37
+ }
38
+ if (absolute === home) {
39
+ return new FoldRegistrationError("Refusing to fold your home directory itself; pick a subdirectory", 403);
40
+ }
41
+ }
42
+ if (!existsSync(absolute) || !statSync(absolute).isDirectory()) {
43
+ return new FoldRegistrationError(`Not a directory: ${absolute}`, 400);
44
+ }
45
+ return null;
46
+ }
47
+ /** Register `rawPath` (absolute or `~/portable`) as a fold. Idempotent:
48
+ * re-registering an existing fold un-archives it and, when `braneId` is
49
+ * given, rebinds it; otherwise it is a no-op. Never deletes. */
50
+ export function registerFold(db, rawPath, opts) {
51
+ const absolute = resolve(fromPortablePath(rawPath.trim()));
52
+ const refusal = foldPathRefusal(absolute, opts);
53
+ if (refusal)
54
+ throw refusal;
55
+ const portable = toPortablePath(absolute);
56
+ const existing = db.prepare("SELECT * FROM folds WHERE path = ?").get(portable);
57
+ if (existing) {
58
+ if (existing.archived) {
59
+ db.prepare("UPDATE folds SET archived = 0 WHERE path = ?").run(portable);
60
+ }
61
+ if (opts.braneId && opts.braneId !== existing.brane_id) {
62
+ db.prepare("UPDATE folds SET brane_id = ? WHERE path = ?").run(opts.braneId, portable);
63
+ }
64
+ }
65
+ else {
66
+ const braneId = opts.braneId ?? getConfig("default.brane", db) ?? getBraneId(db);
67
+ db.prepare("INSERT INTO folds (path, created_at, brane_id, archived) VALUES (?, ?, ?, 0)").run(portable, Date.now(), braneId);
68
+ }
69
+ const fold = db.prepare("SELECT * FROM folds WHERE path = ?").get(portable);
70
+ return { fold, created: !existing };
71
+ }
72
+ //# sourceMappingURL=folds.js.map
@@ -0,0 +1,92 @@
1
+ // TextMate grammar for EBNF / BNF-style rule definitions.
2
+ // Targets ISO EBNF (`rule = ... ;`, `(* comment *)`, `'x'` / `"x"` terminals) and
3
+ // W3C-style EBNF used in XML/HTML specs (`Rule ::= ...`, `#x20`, `[#x0-#x7F]`).
4
+ // BNF angle-bracket non-terminals (`<name>`) are highlighted too.
5
+ export const ebnf = {
6
+ name: "ebnf",
7
+ scopeName: "source.ebnf",
8
+ displayName: "EBNF",
9
+ aliases: ["bnf"],
10
+ patterns: [
11
+ { include: "#comment" },
12
+ { include: "#rule-definition" },
13
+ { include: "#angle-nonterminal" },
14
+ { include: "#char-class" },
15
+ { include: "#string" },
16
+ { include: "#hex-number" },
17
+ { include: "#number" },
18
+ { include: "#operator" },
19
+ { include: "#identifier" },
20
+ ],
21
+ repository: {
22
+ comment: {
23
+ name: "comment.block.ebnf",
24
+ begin: "\\(\\*",
25
+ end: "\\*\\)",
26
+ },
27
+ "rule-definition": {
28
+ match: "^\\s*(<[A-Za-z_][^<>\\s]*>|[A-Za-z_][A-Za-z0-9_-]*)\\s*(::=|=)(?!=)",
29
+ captures: {
30
+ "1": { name: "entity.name.function.ebnf" },
31
+ "2": { name: "keyword.operator.definition.ebnf" },
32
+ },
33
+ },
34
+ "angle-nonterminal": {
35
+ name: "entity.name.tag.ebnf",
36
+ match: "<[A-Za-z_][A-Za-z0-9_-]*>",
37
+ },
38
+ // Regex-style character classes (`[a-zA-Z_]`, `[^']`, `[#x0-#x7F]`).
39
+ // Swallowed as a single scope so the identifier/number/operator/string
40
+ // patterns don't fire on the interior — crucial because a bare `'` or
41
+ // `"` inside `[^'…]` / `[^"…]` would otherwise open a stray string scope
42
+ // that runs away until the next matching quote anywhere on the line.
43
+ "char-class": {
44
+ name: "constant.other.character-class.ebnf",
45
+ begin: "\\[",
46
+ beginCaptures: { "0": { name: "punctuation.definition.character-class.ebnf" } },
47
+ end: "\\]",
48
+ endCaptures: { "0": { name: "punctuation.definition.character-class.ebnf" } },
49
+ patterns: [
50
+ { name: "constant.character.escape.ebnf", match: "\\\\." },
51
+ { name: "constant.numeric.hex.ebnf", match: "#x[0-9A-Fa-f]+" },
52
+ ],
53
+ },
54
+ string: {
55
+ patterns: [
56
+ {
57
+ name: "string.quoted.double.ebnf",
58
+ begin: '"',
59
+ end: '"',
60
+ patterns: [
61
+ { name: "constant.character.escape.ebnf", match: "\\\\." },
62
+ ],
63
+ },
64
+ {
65
+ name: "string.quoted.single.ebnf",
66
+ begin: "'",
67
+ end: "'",
68
+ patterns: [
69
+ { name: "constant.character.escape.ebnf", match: "\\\\." },
70
+ ],
71
+ },
72
+ ],
73
+ },
74
+ "hex-number": {
75
+ name: "constant.numeric.hex.ebnf",
76
+ match: "#x[0-9A-Fa-f]+",
77
+ },
78
+ number: {
79
+ name: "constant.numeric.decimal.ebnf",
80
+ match: "\\b[0-9]+\\b",
81
+ },
82
+ operator: {
83
+ name: "keyword.operator.ebnf",
84
+ match: "::=|[|,;.*+?()\\[\\]{}=\\-→]",
85
+ },
86
+ identifier: {
87
+ name: "entity.name.tag.ebnf",
88
+ match: "\\b[A-Za-z_][A-Za-z0-9_]*\\b",
89
+ },
90
+ },
91
+ };
92
+ //# sourceMappingURL=ebnf.js.map
@@ -0,0 +1,250 @@
1
+ import { extname } from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { createHighlighter, bundledLanguages } from "shiki";
4
+ import { ebnf as ebnfGrammar } from "./grammars/ebnf.js";
5
+ export const HIGHLIGHT_THEME = "github-dark-default";
6
+ // Custom (non-bundled) grammars shipped in-repo. Loaded eagerly at highlighter
7
+ // init so the "is this language known?" check below accepts them alongside
8
+ // shiki's bundled set.
9
+ const CUSTOM_GRAMMARS = [ebnfGrammar];
10
+ const CUSTOM_LANG_NAMES = new Set(CUSTOM_GRAMMARS.flatMap((g) => [g.name, ...(g.aliases ?? [])]));
11
+ function isKnownLang(lang) {
12
+ return lang in bundledLanguages || CUSTOM_LANG_NAMES.has(lang);
13
+ }
14
+ // File extension → shiki bundled language id, for the cases where the two
15
+ // differ (`.rs` → rust, `.py` → python). Extensions that already *are* the
16
+ // language id (`.lean`, `.zig`, `.nix`) need no entry — detectLanguage falls
17
+ // back to the bare extension and lets shiki's own registry decide.
18
+ const EXT_LANG = {
19
+ ".ts": "typescript",
20
+ ".tsx": "tsx",
21
+ ".mts": "typescript",
22
+ ".cts": "typescript",
23
+ ".js": "javascript",
24
+ ".jsx": "jsx",
25
+ ".mjs": "javascript",
26
+ ".cjs": "javascript",
27
+ ".json": "json",
28
+ ".jsonc": "jsonc",
29
+ ".sh": "bash",
30
+ ".bash": "bash",
31
+ ".zsh": "bash",
32
+ ".fish": "fish",
33
+ ".py": "python",
34
+ ".rb": "ruby",
35
+ ".go": "go",
36
+ ".rs": "rust",
37
+ ".c": "c",
38
+ ".h": "c",
39
+ ".cpp": "cpp",
40
+ ".cc": "cpp",
41
+ ".hpp": "cpp",
42
+ ".java": "java",
43
+ ".kt": "kotlin",
44
+ ".swift": "swift",
45
+ ".m": "objective-c",
46
+ ".mm": "objective-cpp",
47
+ ".cs": "csharp",
48
+ ".scala": "scala",
49
+ ".clj": "clojure",
50
+ ".ex": "elixir",
51
+ ".exs": "elixir",
52
+ ".erl": "erlang",
53
+ ".hs": "haskell",
54
+ ".lua": "lua",
55
+ ".php": "php",
56
+ ".dart": "dart",
57
+ ".vue": "vue",
58
+ ".svelte": "svelte",
59
+ ".astro": "astro",
60
+ ".css": "css",
61
+ ".scss": "scss",
62
+ ".less": "less",
63
+ ".html": "html",
64
+ ".htm": "html",
65
+ ".xml": "xml",
66
+ ".svg": "xml",
67
+ ".yaml": "yaml",
68
+ ".yml": "yaml",
69
+ ".toml": "toml",
70
+ ".ini": "ini",
71
+ ".sql": "sql",
72
+ ".graphql": "graphql",
73
+ ".gql": "graphql",
74
+ ".proto": "proto",
75
+ ".nix": "nix",
76
+ ".zig": "zig",
77
+ ".r": "r",
78
+ ".jl": "julia",
79
+ ".ebnf": "ebnf",
80
+ ".bnf": "ebnf",
81
+ };
82
+ // Extensions the *client* renders itself rather than as highlighted source —
83
+ // FilesView routes these to MarkdownContent. Shiki does bundle `md`/`mdx`
84
+ // grammars, so without this the extension fallback below would resolve them and
85
+ // we'd ship a full token payload nobody looks at.
86
+ const CLIENT_RENDERED_EXTS = new Set([".md", ".mdx", ".markdown"]);
87
+ const BASENAME_LANG = {
88
+ "Dockerfile": "docker",
89
+ "Makefile": "make",
90
+ "CMakeLists.txt": "cmake",
91
+ ".gitignore": "shellscript",
92
+ ".env": "shellscript",
93
+ };
94
+ export function detectLanguage(filePath) {
95
+ const base = filePath.split("/").pop() ?? filePath;
96
+ for (const [needle, lang] of Object.entries(BASENAME_LANG)) {
97
+ // Exact match or "needle.<suffix>" (e.g. Dockerfile.dev, .env.local) — avoids
98
+ // bare `startsWith` over-matching ".env" into ".environmental".
99
+ if (base === needle || base.startsWith(needle + "."))
100
+ return lang;
101
+ }
102
+ const ext = extname(base).toLowerCase();
103
+ if (!ext)
104
+ return null;
105
+ if (EXT_LANG[ext])
106
+ return EXT_LANG[ext];
107
+ if (CLIENT_RENDERED_EXTS.has(ext))
108
+ return null;
109
+ // Long tail: for a great many languages the extension and shiki's language id
110
+ // are the same string (lean, zig, nix, tex, diff, …). Rather than grow the map
111
+ // one bug report at a time, ask shiki whether the bare extension names a
112
+ // grammar it has. The explicit map above still wins wherever they disagree.
113
+ const bare = ext.slice(1);
114
+ if (bare && isKnownLang(bare))
115
+ return bare;
116
+ return null;
117
+ }
118
+ // Fence tags that name no shiki grammar and aren't extension-shaped either, so
119
+ // the extension fallback in resolveLang can't reach them. Kept deliberately
120
+ // short: the map below is for words people type in fences, not a second
121
+ // language registry.
122
+ const FENCE_LANG = {
123
+ golang: "go",
124
+ plist: "xml",
125
+ xaml: "xml",
126
+ patch: "diff",
127
+ };
128
+ /**
129
+ * A fenced block's info string → a grammar shiki can actually load, or null to
130
+ * render the block as plain text.
131
+ *
132
+ * The info string is whatever the author typed, and the two obvious sources of
133
+ * it disagree about vocabulary. Shiki's `bundledLanguages` is generous with
134
+ * aliases (`js`, `sh`, `yml`, `c++` all resolve) but has no SVG grammar, so
135
+ * ```svg fell through to plain text — while the *same bytes* opened from the
136
+ * Files tab highlighted fine, because that path resolves `.svg` through
137
+ * EXT_LANG instead. Rather than keep two vocabularies, a tag shiki doesn't know
138
+ * is retried as if it were a file extension, which inherits EXT_LANG whole and
139
+ * every entry added to it later.
140
+ */
141
+ export function resolveLang(lang) {
142
+ if (!lang)
143
+ return null;
144
+ const tag = lang.trim().toLowerCase();
145
+ if (!tag)
146
+ return null;
147
+ if (isKnownLang(tag))
148
+ return tag;
149
+ if (FENCE_LANG[tag])
150
+ return FENCE_LANG[tag];
151
+ return detectLanguage(`x.${tag}`);
152
+ }
153
+ let highlighterP = null;
154
+ async function getHighlighter() {
155
+ if (!highlighterP) {
156
+ highlighterP = createHighlighter({ themes: [HIGHLIGHT_THEME], langs: CUSTOM_GRAMMARS });
157
+ }
158
+ return highlighterP;
159
+ }
160
+ // Memoize per-language load promises so concurrent first-loads of the same
161
+ // language collapse to a single underlying loadLanguage call.
162
+ const langLoads = new Map();
163
+ async function ensureLangLoaded(hl, lang) {
164
+ if (hl.getLoadedLanguages().includes(lang))
165
+ return true;
166
+ let p = langLoads.get(lang);
167
+ if (!p) {
168
+ p = hl.loadLanguage(lang).then(() => undefined);
169
+ langLoads.set(lang, p);
170
+ p.catch(() => {
171
+ // Failed load — drop the memo so a later retry can try again
172
+ langLoads.delete(lang);
173
+ });
174
+ }
175
+ try {
176
+ await p;
177
+ return true;
178
+ }
179
+ catch (err) {
180
+ console.warn(`[highlight] failed to load grammar for ${lang}:`, err);
181
+ return false;
182
+ }
183
+ }
184
+ const CACHE_MAX = 200;
185
+ const cache = new Map();
186
+ function cacheGet(key) {
187
+ const v = cache.get(key);
188
+ if (v !== undefined) {
189
+ cache.delete(key);
190
+ cache.set(key, v);
191
+ }
192
+ return v;
193
+ }
194
+ function cacheSet(key, value) {
195
+ cache.set(key, value);
196
+ if (cache.size > CACHE_MAX) {
197
+ const firstKey = cache.keys().next().value;
198
+ if (firstKey !== undefined)
199
+ cache.delete(firstKey);
200
+ }
201
+ }
202
+ function compactTokens(tokens) {
203
+ return tokens.map((line) => line.map((tok) => {
204
+ const out = { c: tok.content };
205
+ if (tok.color)
206
+ out.f = tok.color;
207
+ if (tok.fontStyle && tok.fontStyle > 0)
208
+ out.s = tok.fontStyle;
209
+ return out;
210
+ }));
211
+ }
212
+ /**
213
+ * Shiki-rendered HTML for a code snippet. Returns null if the tag resolves to
214
+ * no grammar, or the grammar fails to load — caller falls back to raw text.
215
+ */
216
+ export async function highlightHtml(content, lang) {
217
+ const resolved = resolveLang(lang);
218
+ if (!resolved)
219
+ return null;
220
+ const hl = await getHighlighter();
221
+ if (!(await ensureLangLoaded(hl, resolved)))
222
+ return null;
223
+ return hl.codeToHtml(content, { lang: resolved, theme: HIGHLIGHT_THEME });
224
+ }
225
+ export async function highlight(content, lang) {
226
+ // The resolved id is what gets cached and reported back, so ```svg and
227
+ // ```xml share one cache entry rather than tokenizing the same bytes twice.
228
+ const resolved = resolveLang(lang);
229
+ if (!resolved)
230
+ return null;
231
+ const hl = await getHighlighter();
232
+ if (!(await ensureLangLoaded(hl, resolved)))
233
+ return null;
234
+ const sha = createHash("sha1").update(content).digest("hex");
235
+ const key = `${sha}:${resolved}:${HIGHLIGHT_THEME}`;
236
+ const cached = cacheGet(key);
237
+ if (cached)
238
+ return cached;
239
+ const result = hl.codeToTokens(content, { lang: resolved, theme: HIGHLIGHT_THEME });
240
+ const out = {
241
+ lang: resolved,
242
+ theme: HIGHLIGHT_THEME,
243
+ bg: result.bg ?? "#0f0f0f",
244
+ fg: result.fg ?? "#e0e0e0",
245
+ tokens: compactTokens(result.tokens),
246
+ };
247
+ cacheSet(key, out);
248
+ return out;
249
+ }
250
+ //# sourceMappingURL=highlight.js.map
package/dist/ids.js ADDED
@@ -0,0 +1,24 @@
1
+ import { randomBytes } from "node:crypto";
2
+ /**
3
+ * Generate a crypto-random string of `len` lowercase base36 characters
4
+ * (`0-9a-z`). Used for the collision-resistant suffixes of narrative ids
5
+ * (`<agent>-<8>`), brane ids (`brane-<6>`), and server-minted external ids
6
+ * (`external-<8>`).
7
+ */
8
+ export function randomBase36(len) {
9
+ const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
10
+ // Rejection-sample to avoid modulo bias: 256 is not a multiple of 36, so a
11
+ // plain `byte % 36` would map the high bytes (252-255) onto 0-3, skewing
12
+ // those four characters. Discard bytes >= 252 (the largest multiple of 36)
13
+ // and redraw, so every character is uniform over the 36-symbol alphabet.
14
+ let s = "";
15
+ while (s.length < len) {
16
+ const bytes = randomBytes(len - s.length);
17
+ for (let i = 0; i < bytes.length && s.length < len; i++) {
18
+ if (bytes[i] < 252)
19
+ s += alphabet[bytes[i] % 36];
20
+ }
21
+ }
22
+ return s;
23
+ }
24
+ //# sourceMappingURL=ids.js.map
@@ -0,0 +1,233 @@
1
+ /** What a narration is about to look like, told to the agent writing it.
2
+ *
3
+ * Every renderer in this codebase fails *quietly* by design: unparseable
4
+ * LaTeX comes back as its own source rather than as KaTeX's red error markup
5
+ * (`renderMathHtml`), a `$…$` run that trips the currency guard is escaped
6
+ * into prose (`prepareMathMarkdown`), an unclosed fence simply swallows the
7
+ * rest of the message. That is the right behaviour for the *reader*, who
8
+ * wants a document rather than a diagnostic — and exactly wrong for the
9
+ * *writer*, who never finds out that the equation they wrote did not set.
10
+ *
11
+ * This module closes that gap without reopening it for the reader: the
12
+ * rendering is unchanged, and the reasoning the renderers were already doing
13
+ * is reported back through `narrate` instead of being discarded.
14
+ *
15
+ * Two properties make the report trustworthy enough to act on:
16
+ *
17
+ * **It is the real parse, not a lookalike.** The math run is found by the
18
+ * same `prepareMathMarkdown` and rendered by the same micromark pass the
19
+ * export uses, so a warning fires if and only if the operator's view differs
20
+ * from what was intended. A second scanner written to approximate the first
21
+ * would eventually disagree with it, and a linter that cries wolf is worse
22
+ * than no linter.
23
+ *
24
+ * **Every warning carries its fallback** — the literal text that will appear
25
+ * in place of the intended rendering. Whether a failure *matters* is a
26
+ * judgment only the author can make, and it turns almost entirely on that
27
+ * text: `$E = mc^2$` shown as source still communicates; a matrix shown as
28
+ * source is line noise. Reporting the parser's complaint without the fallback
29
+ * would leave that call unmakeable.
30
+ */
31
+ import { micromark } from "micromark";
32
+ import { gfmTable, gfmTableHtml } from "micromark-extension-gfm-table";
33
+ import { gfmAutolinkLiteral, gfmAutolinkLiteralHtml } from "micromark-extension-gfm-autolink-literal";
34
+ import { gfmTaskListItem, gfmTaskListItemHtml } from "micromark-extension-gfm-task-list-item";
35
+ import { math } from "micromark-extension-math";
36
+ import { mathHtmlExtension } from "./math.js";
37
+ import { fromMarkdown } from "mdast-util-from-markdown";
38
+ import { inspectDiagram } from "./diagram/index.js";
39
+ import { mathContentRejection, prepareMathMarkdown, } from "./shared/markdown-math.js";
40
+ /** Does this rejected `$…$` run look like a failed equation rather than prose?
41
+ *
42
+ * The guard in `mathContentRejection` throws out far more currency and shell
43
+ * syntax than it does mathematics, and warning on every one of those would
44
+ * bury the real findings. A backslash command or a sub/superscript that opens
45
+ * a group is the signal that someone was writing TeX: `$\\theta$`, `$x_{i+1}$`.
46
+ * Bare `$PATH` and `$5 to $10` stay silent, which is the whole point.
47
+ */
48
+ const TEX_ISH = /\\[a-zA-Z]|[\^_]\s*[{(]/;
49
+ /** Cheap enough to run on every narration: a source with no math token at all
50
+ * never reaches micromark, and the scan that finds fence and delimiter
51
+ * problems is the same single pass `prepareMathMarkdown` already makes. */
52
+ const HAS_MATH_TOKEN = /(?:^|[^\\])\$/;
53
+ /** How many warnings one narration can produce. A message that trips more than
54
+ * this is broken in a way the first few already explain. */
55
+ const MAX_DIAGNOSTICS = 10;
56
+ export function diagnoseMarkdown(md) {
57
+ const out = [];
58
+ const issues = [];
59
+ const prepared = prepareMathMarkdown(md, (issue) => issues.push(issue));
60
+ for (const issue of issues) {
61
+ const line = lineOf(md, issue.index);
62
+ switch (issue.kind) {
63
+ case "dollar_escaped": {
64
+ const reason = mathContentRejection(issue.content, issue.after);
65
+ if (!reason || !TEX_ISH.test(issue.content))
66
+ break;
67
+ out.push({
68
+ kind: "math_demoted",
69
+ message: `\`$${clamp(issue.content, 60)}$\` looks like math, but ${reason} — so it is not treated as math and renders as literal text.`,
70
+ fallback: `$${clamp(issue.content, 120)}$`,
71
+ line,
72
+ });
73
+ break;
74
+ }
75
+ case "tex_unmatched":
76
+ out.push({
77
+ kind: "math_unclosed",
78
+ message: `${capitalize(issue.reason)} — the LaTeX renders as literal text.`,
79
+ fallback: excerptAt(md, issue.index),
80
+ line,
81
+ });
82
+ break;
83
+ case "display_unclosed":
84
+ out.push({
85
+ kind: "math_unclosed",
86
+ message: "`$$` is never closed — everything after it is swallowed into one display-math block.",
87
+ fallback: excerptAt(md, issue.index),
88
+ line,
89
+ });
90
+ break;
91
+ case "fence_unclosed":
92
+ out.push({
93
+ kind: "fence_unclosed",
94
+ message: "A code fence is never closed — everything after it renders as one code block.",
95
+ fallback: excerptAt(md, issue.index),
96
+ line,
97
+ });
98
+ break;
99
+ }
100
+ }
101
+ if (HAS_MATH_TOKEN.test(prepared)) {
102
+ // Rendered and thrown away: the HTML is the export's business, and all we
103
+ // want from this pass is which runs KaTeX refused.
104
+ micromark(prepared, {
105
+ extensions: [gfmTable(), gfmAutolinkLiteral(), gfmTaskListItem(), math()],
106
+ htmlExtensions: [
107
+ gfmTableHtml(),
108
+ gfmAutolinkLiteralHtml(),
109
+ gfmTaskListItemHtml(),
110
+ mathHtmlExtension((message, fallback) => {
111
+ out.push({
112
+ kind: "math_parse_error",
113
+ message,
114
+ fallback: clamp(fallback, 160),
115
+ // Best-effort: the LaTeX reaching KaTeX has been normalized, so it
116
+ // is not always findable in the source the author typed. A line
117
+ // that might be wrong would be worse than none, so it is omitted
118
+ // rather than guessed.
119
+ line: lineOfValue(md, fallback),
120
+ });
121
+ }),
122
+ ],
123
+ });
124
+ }
125
+ for (const d of diagnoseDiagramFences(md))
126
+ out.push(d);
127
+ return out.slice(0, MAX_DIAGNOSTICS);
128
+ }
129
+ /** ```mermaid fences that will not draw.
130
+ *
131
+ * Same contract as the math warnings: it is the *real* decision, taken by the
132
+ * same `diagnoseDiagram` the renderers call, so a warning fires iff the
133
+ * operator sees source where a diagram was meant.
134
+ *
135
+ * Which is why the fences come from **a markdown parse and not a regex**. The
136
+ * first draft scanned for the fence with a pattern, three lines under a
137
+ * comment forbidding exactly that, and it disagreed with the renderers in
138
+ * both directions inside a week: a mermaid fence quoted *inside* a longer
139
+ * ```` fence — which is how an agent documents this feature — warned about a
140
+ * diagram that was never going to draw, while a fence inside a blockquote
141
+ * renders and was never diagnosed. `fromMarkdown` sees what micromark sees,
142
+ * because it is micromark.
143
+ *
144
+ * `fallback` is the source, because that is literally what appears — the
145
+ * highlighted code block a mermaid fence renders as today. Judging whether it
146
+ * matters is the author's call and turns on reading it: a four-line flowchart
147
+ * shown as source still communicates; a thirty-line one does not.
148
+ */
149
+ function diagnoseDiagramFences(md) {
150
+ const out = [];
151
+ // Cheap gate so an ordinary narration never pays for a parse. Deliberately
152
+ // just the word: anything narrower has to model where a fence may start, and
153
+ // a `>`-prefixed one inside a blockquote is exactly what the first attempt
154
+ // at that missed — which is the bug this function was being rewritten for.
155
+ if (!/mermaid/i.test(md))
156
+ return out;
157
+ let tree;
158
+ try {
159
+ tree = fromMarkdown(md);
160
+ }
161
+ catch {
162
+ return out;
163
+ }
164
+ const walk = (node) => {
165
+ if (!node || typeof node !== "object")
166
+ return;
167
+ if (node.type === "code" && typeof node.lang === "string" &&
168
+ node.lang.toLowerCase() === "mermaid") {
169
+ const body = typeof node.value === "string" ? node.value : "";
170
+ if (body.trim()) {
171
+ const { refusal, literal } = inspectDiagram(body);
172
+ // mdast gives the fence's own line; the body starts on the next one.
173
+ const bodyLine = (node.position?.start?.line ?? 0) + 1;
174
+ // The diagram draws, but some label shows its markup as text. The
175
+ // fallback is the statement's source line, since that is where the
176
+ // characters the operator sees come from.
177
+ const bodyLines = body.split("\n");
178
+ for (const note of literal) {
179
+ out.push({
180
+ kind: "diagram_label_literal",
181
+ message: `In this \`mermaid\` diagram, ${note.message}.`,
182
+ fallback: clamp((bodyLines[note.line - 1] ?? "").trim(), 160),
183
+ line: bodyLine + note.line - 1,
184
+ });
185
+ }
186
+ if (refusal) {
187
+ out.push({
188
+ kind: refusal.kind === "unsupported" ? "diagram_unsupported" : "diagram_parse_error",
189
+ message: `This \`mermaid\` diagram will not draw: ${refusal.message}. It renders as its own source.`,
190
+ fallback: clamp(body.trim(), 280),
191
+ line: refusal.line === undefined ? bodyLine : bodyLine + refusal.line - 1,
192
+ });
193
+ }
194
+ }
195
+ return;
196
+ }
197
+ for (const child of (node.children ?? []))
198
+ walk(child);
199
+ };
200
+ walk(tree);
201
+ return out;
202
+ }
203
+ /** The line a failed run sits on, found by its own source, or undefined. */
204
+ function lineOfValue(md, fallback) {
205
+ // Delimiters and all first: the same LaTeX may appear earlier as a code
206
+ // listing, and a listing is not the run that failed.
207
+ let at = md.indexOf(fallback);
208
+ if (at === -1) {
209
+ const value = fallback.replace(/^\$\$?|\$\$?$/g, "");
210
+ at = value ? md.indexOf(value) : -1;
211
+ }
212
+ return at === -1 ? undefined : lineOf(md, at);
213
+ }
214
+ function lineOf(md, index) {
215
+ let line = 1;
216
+ for (let i = 0; i < index && i < md.length; i++)
217
+ if (md[i] === "\n")
218
+ line++;
219
+ return line;
220
+ }
221
+ /** The rest of the line starting at `index`, capped — enough to recognize the
222
+ * spot without pasting a paragraph back at the author. */
223
+ function excerptAt(md, index, max = 80) {
224
+ const end = md.indexOf("\n", index);
225
+ return clamp(md.slice(index, end === -1 ? md.length : end), max);
226
+ }
227
+ function clamp(s, max) {
228
+ return s.length > max ? s.slice(0, max - 1) + "…" : s;
229
+ }
230
+ function capitalize(s) {
231
+ return s.charAt(0).toUpperCase() + s.slice(1);
232
+ }
233
+ //# sourceMappingURL=markdown-diagnostics.js.map