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.
- package/AGENTS.md +367 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +101 -0
- package/DESIGN.md +405 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +213 -0
- package/dist/absorb/brane-id.js +11 -0
- package/dist/absorb/chatgpt.js +161 -0
- package/dist/absorb/claude-code.js +504 -0
- package/dist/absorb/index.js +543 -0
- package/dist/absorb/organelle.js +345 -0
- package/dist/absorb/run.js +520 -0
- package/dist/absorb/sources.js +168 -0
- package/dist/absorb/types.js +3 -0
- package/dist/agent-env.js +45 -0
- package/dist/agents.js +1168 -0
- package/dist/claude-live-session.js +124 -0
- package/dist/cli.js +50 -0
- package/dist/codex-tool-log.js +144 -0
- package/dist/commands/absorb.js +291 -0
- package/dist/commands/attach.js +31 -0
- package/dist/commands/certify.js +62 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/cost.js +126 -0
- package/dist/commands/daemon.js +350 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/fold.js +95 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/mcp.js +84 -0
- package/dist/commands/meld.js +360 -0
- package/dist/commands/memory.js +215 -0
- package/dist/commands/revive.js +74 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/spawn.js +1101 -0
- package/dist/commands/token.js +154 -0
- package/dist/copilot-tool-log.js +145 -0
- package/dist/cost/pricing.js +116 -0
- package/dist/cost/reprice.js +136 -0
- package/dist/crush-tool-log.js +293 -0
- package/dist/daemon/agent-scope.js +116 -0
- package/dist/daemon/awaiting.js +54 -0
- package/dist/daemon/cert-manager.js +349 -0
- package/dist/daemon/client.js +262 -0
- package/dist/daemon/gated-routes.js +112 -0
- package/dist/daemon/keep-awake.js +103 -0
- package/dist/daemon/local-socket.js +145 -0
- package/dist/daemon/presence-gate.js +206 -0
- package/dist/daemon/server.js +6353 -0
- package/dist/daemon/signal-source.js +35 -0
- package/dist/daemon/tailscale-rebind.js +58 -0
- package/dist/daemon/token-ring.js +263 -0
- package/dist/daemon/token-routes.js +190 -0
- package/dist/db/events.js +152 -0
- package/dist/db/mentions.js +179 -0
- package/dist/db/schema.js +643 -0
- package/dist/db/tokens.js +624 -0
- package/dist/db/tool-uses.js +647 -0
- package/dist/diagram/index.js +85 -0
- package/dist/diagram/label.js +455 -0
- package/dist/diagram/layout.js +476 -0
- package/dist/diagram/links.js +50 -0
- package/dist/diagram/metrics.js +152 -0
- package/dist/diagram/parse.js +487 -0
- package/dist/diagram/svg.js +389 -0
- package/dist/diagram/types.js +9 -0
- package/dist/export.js +629 -0
- package/dist/files/archive-types.js +2 -0
- package/dist/files/archive.js +182 -0
- package/dist/files/browse.js +327 -0
- package/dist/files/bytes.js +162 -0
- package/dist/files/codecs.js +33 -0
- package/dist/files/find.js +212 -0
- package/dist/files/tar.js +223 -0
- package/dist/files/zip.js +209 -0
- package/dist/folds.js +72 -0
- package/dist/grammars/ebnf.js +92 -0
- package/dist/highlight.js +250 -0
- package/dist/ids.js +24 -0
- package/dist/markdown-diagnostics.js +233 -0
- package/dist/math.js +168 -0
- package/dist/mcp/server.js +886 -0
- package/dist/memory/embedder.js +145 -0
- package/dist/memory/embedding-worker.js +71 -0
- package/dist/memory/embeddings.js +91 -0
- package/dist/memory/index.js +232 -0
- package/dist/memory/storage.js +587 -0
- package/dist/memory/wiki.js +688 -0
- package/dist/memory/wordpiece.js +247 -0
- package/dist/mentions-scan.js +40 -0
- package/dist/presence.js +259 -0
- package/dist/session-affinity.js +185 -0
- package/dist/shared/api-request.js +124 -0
- package/dist/shared/app-origin.js +26 -0
- package/dist/shared/block-glyphs.js +114 -0
- package/dist/shared/diagram-css.js +17 -0
- package/dist/shared/export-selection.js +101 -0
- package/dist/shared/find-in-text.js +110 -0
- package/dist/shared/glow-hues.js +225 -0
- package/dist/shared/hex-dump.js +62 -0
- package/dist/shared/hex-pages.js +52 -0
- package/dist/shared/hit-drum.js +79 -0
- package/dist/shared/known-nids.js +58 -0
- package/dist/shared/list-scroll-keys.js +56 -0
- package/dist/shared/markdown-lists.js +72 -0
- package/dist/shared/markdown-math.js +485 -0
- package/dist/shared/markdown-mentions.js +79 -0
- package/dist/shared/markdown-nids.js +41 -0
- package/dist/shared/markdown-protocols.js +53 -0
- package/dist/shared/markdown-refs.js +123 -0
- package/dist/shared/membrane-invasion.js +342 -0
- package/dist/shared/membrane-motion.js +83 -0
- package/dist/shared/membrane-palette.js +253 -0
- package/dist/shared/membrane-terrain.js +212 -0
- package/dist/shared/message-selection.js +123 -0
- package/dist/shared/mo-glyph.js +57 -0
- package/dist/shared/narrative-format.js +237 -0
- package/dist/shared/narrative-menu.js +84 -0
- package/dist/shared/narrative-sort.js +47 -0
- package/dist/shared/narrative-status.js +52 -0
- package/dist/shared/no-pan.js +85 -0
- package/dist/shared/pane-layout.js +50 -0
- package/dist/shared/payload-inline.js +83 -0
- package/dist/shared/speaker-color.js +61 -0
- package/dist/shared/token-rotation.js +14 -0
- package/dist/shared/view-url.js +216 -0
- package/dist/shared/wiki-links.js +70 -0
- package/dist/shared/wiki-window.js +4 -0
- package/dist/tailscale.js +55 -0
- package/dist/test/absorb-claude.test.js +892 -0
- package/dist/test/agent-scope.test.js +193 -0
- package/dist/test/agents.test.js +55 -0
- package/dist/test/api-request.test.js +122 -0
- package/dist/test/app-host-literal.test.js +53 -0
- package/dist/test/awaiting-glow.test.js +54 -0
- package/dist/test/awaiting.test.js +122 -0
- package/dist/test/bind-token.test.js +88 -0
- package/dist/test/block-glyphs.test.js +68 -0
- package/dist/test/cert-manager.test.js +262 -0
- package/dist/test/claude-live-session.test.js +110 -0
- package/dist/test/codex-tool-log.test.js +568 -0
- package/dist/test/config.test.js +71 -0
- package/dist/test/copilot-tool-log.test.js +93 -0
- package/dist/test/cors-preflight.test.js +56 -0
- package/dist/test/cost-linger.test.js +31 -0
- package/dist/test/cost-pricing.test.js +72 -0
- package/dist/test/cost-reprice.test.js +139 -0
- package/dist/test/cost-sessions.test.js +301 -0
- package/dist/test/crush-tool-log.test.js +289 -0
- package/dist/test/daemon-client.test.js +109 -0
- package/dist/test/daemon-socket.test.js +293 -0
- package/dist/test/diagram-arrows.test.js +205 -0
- package/dist/test/diagram-export.test.js +210 -0
- package/dist/test/diagram-labels.test.js +313 -0
- package/dist/test/diagram-parse.test.js +191 -0
- package/dist/test/diagram-render.test.js +175 -0
- package/dist/test/diagram-review.test.js +288 -0
- package/dist/test/diagram-self-loop-labels.test.js +303 -0
- package/dist/test/embedder.test.js +84 -0
- package/dist/test/events.test.js +123 -0
- package/dist/test/export-selection.test.js +102 -0
- package/dist/test/export.test.js +505 -0
- package/dist/test/files-browse.test.js +393 -0
- package/dist/test/files-find.test.js +272 -0
- package/dist/test/find-in-text.test.js +86 -0
- package/dist/test/first-paint-viewport.test.js +59 -0
- package/dist/test/fixtures/embedding-golden.js +23 -0
- package/dist/test/fixtures/wordpiece-golden.js +167 -0
- package/dist/test/glow-hues.test.js +202 -0
- package/dist/test/harness-cost.test.js +423 -0
- package/dist/test/helpers.js +21 -0
- package/dist/test/hex-pages.test.js +68 -0
- package/dist/test/highlight.test.js +69 -0
- package/dist/test/hit-drum.test.js +71 -0
- package/dist/test/hook-config.test.js +299 -0
- package/dist/test/json-gzip.test.js +70 -0
- package/dist/test/key-routing.test.js +58 -0
- package/dist/test/known-nids.test.js +106 -0
- package/dist/test/link-preview.test.js +67 -0
- package/dist/test/list-scroll-keys.test.js +66 -0
- package/dist/test/listen-own-words.test.js +147 -0
- package/dist/test/listen-timeout.test.js +33 -0
- package/dist/test/markdown-diagnostics.test.js +121 -0
- package/dist/test/markdown-lists.test.js +182 -0
- package/dist/test/markdown-math.test.js +256 -0
- package/dist/test/markdown-nids.test.js +146 -0
- package/dist/test/markdown-protocols.test.js +139 -0
- package/dist/test/mcp-bridge-cancel.test.js +98 -0
- package/dist/test/mcp-bridge-reconnect.test.js +121 -0
- package/dist/test/mcp-narrative.test.js +193 -0
- package/dist/test/mcp.test.js +548 -0
- package/dist/test/meld.test.js +143 -0
- package/dist/test/membrane-invasion.test.js +482 -0
- package/dist/test/membrane-motion.test.js +101 -0
- package/dist/test/membrane-palette.test.js +166 -0
- package/dist/test/membrane-terrain.test.js +170 -0
- package/dist/test/memory-log.test.js +202 -0
- package/dist/test/mentions-e2e.test.js +261 -0
- package/dist/test/mentions-ledger.test.js +265 -0
- package/dist/test/mentions-listen.test.js +265 -0
- package/dist/test/mentions.test.js +238 -0
- package/dist/test/message-selection.test.js +114 -0
- package/dist/test/mo-glyph.test.js +22 -0
- package/dist/test/model-resolution.test.js +166 -0
- package/dist/test/narrate-validate.test.js +77 -0
- package/dist/test/narrate-wait.test.js +160 -0
- package/dist/test/narrative-format.test.js +219 -0
- package/dist/test/narrative-menu.test.js +150 -0
- package/dist/test/narrative-sort.test.js +75 -0
- package/dist/test/no-pan.test.js +60 -0
- package/dist/test/pane-layout.test.js +153 -0
- package/dist/test/paths.test.js +37 -0
- package/dist/test/payload-inline.test.js +91 -0
- package/dist/test/precache-fonts.test.js +72 -0
- package/dist/test/presence-gate.test.js +440 -0
- package/dist/test/presence.test.js +160 -0
- package/dist/test/read-events.test.js +220 -0
- package/dist/test/remote-meld.test.js +128 -0
- package/dist/test/revive-cli.test.js +93 -0
- package/dist/test/search-floor.test.js +206 -0
- package/dist/test/search-fusion.test.js +168 -0
- package/dist/test/search.test.js +205 -0
- package/dist/test/security.test.js +48 -0
- package/dist/test/selection-wiring.test.js +61 -0
- package/dist/test/session-affinity.test.js +295 -0
- package/dist/test/signal-source.test.js +32 -0
- package/dist/test/socket-account.test.js +268 -0
- package/dist/test/spawn-toml.test.js +72 -0
- package/dist/test/speaker-color.test.js +90 -0
- package/dist/test/tailscale-rebind.test.js +79 -0
- package/dist/test/terminal-cursor.test.js +61 -0
- package/dist/test/terminal-resync.test.js +23 -0
- package/dist/test/terminal-size.test.js +36 -0
- package/dist/test/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +646 -0
- package/dist/test/token-lapse.test.js +94 -0
- package/dist/test/token-ring.test.js +188 -0
- package/dist/test/token-rotate-cli.test.js +58 -0
- package/dist/test/token-routes.test.js +95 -0
- package/dist/test/tool-uses.test.js +1006 -0
- package/dist/test/usage-ledger.test.js +126 -0
- package/dist/test/view-url.test.js +627 -0
- package/dist/test/wiki-links.test.js +57 -0
- package/dist/test/wiki-sources.test.js +85 -0
- package/dist/test/wiki-window.test.js +31 -0
- package/dist/test/wiki.test.js +399 -0
- package/dist/test/wordpiece.test.js +68 -0
- package/dist/test/wrangler-route.test.js +27 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +213 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +154 -0
- package/docs/README.md +3 -0
- package/docs/absorb.md +231 -0
- package/docs/cost-ledger.md +125 -0
- package/docs/daemon-api.md +189 -0
- package/docs/diagrams.md +163 -0
- package/docs/doctor.md +36 -0
- package/docs/export-scope.md +69 -0
- package/docs/files-view.md +133 -0
- package/docs/link-previews.md +38 -0
- package/docs/math.md +32 -0
- package/docs/mcp.md +69 -0
- package/docs/memory-search.md +75 -0
- package/docs/mentions.md +101 -0
- package/docs/multi-brane.md +14 -0
- package/docs/passkeys.md +165 -0
- package/docs/presence.md +202 -0
- package/docs/releasing.md +104 -0
- package/docs/rendering-warnings.md +22 -0
- package/docs/request-layer.md +72 -0
- package/docs/selection-store.md +50 -0
- package/docs/session-affinity.md +61 -0
- package/docs/terminal-mirroring.md +243 -0
- package/docs/token-rotation.md +431 -0
- package/docs/view-urls.md +134 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
- package/expo/dist/apple-touch-icon.png +0 -0
- package/expo/dist/favicon.svg +3 -0
- package/expo/dist/index.html +177 -0
- package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/expo/dist/katex/katex.min.css +1 -0
- package/expo/dist/manifest.json +16 -0
- package/expo/dist/metadata.json +1 -0
- package/expo/dist/og/membrane.v1.png +0 -0
- package/expo/dist/pdf.min.mjs +28 -0
- package/expo/dist/pdf.worker.min.mjs +28 -0
- package/expo/dist/sw.js +154 -0
- package/package.json +79 -27
- package/protocols/LIVE_loop.md +407 -0
- package/protocols/bebrief.md +41 -0
- package/protocols/digest.md +231 -0
- package/protocols/fly.md +266 -0
- package/protocols/idle.md +81 -0
- package/protocols/keeper.md +829 -0
- package/protocols/markdown.md +80 -0
- package/protocols/memory.md +419 -0
- package/protocols/mentions.md +93 -0
- package/protocols/payloads.md +53 -0
- package/protocols/prelude.md +91 -0
- package/protocols/ramify.md +105 -0
- package/protocols/review.md +356 -0
- package/protocols/rrr.md +232 -0
- package/protocols/scribe.md +429 -0
- package/protocols/summarize.md +203 -0
- package/protocols/twin.md +174 -0
- package/protocols/wiki.md +438 -0
- package/protocols/worker.md +38 -0
- package/wiki/shared/append-only-event-log.md +148 -0
- package/wiki/shared/index.md +53 -0
- package/wiki/shared/owning-your-narratives.md +161 -0
- package/wiki/shared/premature-ontologies.md +435 -0
- package/wiki/shared/sedimentary-superintelligence.md +128 -0
- package/wiki/shared/sequestering-the-narrative.md +143 -0
- package/wiki/shared/why-wikis-work.md +216 -0
- package/.travis.yml +0 -7
- package/lib/brane.cjs.js +0 -12
- package/lib/brane.cjs.js.map +0 -1
- package/lib/brane.d.ts +0 -11
- package/lib/brane.js +0 -6
- package/lib/brane.js.map +0 -1
- package/lib/tests.d.ts +0 -1
- package/lib/tests.js +0 -8
- package/lib/tests.js.map +0 -1
- package/rollup.config.js +0 -21
- package/scripts/test.sh +0 -8
- package/src/brane.ts +0 -21
- package/src/tests.ts +0 -8
- package/tsconfig.json +0 -16
package/docs/diagrams.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Diagram Rendering
|
|
2
|
+
|
|
3
|
+
A ` ```mermaid ` fence draws. Like code highlighting and unlike KaTeX it renders
|
|
4
|
+
**on the daemon** — `POST /api/diagram` for the app, a direct call for the HTML
|
|
5
|
+
export — because a diagram is rare and heavy where inline math is ubiquitous and
|
|
6
|
+
light. The Expo bundle gains nothing: `@dagrejs/dagre` (1.9 MB, 2 packages) is
|
|
7
|
+
the only new dependency and it never reaches the browser.
|
|
8
|
+
|
|
9
|
+
| Piece | Where | Does |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `parseDiagram` | `src/diagram/parse.ts` | Source → IR, or a refusal with a reason and a line. Linear time — see below |
|
|
12
|
+
| `parseLabel` | `src/diagram/label.ts` | Label text → lines of styled runs, for both of mermaid's label grammars — see below |
|
|
13
|
+
| `measureNode` | `src/diagram/metrics.ts` | Text width without a DOM — Helvetica and Helvetica-Bold advances, per-block fallbacks |
|
|
14
|
+
| `layoutDiagram` | `src/diagram/layout.ts` | dagre, plus **face-projected attachment** (below), resolved `classDef`s, and synthesised self-loops |
|
|
15
|
+
| `basisPath` | `src/diagram/svg.ts` | `d3-shape`'s `curveBasis`, ported; pinned against d3's own output |
|
|
16
|
+
| `nidHrefFor` | `src/diagram/links.ts` | The seam: a node labelled with a real narrative becomes a link. Takes the target form, so the export points at sibling `<nid>.html` files rather than the app's `brane://` scheme |
|
|
17
|
+
| `renderDiagram` | `src/diagram/index.ts` | The one entry point, for both targets |
|
|
18
|
+
| `edgeHighlight` / `DIAGRAM_EDGE_HIGHLIGHT_CSS` | `src/diagram/svg.ts`, `src/shared/diagram-css.ts` | Hover or tap an arrow, or its label, to draw it again in a highlight color, above every other edge and label and below the boxes, which cover its ends. CSS only, so the export stays scriptless; the export inlines the rule and `DiagramView` injects the same string. Label groups paint above line groups, so a later arrow's hit stroke cannot cover an earlier arrow's label |
|
|
19
|
+
|
|
20
|
+
**The dialect is measured, not chosen.** Across every mermaid fence in the
|
|
21
|
+
archive (21 diagrams, 316 lines, home brane plus two absorbed ones): `flowchart`
|
|
22
|
+
and `graph` only, `-->` 131 and `<-->` 9, `[rect]` 125 · `(round)` 9 ·
|
|
23
|
+
`[(cyl)]` 5 · `{diamond}` 4 · `>asym]` 4 · `[[sub]]` 1, plus `subgraph`, `&`,
|
|
24
|
+
`<br/>`, `classDef`/`class`/`style` and `%%`. So that is what draws, and 20 of
|
|
25
|
+
the 21 do; the refusal is the one `stateDiagram-v2`, a later increment. Bump the
|
|
26
|
+
census when the dialect grows. Styled label text is the one part of the
|
|
27
|
+
dialect admitted without a count: on 2026-10-01 this daemon's 26 fences held
|
|
28
|
+
no markdown string, `<b>` or `font-weight` at all, but an agent on another
|
|
29
|
+
daemon that wanted bold tried all three in a row and got literal tags, then
|
|
30
|
+
literal backticks and asterisks, then nothing, with a clean receipt each time
|
|
31
|
+
(claude-cdouqlgt:12–:24). Absence from the census was the quiet failure, not
|
|
32
|
+
disuse.
|
|
33
|
+
|
|
34
|
+
Eight things are less obvious than they look:
|
|
35
|
+
|
|
36
|
+
**The dialect adds no syntax.** Every brane-specific behaviour is
|
|
37
|
+
*interpretation* of text that is already legal mermaid — a node labelled
|
|
38
|
+
`claude-0qjv4nny` is a link here and plain text on GitHub. So the same fence
|
|
39
|
+
stays portable, and there is no second grammar to keep in step. Only a label
|
|
40
|
+
that is *entirely* a reference resolves, and only to a narrative that exists,
|
|
41
|
+
which is the rule `markdown-nids.ts` already applies in prose. A node **cites
|
|
42
|
+
and does not address**: `@nid` is deliberately not linked, because
|
|
43
|
+
`mentions-scan` walks the rendered markdown where a fence is inert, so linking
|
|
44
|
+
it would show a mention that notifies nobody.
|
|
45
|
+
|
|
46
|
+
**Arrows arrive crooked because of the attachment point, not the curve.** dagre
|
|
47
|
+
picks an edge's terminal point by intersecting the node's box with the chord
|
|
48
|
+
from the node's *centre* to the first bend; that chord is diagonal, so the tilt
|
|
49
|
+
is baked in before any curve is fitted — 28px of lean on the LIVE loop's
|
|
50
|
+
`Listen → Interpret`. `projectAttachment` slides the point along its face until
|
|
51
|
+
it lines up under the bend, which drops mean arrival angle from 31.4° to 8.7°
|
|
52
|
+
**with the curve untouched**. Four curve-side fixes were built and measured
|
|
53
|
+
first (clamped-tangent Hermite, basis + normal stubs, basis + straight tail,
|
|
54
|
+
rotating the arrowhead alone); every one bought squareness by adding
|
|
55
|
+
inflections, and inflections are what read as busy. `curveBasis` stays exactly
|
|
56
|
+
what mermaid uses. `faceOffset` generalises the projection to diamonds, circles
|
|
57
|
+
and hexagons, whose boundary is not a box.
|
|
58
|
+
|
|
59
|
+
**A refusal is a normal answer, and needs no new failure path.** `renderDiagram`
|
|
60
|
+
returns `null`, and both call sites already handled that: `export.ts`'s fence
|
|
61
|
+
loop substitutes only non-null results, and the app's `DiagramView` falls back
|
|
62
|
+
to `HighlightedCode`. So an undrawn diagram is exactly the highlighted code
|
|
63
|
+
block a mermaid fence rendered as before this existed. `diagnoseDiagram` reports
|
|
64
|
+
*why* through `narrate`'s `warnings` (`diagram_unsupported`,
|
|
65
|
+
`diagram_parse_error`) — the same real decision the renderer takes, over fences
|
|
66
|
+
found by `fromMarkdown` rather than by a pattern. The first draft scanned for
|
|
67
|
+
them with a regex three lines under a comment forbidding exactly that, and it
|
|
68
|
+
disagreed with the renderers in both directions: a mermaid fence quoted inside
|
|
69
|
+
a longer ```` fence warned about a diagram that was never going to draw, and
|
|
70
|
+
one inside a blockquote drew but was never diagnosed.
|
|
71
|
+
|
|
72
|
+
**Nothing from a fence reaches the output as markup.** Labels are escaped;
|
|
73
|
+
`<br/>`, `<b>` and markdown strings are parsed into styled runs, which become
|
|
74
|
+
`<tspan>`s whose attributes `svg.ts` writes itself; `style` declarations are
|
|
75
|
+
filtered to seven properties with a value charset that excludes `url(`, and
|
|
76
|
+
`font-weight`/`font-style` to their keywords.
|
|
77
|
+
There is deliberately **no `<style>` element** — an inline SVG's `<style>` is
|
|
78
|
+
*document*-global, so one diagram's `text { font-family }` would restyle every
|
|
79
|
+
other diagram on the page. Theming is CSS custom properties
|
|
80
|
+
(`--brane-diagram-*`) **whose fallbacks are brane's dark palette**, and marker
|
|
81
|
+
ids are hashed per source so two diagrams on a page cannot share an arrowhead.
|
|
82
|
+
The fallbacks matter more than the properties: nothing defines the latter, and
|
|
83
|
+
the first draft shipped light-mode values that drew `#33373d` lines on the
|
|
84
|
+
app's `#121214` and the export's `#000`.
|
|
85
|
+
|
|
86
|
+
**Labels have two grammars, and only one of them is markdown.** In a plain
|
|
87
|
+
label (`A[text]`, `A["text"]`) mermaid honours `<br/>`, `<b>`, `<i>`,
|
|
88
|
+
`<code>`, `<u>`, `<s>` and their kin, and `**x**` is two asterisks, on GitHub
|
|
89
|
+
too. A **markdown string** (``A["`text`"]``) is the other: the backticks just
|
|
90
|
+
inside the quotes are mermaid's marker, not over-quoting, and inside one
|
|
91
|
+
`**bold**` and `*italic*` render, a newline is a line break, and long lines
|
|
92
|
+
wrap. `label.ts` parses each into runs (`{ text, bold?, italic?, code?,
|
|
93
|
+
strike?, underline? }`) that the IR carries beside the plain `lines` links and
|
|
94
|
+
search still read. A markdown string goes through `fromMarkdown`, so "bold" means
|
|
95
|
+
here what it means in prose. It can span source lines, so the parser joins a
|
|
96
|
+
statement until every ``"` `` it opened is closed, held to `MAX_LINE_LENGTH`
|
|
97
|
+
like a line. Wrapping is mermaid's 200px at its 16px, scaled to brane's 14px:
|
|
98
|
+
the em width is what decides where a line breaks, and an unscaled 200px kept
|
|
99
|
+
together words that GitHub splits. Bold is measured with Helvetica-Bold's own
|
|
100
|
+
advances, so a bold head gets the box it needs. `font-weight` and
|
|
101
|
+
`font-style` from `style` or `classDef` are folded into the runs in layout, so
|
|
102
|
+
what is measured is what is drawn. Everything that still shows as typed (`**`
|
|
103
|
+
in a plain label, an unknown tag, a link in a markdown string) comes back from
|
|
104
|
+
the same parse as a `diagram_label_literal` warning that says how to style it.
|
|
105
|
+
The diagram still draws. It is a note, not a refusal.
|
|
106
|
+
|
|
107
|
+
A node's markdown string may also hold a **GFM table**, which mermaid does not
|
|
108
|
+
draw at all ("Unsupported markdown: table"). Brane draws it, and that is
|
|
109
|
+
allowed for two reasons. The fence is still valid mermaid, so GitHub shows the
|
|
110
|
+
fallback text and nothing breaks. And the drawing is static SVG made on the
|
|
111
|
+
daemon, so the HTML export carries no rendering logic for it (the condition
|
|
112
|
+
the operator set, claude-uwaj0umf:33). A table is a `TableBlock` among the
|
|
113
|
+
label's lines and takes `1 + rows` line slots. Its columns share widths, which
|
|
114
|
+
is the one thing a stack of centred `<tspan>`s cannot express, so a node
|
|
115
|
+
holding one is drawn as a `<text>` per line or cell on the same baselines the
|
|
116
|
+
stack would have used. Header cells are bolded at parse time, so measurement
|
|
117
|
+
and drawing agree without either knowing which row is the header. Edge labels
|
|
118
|
+
and subgraph titles have no box for a grid, so there a table shows as source,
|
|
119
|
+
with a warning. Plain labels also decode character references (`<`,
|
|
120
|
+
`→`, `→`) after tags are scanned, as mermaid's HTML labels do, so
|
|
121
|
+
a decoded `<` is never read as a tag.
|
|
122
|
+
|
|
123
|
+
**The parser runs on the `narrate` path, so its cost is a security property.**
|
|
124
|
+
`diagnoseMarkdown` calls it for every narration containing a mermaid fence, no
|
|
125
|
+
route and no operator involved. The first draft's inline-label patterns were
|
|
126
|
+
`-{2,}\s+(.+?)\s+-{2,}>` — three overlapping quantifiers over whitespace,
|
|
127
|
+
cubic in the run length, 1.5 s on a 2,000-space line and 8× per doubling, with
|
|
128
|
+
`MAX_SOURCE_BYTES` no defence because it bounds *n* while the cost is n³. Label
|
|
129
|
+
groups now anchor both ends on a non-space and exclude their own link's
|
|
130
|
+
punctuation, there is a `MAX_LINE_LENGTH`, and `diagram-review.test.ts` asserts
|
|
131
|
+
the complexity rather than the timing. Treat any future regex here the same
|
|
132
|
+
way.
|
|
133
|
+
|
|
134
|
+
**A wrong diagram is worse than no diagram.** Three of the review findings on
|
|
135
|
+
the original PR were *silent*: `A -.-> B -.-> C` drew one edge and dropped a
|
|
136
|
+
node, `A --- B --> C` likewise, and an edge into a subgraph id threw inside
|
|
137
|
+
dagre, was caught, and fell back with no warning at all — breaking the "warns
|
|
138
|
+
iff the operator sees source" rule below. The parser now refuses each with a
|
|
139
|
+
reason. When in doubt, refuse: the reader gets source and the author gets told.
|
|
140
|
+
|
|
141
|
+
**Self-loops are never given to dagre, and bow off a face no rank edge uses.**
|
|
142
|
+
Dagre returns a degenerate path for `A --> A` — duplicated points that double
|
|
143
|
+
back and start outside the node — so a loop is synthesised as an arc, with its
|
|
144
|
+
label past the apex. It leaves a *cross-axis* face (right in TD/BT, bottom in
|
|
145
|
+
LR/RL): dagre gives every edge `minlen` 1, so edges arrive only on the
|
|
146
|
+
rank-facing sides, and a loop there put its label exactly where they cross —
|
|
147
|
+
on the Rule 30 board, 101's label read as the 110→101 edge's. Dagre is told
|
|
148
|
+
about the loop by a box grown on the cross axis (`loopBox`), which keeps
|
|
149
|
+
neighbours clear: a TD/BT diagram's height (the chat bubble's) drops against
|
|
150
|
+
#504's layout, and an LR/RL one's grows, since its loops now hang below the
|
|
151
|
+
node — the price of labels that no longer sit on rank edges. Edges on a
|
|
152
|
+
loop-bearing node are pinned to its rank faces, and run straight along the
|
|
153
|
+
rank axis from dagre's widened box to the drawn face (`rankKnot`), so a steep
|
|
154
|
+
edge's last leg can't cut the corner of a chip wider than the node. A bow's
|
|
155
|
+
drawn apex is 23/24 of its control depth; label placement depends on it. Stacked loops step outward by
|
|
156
|
+
each chip's extent along the bow, and an outer bow flares past every chip
|
|
157
|
+
inside it. `diagram-self-loop-labels.test.ts` holds each chip against every
|
|
158
|
+
other edge's drawn *path*, not just other chips — the check the first
|
|
159
|
+
version lacked. Parallel and antiparallel edges, by contrast, dagre separates
|
|
160
|
+
correctly on its own.
|
|
161
|
+
|
|
162
|
+
Native renders the code block: there is no DOM to put an SVG into, and the
|
|
163
|
+
honest options are their own change.
|
package/docs/doctor.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Doctor
|
|
2
|
+
|
|
3
|
+
`brane doctor` answers "is this install sound?" on the machine it landed on.
|
|
4
|
+
It prints a short table of environment checks and then runs the **shipped
|
|
5
|
+
test suite** — `dist/test/*.test.js` ships in the tarball on purpose — under
|
|
6
|
+
the same `node` that runs the CLI, streaming the reporter and exiting with
|
|
7
|
+
its status. The suite needs no daemon, tmux, network, or model download: the
|
|
8
|
+
search tests inject a stub embedder and the wiki/fold tests use temp dirs and
|
|
9
|
+
in-memory SQLite, so a red result is the install, not the environment.
|
|
10
|
+
|
|
11
|
+
No test may open the operator's database. `openDatabase()` throws when
|
|
12
|
+
`NODE_TEST_CONTEXT` is set (which `node --test` does in every test process),
|
|
13
|
+
so a code path that reaches it without a `db` fails its test instead of
|
|
14
|
+
opening `~/.brane/brane.db` and running migrations on it — from a checkout or
|
|
15
|
+
from `doctor` on an installed machine. Code the suite drives takes a `db`
|
|
16
|
+
(`getConfig(key, db)`); tests build theirs with `createTestDb()`.
|
|
17
|
+
|
|
18
|
+
Two tests read *source* rather than import code (`api-request.test.ts` lints
|
|
19
|
+
`expo/lib/api.ts`; `key-routing.test.ts` lints two `.tsx` files). The source
|
|
20
|
+
isn't in the tarball, so they `skip` with a reason when it's absent — green by
|
|
21
|
+
construction from an install, still enforced from a checkout. A source-reading
|
|
22
|
+
test added later must do the same, or `doctor` goes red on every install.
|
|
23
|
+
|
|
24
|
+
The daemon check asks two questions: is one running (and from which
|
|
25
|
+
checkout), and can *this shell* reach it. Outside an agent session the CLI
|
|
26
|
+
holds no token and reaches the daemon only over `~/.brane/daemon.sock`, so
|
|
27
|
+
the check fails, with the reason, when the daemon is not listening there or
|
|
28
|
+
`~/.brane` is not `0700` and its own (`shellReachProblem()`). A `– token
|
|
29
|
+
file` line appears when `~/.brane/.daemon_token` is still on disk: older
|
|
30
|
+
branes kept this machine's local token there, the daemon empties it at its
|
|
31
|
+
first start, and removing the leftover is the operator's to do, with the
|
|
32
|
+
`rm` line printed (`leftoverTokenFile()`; see [No local
|
|
33
|
+
token](token-rotation.md#no-local-token)).
|
|
34
|
+
|
|
35
|
+
`--no-suite` runs only the environment checks; `--suite-only` runs only the
|
|
36
|
+
tests (`brane doctor --suite-only` is the one-liner for a bug report).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Export Scope
|
|
2
|
+
|
|
3
|
+
An export is the whole conversation, the messages the operator picked, or
|
|
4
|
+
everything but them. The picking is not new: the chat has kept a selection
|
|
5
|
+
since `re` chips existed — tap a bubble's timestamp and its seq joins
|
|
6
|
+
`selectedSeqs` (`expo/app/[nid].tsx`), which is what the composer attaches
|
|
7
|
+
to the next signal as `re`. Export reads the same set, as *"these ones"* or
|
|
8
|
+
as *"all but these"*.
|
|
9
|
+
|
|
10
|
+
| Piece | Where | Does |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `MessageSelection` | `src/shared/message-selection.ts` | The selection itself, with per-seq subscriptions — see [The Selection Store](selection-store.md) |
|
|
13
|
+
| `formatSeqParam` / `parseSeqParam` | `src/shared/export-selection.ts` | The `seqs=12,14,19` grammar, both halves in one module; `src/test/export-selection.test.ts` |
|
|
14
|
+
| `SeqSelectionMode` | `src/shared/export-selection.ts` | Which side of it: `only` (the default) or `except`, on the wire as `seqs_mode` |
|
|
15
|
+
| `selectedCount` / `ExportScope` | `src/shared/narrative-menu.ts` | Splits the 🦠 item into **📨 Export 2 selected** and **📤 Export 2 excluded**, and tells the press which one — the list card has no selection and passes nothing |
|
|
16
|
+
| `ExportOptions.seqs` / `.seqsMode` | `src/export.ts` | Keeps (or drops) those events, counts the gaps, writes the disclosure |
|
|
17
|
+
|
|
18
|
+
Seven things are less obvious than they look:
|
|
19
|
+
|
|
20
|
+
**An excerpt says it is one, in the line that used to say how many.**
|
|
21
|
+
`Exported … · 3 of 47 messages` is the whole disclosure, and it is
|
|
22
|
+
load-bearing: the reader of an exported file is generally not the operator
|
|
23
|
+
who made it, and a 3-message document that does not mention the other 44
|
|
24
|
+
reads as a short conversation rather than a quotation from a long one.
|
|
25
|
+
|
|
26
|
+
**A gap between two shown messages is marked; a gap before or after them is
|
|
27
|
+
not.** `⋯ 4 messages not shown` sits between non-adjacent picks, because
|
|
28
|
+
two bubbles stacked in a transcript otherwise assert adjacency — the one
|
|
29
|
+
claim an excerpt must never make by accident. What was dropped from the
|
|
30
|
+
ends is already covered by the header's count, and marking it would put an
|
|
31
|
+
elision row at the top of nearly every excerpt.
|
|
32
|
+
|
|
33
|
+
**Anchors stay `#seq-N`.** Rows are not renumbered, so `nid:seq` citations
|
|
34
|
+
into an excerpt mean what they mean everywhere else, and a full export and
|
|
35
|
+
an excerpt of the same narrative agree on every link. It is also why the
|
|
36
|
+
selection travels as seqs rather than as an index range.
|
|
37
|
+
|
|
38
|
+
**An unreadable `seqs` exports everything rather than failing.** An export
|
|
39
|
+
is a read; a 400 would cost the operator their transcript over a typo in a
|
|
40
|
+
URL they probably did not type. Empty, absent, and junk all mean "no
|
|
41
|
+
selection", and `{ seqs: [] }` is byte-identical to the export brane made
|
|
42
|
+
before this parameter existed — pinned by `src/test/export.test.ts`.
|
|
43
|
+
|
|
44
|
+
**The mode's tolerance runs one way.** Only the exact word `except`
|
|
45
|
+
inverts an export; `Except`, `exclude`, `1` and junk all read as `only`.
|
|
46
|
+
The asymmetry is the point: a garbled `only` hands the operator a document
|
|
47
|
+
whose header already says how much of the conversation it holds, while a
|
|
48
|
+
garbled `except` would hand them the *complement* of what they asked for —
|
|
49
|
+
and a selection is as often "the messages nobody else should read" as it is
|
|
50
|
+
"the interesting ones". For the same reason the parameter is written only
|
|
51
|
+
when it changes something, so an ordinary excerpt's URL is unchanged.
|
|
52
|
+
|
|
53
|
+
**An excerpt's file name carries its count; a redaction leaves the summary
|
|
54
|
+
out.** The 💾 button saves `<nid>.html` for a full export and
|
|
55
|
+
`<nid>.3-of-47.<digest>.html` for either scope (`exportFileName`): the count
|
|
56
|
+
the header discloses, plus six hex digits naming exactly which seqs it holds.
|
|
57
|
+
The count alone gave `only` and `except` of a half-selection one name, and
|
|
58
|
+
they are the pair a redaction exists to keep apart. Like the header, the name
|
|
59
|
+
doesn't say which side of the line was taken. An `except` export drops
|
|
60
|
+
the narrative's summary, which is agent-written over the whole conversation
|
|
61
|
+
and can paraphrase exactly what was left out.
|
|
62
|
+
|
|
63
|
+
**The complement reads the conversation; the excerpt does not.** `seqs IN
|
|
64
|
+
(…)` is an index lookup per selected message, which is why exporting two
|
|
65
|
+
messages out of ten thousand costs two rows. Excluding two of ten thousand
|
|
66
|
+
exports 9,998 of them, so it reads the log and drops the picks — a full
|
|
67
|
+
export's cost, which is what it is. Both scopes then disclose identically:
|
|
68
|
+
`3 of 47` and `44 of 47` are both excerpts, and neither says which side of
|
|
69
|
+
the line it took.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Files View
|
|
2
|
+
|
|
3
|
+
How the daemon serves a narrative's fold to the Files tab, what it can show, and how 🔎 searches a binary.
|
|
4
|
+
|
|
5
|
+
## Files API
|
|
6
|
+
|
|
7
|
+
`GET /api/files?nid=<nid>&path=<rel>` browses the filesystem under a narrative's `fold_path`. The route is `browseFold()` (`src/files/browse.ts`), a pure function of (fold, path, options) so the suite drives it without HTTP. The response is discriminated on `type`:
|
|
8
|
+
|
|
9
|
+
- `{ type: "directory", path, entries: [{ name, is_dir, size, mtime, archive?, link? }], archive? }` — dotfiles hidden
|
|
10
|
+
- `{ type: "file", path, size, mtime, content, truncated?, binary?, highlight?, hex?, link?, notice?, archive? }`
|
|
11
|
+
|
|
12
|
+
`highlight` is a pre-tokenized Shiki payload (`{ lang, theme, bg, fg, tokens }`) so the client renders colored code without shipping a highlighter to the browser. Adding `&raw=1` streams the bytes instead of JSON, for any file: the `src` for images and PDFs, and **Download** for a file too large to show (which, before, got JSON back). `&offset=N` asks for the hex page at byte N of a binary file. A path may run through an archive (`dist/app.zip/src/main.ts`); what that means is [Files View Formats](#files-view-formats).
|
|
13
|
+
|
|
14
|
+
Path handling is traversal-confined: absolute paths are rejected, `#`/`?` fragments are stripped, the target is `resolve()`d and must equal the fold root or sit beneath it. The echoed `path` is normalized so breadcrumbs never render `fold/./blah`.
|
|
15
|
+
|
|
16
|
+
`GET /api/files/list?nid=<nid>` returns `{ files, truncated }` — a flat path list for the whole fold, feeding the client-side fuzzy finder. It does not descend into archives: that would mean opening every one in the fold to answer a keystroke.
|
|
17
|
+
|
|
18
|
+
## Files View Formats
|
|
19
|
+
|
|
20
|
+
What the Files view can show is one of brane's quiet advantages, so it is
|
|
21
|
+
kept as a census, and a format that renders is pinned by a test that builds
|
|
22
|
+
it byte by byte (`src/test/files-browse.test.ts`, which needs no `zip` or
|
|
23
|
+
`tar` binary, so it runs under `brane doctor` too).
|
|
24
|
+
|
|
25
|
+
| File | Shown as | By |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| Source and text | Shiki-highlighted code with line anchors | `CodeView` |
|
|
28
|
+
| Markdown (`.md`, `.mdx`, and `.md.gz`) | Rendered, math and diagrams included | `MarkdownContent` |
|
|
29
|
+
| Images, PDFs | Inline image; pdf.js pages | `<img>`, `PdfView` |
|
|
30
|
+
| **Anything else** | Hex, `xxd`-style, 16 KB a page, 16 bytes a row (8 on a phone), searchable to the last byte | `HexView` |
|
|
31
|
+
| `.zip` and its kin (`.jar` `.war` `.aar` `.apk` `.ipa` `.whl` `.epub` `.docx` `.xlsx` `.pptx` `.odt` `.nupkg` `.vsix` `.xpi` …) | A folder | `src/files/zip.ts` |
|
|
32
|
+
| `.tar`, `.tar.gz`/`.tgz`, `.tar.br`, `.tar.zst` (Node ≥ 22.15) | A folder | `src/files/tar.ts` |
|
|
33
|
+
| `.gz`, `.br`, `.zst` | What they compress, named by what's inside | `archiveFormatFor` |
|
|
34
|
+
|
|
35
|
+
**PDFs use pdf.js's legacy build.** `PdfView` imports `public/pdf.min.mjs`
|
|
36
|
+
and its worker at runtime, and both come from `pdfjs-dist/legacy/build/`
|
|
37
|
+
(`npm run sync:pdfjs` in `expo/`, output committed). The modern `build/`
|
|
38
|
+
calls `Map.prototype.getOrInsertComputed` with no fallback, so on a browser
|
|
39
|
+
without it the first `page.render` throws and every page stays blank. The
|
|
40
|
+
legacy build carries the core-js polyfills, worker included, for about 60 KB.
|
|
41
|
+
|
|
42
|
+
**Hex is the floor.** A file is binary when a NUL appears in its first 8000
|
|
43
|
+
bytes, git's rule, and it pages rather than truncating, so a 50 MB `.so` is
|
|
44
|
+
browsable where a 3 MB log still gets "too large". Before, anything that was
|
|
45
|
+
not an image or PDF was decoded as UTF-8 and shown as noise. Pages sit where
|
|
46
|
+
they belong in the file, in whatever order they were loaded, with a gap row
|
|
47
|
+
for each run between them (`⋯ 1,228,800 bytes not loaded`, and ↓ / ↑ to
|
|
48
|
+
load the 16 KB on either side); `hexSegments` in `src/shared/hex-pages.ts`
|
|
49
|
+
lays that out. The pages live in a `HexDoc` (`expo/lib/hexDoc.ts`) rather
|
|
50
|
+
than the view's state, because 🔎 loads them too.
|
|
51
|
+
|
|
52
|
+
## Searching the bytes
|
|
53
|
+
|
|
54
|
+
🔎 on a hex file is the daemon's, not the DOM's. It used to search the text
|
|
55
|
+
of the pages already loaded, which missed three ways: nothing on an unloaded
|
|
56
|
+
page existed, a string that wrapped a row was split by the offset and hex
|
|
57
|
+
columns between its halves, and `cffaedfe` never matched a column that reads
|
|
58
|
+
`cf fa ed fe`. `GET /api/files/find` (`src/files/find.ts`) reads the bytes
|
|
59
|
+
themselves, through the `ByteSource` the hex page reads, so an archive member
|
|
60
|
+
or a `.gz` body is searched as it is shown.
|
|
61
|
+
|
|
62
|
+
A query is matched as its UTF-8 text, smart-case like 🔎 everywhere else,
|
|
63
|
+
and, when it is a run of hex pairs (`cf fa ed fe`, `cffaedfe`, `0xCAFE`), as
|
|
64
|
+
those bytes as well: `cafe` finds the word and 0xCA 0xFE both. A hit is
|
|
65
|
+
painted by byte range in the hex column and the gutter alike
|
|
66
|
+
(`expo/lib/hexFind.ts`), one range per row it spans.
|
|
67
|
+
|
|
68
|
+
**What a search returns is bounded by pages, not by the file.** Hits come
|
|
69
|
+
from at most the first 64 hex pages that have any (1 MB, as much as the view
|
|
70
|
+
will hold), and at most 5,000 of them. A one-byte query in a 2 GB binary
|
|
71
|
+
therefore stops after a megabyte of hits instead of reading two gigabytes
|
|
72
|
+
for hits nobody will visit; the status line pinned above the rows says so
|
|
73
|
+
and offers **Search on from 0x…**, the next window. The drum rolls over
|
|
74
|
+
every hit in the window and **fetches nothing while it rolls**: the page
|
|
75
|
+
under a hit loads when the drum comes to rest on it, and until then the
|
|
76
|
+
hit's gap row stands where it will be. Measured in headless Chromium: a
|
|
77
|
+
30-hit roll across a 40 MB file made no page requests while rolling and one
|
|
78
|
+
at rest.
|
|
79
|
+
|
|
80
|
+
The other bounds are against accidents: a token holder can already type into
|
|
81
|
+
every tmux session.
|
|
82
|
+
|
|
83
|
+
| Bound | Why |
|
|
84
|
+
|---|---|
|
|
85
|
+
| One pass per request (`ByteSource.chunks`), never re-reading a compressed prefix | Paged requests would re-inflate a `.gz` from its start each time |
|
|
86
|
+
| Stops at the reach limit, and says so (`stop: "reach"`) | `chunks` throws `ReachError` where `stream()` would end quietly |
|
|
87
|
+
| 30 s of scanning, then `stop: "time"` and a `next` to resume from | A slow disk or a vast file doesn't hold a request open indefinitely |
|
|
88
|
+
| At most 2 scans daemon-wide; a third supersedes the oldest (`FindSlots`) | The likeliest third is the same reader's next keystroke |
|
|
89
|
+
| The app debounces 200 ms and aborts the previous request, and a closed request stops the read | Typing `NEEDLE` is one scan, not six |
|
|
90
|
+
| Needles of at most 256 bytes | A needle is held in every chunk's carry, and nothing a reader types is longer |
|
|
91
|
+
|
|
92
|
+
Hits are reported in offset order across chunk seams, two needles included:
|
|
93
|
+
a hit is reported only once every needle that could start before it has been
|
|
94
|
+
seen whole. That is what makes the page count a running count and `next` a
|
|
95
|
+
resume point that skips nothing, and `src/test/files-find.test.ts` holds it
|
|
96
|
+
against a naive search over 200 randomized splits.
|
|
97
|
+
|
|
98
|
+
**An archive is a folder, and nothing is extracted.** A `ByteSource`
|
|
99
|
+
(`src/files/bytes.ts`) is anything the view can open: a file, an archive
|
|
100
|
+
member, a `.gz` body. Every renderer reads through `respondWithSource`, so a
|
|
101
|
+
member gets highlighting, images, hex and download for free. A zip is listed
|
|
102
|
+
from its central directory (one read, nothing inflated) and a member inflates
|
|
103
|
+
alone; a tar is one pass over its headers with the data skipped, cached per
|
|
104
|
+
(path, size, mtime) because a compressed tar has no index. Archives do not
|
|
105
|
+
nest: a zip inside a zip opens as hex.
|
|
106
|
+
|
|
107
|
+
The usual archive vulnerabilities come from *extracting*, and there is no
|
|
108
|
+
extraction here:
|
|
109
|
+
|
|
110
|
+
- **Zip-slip, symlink and hard-link entries, overwrites, setuid bits:** not
|
|
111
|
+
applicable, since nothing is written. Member names are only displayed.
|
|
112
|
+
`normalizeMemberName` drops leading slashes and hides any name with `..`
|
|
113
|
+
or a NUL, counting it (`archive.skipped`, shown as a banner) rather than
|
|
114
|
+
drawing it somewhere it doesn't belong. Links are shown with their target
|
|
115
|
+
and never followed; a tar hard link resolves once, to an earlier member of
|
|
116
|
+
the same archive.
|
|
117
|
+
- **Decompression bombs:** a read decodes only as far as it asked for and
|
|
118
|
+
tears the pipeline down (`sliceStream`), so output is bounded by the
|
|
119
|
+
request, never by a size the archive claims. Opening a 64 MB-of-zeros
|
|
120
|
+
member decodes one 16 KB page. No read reaches past 1 GB into a
|
|
121
|
+
compressed stream (`MAX_STREAM_REACH`), which bounds the CPU any one
|
|
122
|
+
request can spend, and a tar listing stops at 1 GB decoded or 200,000
|
|
123
|
+
members and says it is partial.
|
|
124
|
+
- **Parser bugs:** the readers are TypeScript plus Node's own zlib. No
|
|
125
|
+
archive tool is shelled out to: `unzip` and `bsdtar` are C with CVE
|
|
126
|
+
histories, and bsdtar in particular auto-detects dozens of formats nobody
|
|
127
|
+
asked it to parse. Every zip offset is checked against the file's real
|
|
128
|
+
size before it is read. A malformed archive throws, and a file that isn't
|
|
129
|
+
what its name claims shows as the file it is.
|
|
130
|
+
|
|
131
|
+
Refused, with the reason shown in the view: encrypted zip members,
|
|
132
|
+
compression methods other than stored and deflate, and device or FIFO
|
|
133
|
+
entries.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Link Previews
|
|
2
|
+
|
|
3
|
+
A brane URL pasted into iMessage, Slack, Discord or Telegram gets a card,
|
|
4
|
+
and the card is built from the HTML the URL serves — by a fetcher that
|
|
5
|
+
never runs the bundle. That bounds what the card can be, and the bound is
|
|
6
|
+
by design: the daemon's host and token ride in the fragment ([View
|
|
7
|
+
URLs](view-urls.md)), which no fetcher sends, and the daemon itself is
|
|
8
|
+
tailnet-only. So a card can carry what brane *is* and what it looks like,
|
|
9
|
+
never a narrative's title, summary or messages. (iMessage is the one app
|
|
10
|
+
that unfurls on the *sender's* device, which is on the tailnet — but a
|
|
11
|
+
direct-daemon link still serves the same shell, so it sees the same card.)
|
|
12
|
+
|
|
13
|
+
The card is `og:*` and `twitter:card` tags in **`expo/public/index.html`**,
|
|
14
|
+
the template that actually ships, with `app/+html.tsx` kept in step the way
|
|
15
|
+
the KaTeX and manifest links are. The picture is the 膜 — a still of the
|
|
16
|
+
real `Membrane` component, not a hand-ported approximation of its shader —
|
|
17
|
+
at `public/og/membrane.v<N>.png` (1200×630, the size every unfurler renders
|
|
18
|
+
as a large card), plus `public/apple-touch-icon.png` (180×180, which is
|
|
19
|
+
what iMessage's small card and an iOS home-screen add use; the SVG favicon
|
|
20
|
+
counts for neither). `expo/scripts/og-membrane/render.mjs` produces both
|
|
21
|
+
by mounting the component alone in a headless Chromium and screenshotting
|
|
22
|
+
it; each run rolls a fresh scheme, so run it until a frame pleases, then
|
|
23
|
+
**bump the version in the filename and in both `og:image` tags** — iMessage
|
|
24
|
+
caches a preview by URL until the URL changes. `src/test/link-preview.test.ts`
|
|
25
|
+
pins that the two heads agree, that the URL names a file `public/` ships at
|
|
26
|
+
the declared size, and that the name is versioned.
|
|
27
|
+
|
|
28
|
+
Two things are less obvious than they look:
|
|
29
|
+
|
|
30
|
+
**The image URL is absolute, on the shared host.** A daemon's own bundle
|
|
31
|
+
serves this same `index.html`, and a phone unfurling a direct-daemon link
|
|
32
|
+
can still fetch `https://brane.organelle.dev/og/…` — a relative path would
|
|
33
|
+
point at the daemon, which the fetcher may or may not reach.
|
|
34
|
+
|
|
35
|
+
**Stills only.** iMessage, Telegram, Discord and Facebook will animate an
|
|
36
|
+
`og:video` loop, and Slack alone offers an interactive card, through a
|
|
37
|
+
registered app. Neither is used: the card is a picture, by decision
|
|
38
|
+
(claude-hun39p6w:10).
|
package/docs/math.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Math Rendering
|
|
2
|
+
|
|
3
|
+
LaTeX in narrative markdown renders through **KaTeX**, in the app and in the HTML export alike. The two call the *same* `renderMathHtml()` in `src/shared/markdown-math.ts`, which is what makes an export a snapshot of the live view rather than a lookalike of it — the export contains no script, so all rendering happens ahead of time.
|
|
4
|
+
|
|
5
|
+
| Piece | Where | Does |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `renderMathHtml()` | `src/shared/markdown-math.ts` | The one KaTeX call. `htmlAndMathml`; unparseable LaTeX falls back to the literal source, not KaTeX's red error markup |
|
|
8
|
+
| `prepareMathMarkdown()` | `src/shared/markdown-math.ts` | Pre-parse normalization, run by both sides before micromark/mdast |
|
|
9
|
+
| `mathHtmlExtension()`, `katexStyleFor()` | `src/math.ts` | Export only: micromark hookup, and the inlined stylesheet |
|
|
10
|
+
| `public/katex/` | `expo/` | The app's stylesheet + woff2, vendored by `npm run sync:katex` |
|
|
11
|
+
| `diagnoseMarkdown()` | `src/markdown-diagnostics.ts` | The same pipeline run to *report* rather than render — see [Rendering Warnings](rendering-warnings.md) |
|
|
12
|
+
|
|
13
|
+
**Every delimiter an LLM might reach for.** Which one a model writes is house style, not a standard, and the difference is stark: across the 42,521 events this was built against, `\(…\)` appears 1576 times and `\[…\]` 351 — all from absorbed ChatGPT history — against 578 single-dollar runs, which are mostly Claude's. A `$`-only reader would leave two thirds of the corpus as prose. `prepareMathMarkdown` normalizes the lot into the `$` forms before parsing, so one syntax reaches both renderers:
|
|
14
|
+
|
|
15
|
+
| Written | Becomes | Renders |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `$x$` | unchanged | inline |
|
|
18
|
+
| `$$x$$` alone on a line | fenced `$$` | display |
|
|
19
|
+
| `\(x\)` | `$x$` | inline |
|
|
20
|
+
| `\[x\]` alone on a line | fenced `$$` | display |
|
|
21
|
+
| ` ```math ` fence | fenced `$$` | display |
|
|
22
|
+
| ` ```latex ` fence | unchanged | source listing, as intended |
|
|
23
|
+
|
|
24
|
+
Unlike `$`, the TeX pairs are unambiguous and need no guard — nothing in ordinary prose looks like them (0 false positives across all 1576). KaTeX also gets a `\*` → `*` macro: that isn't TeX, it's a model escaping an asterisk so markdown won't read emphasis and leaving the escape inside the math. It accounts for 13 of the 18 equations in the archive KaTeX would otherwise reject.
|
|
25
|
+
|
|
26
|
+
Two more things are less obvious than they look:
|
|
27
|
+
|
|
28
|
+
**`$` is mostly not a delimiter.** Narratives are full of prices, shell variables and awk fields, and `micromark-extension-math` is more permissive than Pandoc — it reads `$10k–$500k` as math over `10k–`. `prepareMathMarkdown` escapes the openers that fail a Pandoc-style guard (no space just inside a delimiter, no digit after the closer, no `${…}`, no raw `"`) before parsing, since the extension offers no veto once it has tokenized. It also promotes a one-line `$$…$$` paragraph to the fenced form, which is the only shape the extension renders in display mode. Both sides must run it or exports stop matching the app.
|
|
29
|
+
|
|
30
|
+
**Fonts don't come along by themselves.** KaTeX lays its span tree out entirely in CSS and needs its woff2 faces; without them math is a pile of overlapping glyphs. The export inlines only the families the document reaches for (~166 KB of base64 for an equation-dense narrative, nothing at all for one with no math), derived from the stylesheet so a KaTeX upgrade can't silently drop a face. The app can't inline per document, so it serves `public/katex/` — vendored the way `pdf.min.mjs` already is, because metro bundles a CSS *import* but never copies the `url(fonts/…)` it points at. The `<link>` lives in **`public/index.html`**, which is the real HTML template; `app/+html.tsx` is kept in step but is not what ships.
|
|
31
|
+
|
|
32
|
+
`src/shared/markdown-math.ts` is also the first shared module to import an npm package. Metro resolves a module's dependencies from its own directory and `src/shared` sits outside the expo project root, so `metro.config.js` maps `katex` explicitly — the next shared module with a bare import will need the same.
|
package/docs/mcp.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# MCP and the LIVE Loop
|
|
2
|
+
|
|
3
|
+
The MCP bridge, how a parked `listen` wakes and retires, the LIVE loop tools, and narrative metadata.
|
|
4
|
+
|
|
5
|
+
## MCP Architecture
|
|
6
|
+
|
|
7
|
+
The MCP server (`src/mcp/server.ts`) is a **thin stdio↔WebSocket bridge**. It has zero SQLite dependency. All tool logic (narrate, listen, whoami, revive, etc.) lives in the daemon at `src/daemon/server.ts`.
|
|
8
|
+
|
|
9
|
+
**Error handling convention**: Use `sendToolError()` for real errors (maps to MCP `isError: true`), not `sendToolResult({ error: ... })`. The bridge at `src/mcp/server.ts` translates `tool_error` → `isError: true` on the MCP response. Foreign narration to non-existent narrative IDs returns a tool error.
|
|
10
|
+
|
|
11
|
+
When `brane spawn` creates an agent session, it sets `BRANE_NARRATIVE_ID` as a tmux session environment variable (not in the MCP config file). The MCP bridge process inherits this from the tmux environment at startup. This allows multiple agents to share the same fold without conflicting MCP configs. The one exception is Copilot, which requires explicit env in its per-session config file since it doesn't inherit parent env vars into MCP subprocesses.
|
|
12
|
+
|
|
13
|
+
### Listen Wakeup
|
|
14
|
+
|
|
15
|
+
**The caller's own words never interrupt its waiting.** `checkForEvents`
|
|
16
|
+
returns everything in range but reports an *arrival* only for an event whose
|
|
17
|
+
server-stamped `source` isn't the caller, so an agent's own narration (here or
|
|
18
|
+
narrated into another narrative) neither wakes its parked listen, ends its
|
|
19
|
+
waiting narrate, nor returns a blocking listen at once. It is still returned
|
|
20
|
+
alongside whatever does arrive, and a `wait: false` read returns it on its
|
|
21
|
+
own. #515 dropped those events from forward reads instead, and an agent that
|
|
22
|
+
resumed after compaction with a remembered cursor lost them silently; the
|
|
23
|
+
daemon can't tell that read from any other. Pinned by
|
|
24
|
+
`src/test/listen-own-words.test.ts`.
|
|
25
|
+
|
|
26
|
+
**A listen outlives nothing.** A parked listen (or waiting narrate) whose
|
|
27
|
+
socket closes is retired by the MCP close handler — `cleanup()` unregisters
|
|
28
|
+
its wakeups, its tool row settles, and no reply is built — and `wakeup`
|
|
29
|
+
refuses to answer on a socket that isn't `OPEN`. Otherwise the next event
|
|
30
|
+
ran its `reply`, and the mention receipt inside it marked mentions sent
|
|
31
|
+
(and read) into a socket that drops the send.
|
|
32
|
+
|
|
33
|
+
**Nor does it outlive its caller's patience.** Esc, or killing a
|
|
34
|
+
backgrounded tool task, reaches the bridge as MCP `notifications/cancelled`.
|
|
35
|
+
The bridge forgets the call (a late result is dropped, never sent) and sends
|
|
36
|
+
the daemon `tool_cancel`; `cancelMcpCall` retires the parked call as the
|
|
37
|
+
close handler does, and the awaiting glow goes out at once. Before, the bridge answered the notification "Method not found" (an
|
|
38
|
+
error with no id, which Claude Code logs as a connection error) and the
|
|
39
|
+
daemon never heard, so a cancelled listen glowed until the next event or the
|
|
40
|
+
hour-long timeout. Notifications are never answered, known or not. Pinned by
|
|
41
|
+
`src/test/mcp-bridge-cancel.test.ts` and `src/test/awaiting.test.ts`.
|
|
42
|
+
|
|
43
|
+
The daemon uses a `listenWaiters` map to immediately wake blocked `listen` calls when new events arrive. When a signal is sent or a narration is written, `notifyListenWaiters(nid)` fires all registered callbacks for that narrative. This replaces polling and gives near-instant delivery.
|
|
44
|
+
|
|
45
|
+
`narrate({ wait: true })` is a narrate followed by a listen in one call, so an exchange costs the agent one re-read of its context instead of two. `handleMcpNarrate` writes the narration and returns a `NarrateWaitPlan`; the dispatcher hands the plan to `startMcpListen`, which parks on the plan's cursor with the new narration filtered out wherever it landed (`omit` in `checkForEvents`) and returns listen's result plus the narration's `nid`, `seq` and `warnings`. There is no second waiting mechanism: wakeup, poller, timeout, mention receipt and shutdown flush are listen's, and the pending entry carries its `toolName` so the narrate's row is the one that settles. `narrateWaitCursor` accepts any cursor `listen` does, multi-narrative included (claude-2il811nw:31); with none, it waits from the current heads of the caller's narrative and, for a foreign narration, the target's. `before`/`tail` have nothing to wait for, and they fall back rather than refuse: the narrate posts as usual and its receipt carries `wait_ignored`, since a refusal would cost the agent the narration and a turn to resend it. On a lost daemon connection the bridge tells a pending narrate to check before re-narrating rather than to retry, since a waiting narrate has always already posted.
|
|
46
|
+
|
|
47
|
+
## LIVE Loop Protocol
|
|
48
|
+
|
|
49
|
+
Agents communicate via cursor-based long-polling. Read `protocols/LIVE_loop.md` for full details.
|
|
50
|
+
|
|
51
|
+
Core tools available via MCP:
|
|
52
|
+
|
|
53
|
+
| Tool | Purpose |
|
|
54
|
+
|------|---------|
|
|
55
|
+
| **narrate** | Vocalize status + receive pending signals (non-blocking) |
|
|
56
|
+
| **listen** | Block until signals arrive (long-poll with timeout) |
|
|
57
|
+
| **whoami** | Get narrative ID and brane ID |
|
|
58
|
+
| **revive** | Restart a dead agent session |
|
|
59
|
+
| **narrative_search** | Semantic search across narrative history |
|
|
60
|
+
| **narrative_info** | Get narrative info: browse recent narratives or get details on specific ones |
|
|
61
|
+
| **list_peers** | List active LIVE-loop agent peers, grouped by fold |
|
|
62
|
+
| **list_branes** | List all branes hosted by this daemon (caller's home first) — use to expand `narrative_search` / `narrative_info` across branes |
|
|
63
|
+
| **list_protocols** / **read_protocol** | Access protocol documents |
|
|
64
|
+
|
|
65
|
+
## Narrative Metadata
|
|
66
|
+
|
|
67
|
+
Narratives have persistent `summary` and `title` columns. Agents set them via the `meta` parameter on `narrate`. The daemon broadcasts metadata changes over the control WebSocket as `type: "meta"` messages. The Expo app renders the summary as a subtitle below the narrative ID.
|
|
68
|
+
|
|
69
|
+
Narratives can be **archived** via `PATCH /api/narratives/:id/archive`. Archived narratives are hidden from the default list but preserved in the database.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Memory and Search Ranking
|
|
2
|
+
|
|
3
|
+
How narrative history is indexed for semantic search, and how hybrid search ranks what it finds.
|
|
4
|
+
|
|
5
|
+
## Memory System
|
|
6
|
+
|
|
7
|
+
The memory system provides semantic search across narrative history:
|
|
8
|
+
|
|
9
|
+
- **Vector embeddings** via `onnxruntime-node` directly (bge-small-en-v1.5, 384-dim) — `embedder.ts` does the four steps brane used from `@huggingface/transformers` (tokenize → ONNX session → masked mean pool → L2 normalize), producing vectors identical to the pipeline it replaced (worst cosine 0.9999996), so existing `memory_vec` rows stay valid. Tokenization is `wordpiece.ts`, a WordPiece port whose ids match transformers.js exactly. The model cache layout (`~/.brane/models/<repo>/<file>`) is unchanged, so existing caches carry over.
|
|
10
|
+
- **FTS5 full-text search** for keyword matching
|
|
11
|
+
- **Hybrid search** combining both approaches — see [Search Ranking](#search-ranking)
|
|
12
|
+
- **Auto-indexing** of new events on first search
|
|
13
|
+
- CLI: `brane memory search <query>`, `brane memory log`, `brane memory index`
|
|
14
|
+
- MCP: `narrative_search` and `narrative_info` tools
|
|
15
|
+
|
|
16
|
+
## Search Ranking
|
|
17
|
+
|
|
18
|
+
`searchHybrid` (`src/memory/storage.ts`) fuses **three ranked arms**, and that it
|
|
19
|
+
fuses *ranks* rather than *scores* is the whole design.
|
|
20
|
+
|
|
21
|
+
| Arm | Query | For |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| ALL | `prepareFtsQuery` — FTS5 implicit AND | A remembered fragment. Precise; returns nothing for a question |
|
|
24
|
+
| ANY | `prepareFtsAnyQuery` — content terms ORed | A question. Always answers; ranks loosely |
|
|
25
|
+
| vector | KNN over `memory_vec` | Rows sharing no terms with the query at all |
|
|
26
|
+
|
|
27
|
+
Each contributes `weight / (RRF_K + rank)`; the fused total is then multiplied by
|
|
28
|
+
how much of the query the row actually contains.
|
|
29
|
+
|
|
30
|
+
**Scores from different arms are not comparable, and pretending otherwise
|
|
31
|
+
inverted the ranking.** The vector arm's number is a cosine — measured across
|
|
32
|
+
this archive it spans 0.62–0.73 for *everything*, a band so narrow it barely
|
|
33
|
+
orders anything — while the lexical arms' is a squashed bm25 topping out near
|
|
34
|
+
0.7 for an excellent hit. Blending those, plus a flat `+0.5` awarded for **any
|
|
35
|
+
single** query term appearing, produced the failure this replaced: for
|
|
36
|
+
`self-inflicted instance restarted the daemon four times today`, the only chunk
|
|
37
|
+
in 43,876 containing all eight terms came back **11th**, behind ten one-line
|
|
38
|
+
operator signals ("Restart* daemon", "Daemon is restarted") that each held one
|
|
39
|
+
word and sat near the query in embedding space. Over 60 verbatim quotations
|
|
40
|
+
pulled from real events, the source ranked first **50%** of the time; under rank
|
|
41
|
+
fusion, **90%** (top-10: 85% → 98%).
|
|
42
|
+
|
|
43
|
+
Four things are less obvious than they look:
|
|
44
|
+
|
|
45
|
+
**Coverage ranks; it does not gate.** A row is credited for the *fraction* of
|
|
46
|
+
query terms it holds — 1-of-8 and 8-of-8 used to score identically. But
|
|
47
|
+
qualifying as lexical evidence is a **count** (`MIN_TERM_HITS`), not a fraction,
|
|
48
|
+
because a fraction punishes long queries for being long: a narration matching 2
|
|
49
|
+
of 8 terms of a paraphrased question is real evidence that a 50% gate discards.
|
|
50
|
+
|
|
51
|
+
**The ANY arm exists because AND silently disables half the search.** No chunk
|
|
52
|
+
contains all thirteen words of a natural-language question, so the strict arm
|
|
53
|
+
returns zero candidates exactly when the query is hardest — leaving the vector
|
|
54
|
+
arm alone, which on this corpus finds a quotation's own source only a third of
|
|
55
|
+
the time. Stopwords are dropped from ANY (ORed, `the` matches most of the
|
|
56
|
+
archive) and kept in ALL, where every term is a restriction and narrows for
|
|
57
|
+
free.
|
|
58
|
+
|
|
59
|
+
**The length penalty is gated on lexical evidence, not on `match`.** They differ
|
|
60
|
+
for precisely the rows it exists to catch: a two-character chunk sitting close to
|
|
61
|
+
the query clears `MIN_SEMANTIC_SCORE`, gets tagged `semantic`, and would sail
|
|
62
|
+
past a `!match` guard. Similarity to a very short string is cheap, and is not
|
|
63
|
+
evidence that the string answers anything.
|
|
64
|
+
|
|
65
|
+
**The `weak` floor is relative.** A fused rank has no absolute meaning to
|
|
66
|
+
threshold against — it depends on how many arms returned the row and how deep
|
|
67
|
+
each went — so the old `score >= 0.35` constant would pass everything or nothing
|
|
68
|
+
as `limit` moved. Quoted phrases remain the one filter allowed to return nothing;
|
|
69
|
+
every other path still degrades to best-effort guesses rather than a blank page.
|
|
70
|
+
|
|
71
|
+
`src/test/search-fusion.test.ts` pins each defect as the smallest corpus that
|
|
72
|
+
reproduces it, with hand-chosen vectors. Deliberately **not** an aggregate
|
|
73
|
+
accuracy figure: the same probe read 52%, 50% and 37% across one afternoon
|
|
74
|
+
purely as new events were indexed, so a percentage over the live archive would be
|
|
75
|
+
a flaky test. A percentage says something regressed; these say which thing.
|