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
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { parseDiagram } from "../diagram/parse.js";
|
|
4
|
+
import { renderDiagram } from "../diagram/index.js";
|
|
5
|
+
import { layoutDiagram } from "../diagram/layout.js";
|
|
6
|
+
import { measureEdgeLabel } from "../diagram/metrics.js";
|
|
7
|
+
// A self-loop's label was parsed, carried through the IR, and dropped at the
|
|
8
|
+
// last step: layout synthesises loops itself (dagre never sees them) and left
|
|
9
|
+
// `labelPos` undefined, which the renderer reads as "no label". Nothing
|
|
10
|
+
// warned — the diagram drew — so a Rule 30 state graph came back with all 24
|
|
11
|
+
// arrows and none of the three loop labels that distinguish the vacuum from
|
|
12
|
+
// the two stripe phases (claude-1vugmmfw:5, :7, :9). Every test here reads the
|
|
13
|
+
// *output*: a label in the IR proves nothing, which is the trap the `classDef`
|
|
14
|
+
// test fell into before it.
|
|
15
|
+
const ok = (src) => {
|
|
16
|
+
const r = parseDiagram(src);
|
|
17
|
+
assert.equal(r.ok, true, `expected a parse, got ${r.ok ? "" : r.refusal.message}`);
|
|
18
|
+
return r.ok ? r.ir : undefined;
|
|
19
|
+
};
|
|
20
|
+
/** The chip `svg.ts` draws for a label, as a rect — the thing that has to fit. */
|
|
21
|
+
function chipRect(e) {
|
|
22
|
+
assert.ok(e.label && e.labelPos, `${e.from}→${e.to} has a placed label`);
|
|
23
|
+
const { width, height } = measureEdgeLabel(e.label);
|
|
24
|
+
return {
|
|
25
|
+
left: e.labelPos.x - width / 2, right: e.labelPos.x + width / 2,
|
|
26
|
+
top: e.labelPos.y - height / 2, bottom: e.labelPos.y + height / 2,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const intersects = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
|
|
30
|
+
function assertInside(placed, r, what) {
|
|
31
|
+
assert.ok(r.left >= 0 && r.top >= 0 && r.right <= placed.width && r.bottom <= placed.height, `${what} chip ${JSON.stringify(r)} is not inside ${placed.width}x${placed.height}`);
|
|
32
|
+
}
|
|
33
|
+
/** The diagram self-loop labels were built for: eight states, 24 arrows, three
|
|
34
|
+
* loops (claude-1vugmmfw:5). */
|
|
35
|
+
const RULE30 = `flowchart TD
|
|
36
|
+
s000(["000"])
|
|
37
|
+
s001(["001"])
|
|
38
|
+
s010(["010"])
|
|
39
|
+
s011(["011"])
|
|
40
|
+
s100(["100"])
|
|
41
|
+
s101(["101"])
|
|
42
|
+
s110(["110"])
|
|
43
|
+
s111(["111"])
|
|
44
|
+
s000 -->|"00000"| s000
|
|
45
|
+
s000 -->|"00001"| s001
|
|
46
|
+
s000 -->|"10000"| s100
|
|
47
|
+
s000 -->|"10001"| s101
|
|
48
|
+
s001 -->|"00010/00011"| s011
|
|
49
|
+
s001 -->|"10010/10011"| s111
|
|
50
|
+
s010 -->|"10101"| s010
|
|
51
|
+
s010 -->|"10100"| s011
|
|
52
|
+
s010 -->|"00101"| s110
|
|
53
|
+
s010 -->|"00100"| s111
|
|
54
|
+
s011 -->|"10110/10111"| s010
|
|
55
|
+
s011 -->|"00110/00111"| s110
|
|
56
|
+
s100 -->|"11000"| s010
|
|
57
|
+
s100 -->|"11001"| s011
|
|
58
|
+
s100 -->|"01000"| s110
|
|
59
|
+
s100 -->|"01001"| s111
|
|
60
|
+
s101 -->|"11010/11011"| s001
|
|
61
|
+
s101 -->|"01010/01011"| s101
|
|
62
|
+
s110 -->|"11101"| s000
|
|
63
|
+
s110 -->|"11100"| s001
|
|
64
|
+
s110 -->|"01101"| s100
|
|
65
|
+
s110 -->|"01100"| s101
|
|
66
|
+
s111 -->|"11110/11111"| s000
|
|
67
|
+
s111 -->|"01110/01111"| s100`;
|
|
68
|
+
const loopOf = (placed, id) => placed.edges.find((e) => e.selfLoop && e.from === id);
|
|
69
|
+
describe("a self-loop's label is drawn", () => {
|
|
70
|
+
const DIRS = ["TD", "BT", "LR", "RL"];
|
|
71
|
+
it("reaches the SVG in every direction", () => {
|
|
72
|
+
for (const dir of DIRS) {
|
|
73
|
+
const r = renderDiagram(`flowchart ${dir}\n A -->|"vacuum"| A\n A --> B`);
|
|
74
|
+
assert.ok(r && r.svg.includes(">vacuum<"), `${dir}: the loop label is in the output`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
it("fits inside the viewBox, by its chip's four corners, on a top-rank node", () => {
|
|
78
|
+
// The union that sizes the canvas covered only the curve's points, and a
|
|
79
|
+
// chip is wider than the point it hangs on. "The text is in the SVG"
|
|
80
|
+
// would pass a chip drawn at negative y and never seen.
|
|
81
|
+
for (const dir of DIRS) {
|
|
82
|
+
const placed = layoutDiagram(ok(`flowchart ${dir}\n A -->|"a long loop label"| A\n A --> B`));
|
|
83
|
+
assertInside(placed, chipRect(loopOf(placed, "A")), `${dir} A`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
it("fits on the far side too, where only the maximum would have noticed", () => {
|
|
87
|
+
// A right-hand node in LR bows past `graph.width`; a bottom node in BT
|
|
88
|
+
// bows below `graph.height`. Both grow the canvas rather than clipping.
|
|
89
|
+
for (const src of [
|
|
90
|
+
`flowchart LR\n A --> B\n B -->|"a long loop label"| B`,
|
|
91
|
+
`flowchart TD\n A --> B\n B -->|"a long loop label"| B`,
|
|
92
|
+
]) {
|
|
93
|
+
const placed = layoutDiagram(ok(src));
|
|
94
|
+
assertInside(placed, chipRect(loopOf(placed, "B")), src);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
it("sits outside the node, off a cross-axis face — one no rank edge uses", () => {
|
|
98
|
+
for (const dir of ["TD", "BT"]) {
|
|
99
|
+
const placed = layoutDiagram(ok(`flowchart ${dir}\n A -->|"x"| A`));
|
|
100
|
+
const a = placed.nodes[0];
|
|
101
|
+
assert.ok(chipRect(loopOf(placed, "A")).left > a.x + a.width / 2, `${dir}: right of the right face`);
|
|
102
|
+
}
|
|
103
|
+
for (const dir of ["LR", "RL"]) {
|
|
104
|
+
const placed = layoutDiagram(ok(`flowchart ${dir}\n A -->|"x"| A`));
|
|
105
|
+
const a = placed.nodes[0];
|
|
106
|
+
assert.ok(chipRect(loopOf(placed, "A")).top > a.y + a.height / 2, `${dir}: below the bottom face`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe("the loop's space is reserved from dagre", () => {
|
|
111
|
+
// Dagre routes around what it was told about, and it was told nothing about
|
|
112
|
+
// the bow. On the Rule 30 graph it parked `01100` (110→101) exactly where
|
|
113
|
+
// 101's loop bows, and with the loop's own label missing, a reader took it
|
|
114
|
+
// for the loop's. Placing the label alone would have turned one wrong chip
|
|
115
|
+
// into two overlapping ones.
|
|
116
|
+
it("keeps another edge's chip out of the loop's chip", () => {
|
|
117
|
+
for (const dir of ["TD", "LR"]) {
|
|
118
|
+
const placed = layoutDiagram(ok(`flowchart ${dir}\n B -->|"from B to A"| A\n A -->|"loop on A"| A`));
|
|
119
|
+
const loop = chipRect(loopOf(placed, "A"));
|
|
120
|
+
const other = chipRect(placed.edges.find((e) => !e.selfLoop));
|
|
121
|
+
assert.ok(!intersects(loop, other), `${dir}: ${JSON.stringify(loop)} overlaps ${JSON.stringify(other)}`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
it("but the node itself is drawn at its measured size, and edges attach to it", () => {
|
|
125
|
+
// Only dagre's box grows. If the drawn node grew with it, or attachments
|
|
126
|
+
// used the inflated extents, every other edge on a loop-bearing node
|
|
127
|
+
// would end in the air above its face.
|
|
128
|
+
const placed = layoutDiagram(ok(`flowchart TD\n B -->|"in"| A\n A -->|"loop"| A`));
|
|
129
|
+
const plain = layoutDiagram(ok(`flowchart TD\n B -->|"in"| A`));
|
|
130
|
+
const a = placed.nodes.find((n) => n.id === "A");
|
|
131
|
+
const a0 = plain.nodes.find((n) => n.id === "A");
|
|
132
|
+
assert.deepEqual([a.width, a.height], [a0.width, a0.height]);
|
|
133
|
+
const into = placed.edges.find((e) => !e.selfLoop);
|
|
134
|
+
const end = into.points[into.points.length - 1];
|
|
135
|
+
assert.equal(end.y, a.y - a.height / 2, "the incoming edge ends on A's top face");
|
|
136
|
+
});
|
|
137
|
+
it("does not grow a node that has no loop", () => {
|
|
138
|
+
const withLoop = layoutDiagram(ok(`flowchart TD\n A --> B\n B -->|"loop"| B`));
|
|
139
|
+
const without = layoutDiagram(ok(`flowchart TD\n A --> B`));
|
|
140
|
+
const a = withLoop.nodes.find((n) => n.id === "A");
|
|
141
|
+
const a0 = without.nodes.find((n) => n.id === "A");
|
|
142
|
+
assert.deepEqual([a.width, a.height, a.y], [a0.width, a0.height, a0.y], "A keeps its size and its rank");
|
|
143
|
+
});
|
|
144
|
+
it("grows the cross axis, not the rank axis", () => {
|
|
145
|
+
// A TD diagram's height is the chat bubble's height. #504 reserved the
|
|
146
|
+
// loop's room along the rank axis — +50% on a two-node graph — and on
|
|
147
|
+
// the side every rank edge arrives from.
|
|
148
|
+
const withLoop = layoutDiagram(ok(`flowchart TD\n B -->|"in"| A\n A -->|"loop"| A`));
|
|
149
|
+
const without = layoutDiagram(ok(`flowchart TD\n B -->|"in"| A`));
|
|
150
|
+
assert.ok(withLoop.height <= without.height + 8, `${withLoop.height} vs ${without.height}`);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe("two loops on one node", () => {
|
|
154
|
+
// They used to draw the same bow twice — and would have stacked their
|
|
155
|
+
// chips on top of each other. The second bows outside the first's chip.
|
|
156
|
+
it("draw as two bows with two legible chips", () => {
|
|
157
|
+
for (const dir of ["TD", "LR"]) {
|
|
158
|
+
const placed = layoutDiagram(ok(`flowchart ${dir}\n A -->|"first"| A\n A -->|"second"| A\n A --> B`));
|
|
159
|
+
const loops = placed.edges.filter((e) => e.selfLoop);
|
|
160
|
+
assert.equal(loops.length, 2);
|
|
161
|
+
assert.notDeepEqual(loops[0].points, loops[1].points, `${dir}: distinct bows`);
|
|
162
|
+
const [c0, c1] = loops.map(chipRect);
|
|
163
|
+
assert.ok(!intersects(c0, c1), `${dir}: chips ${JSON.stringify(c0)} / ${JSON.stringify(c1)}`);
|
|
164
|
+
assertInside(placed, c0, `${dir} first`);
|
|
165
|
+
assertInside(placed, c1, `${dir} second`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe("the Rule 30 state graph", () => {
|
|
170
|
+
// The diagram this was found on: eight states, 24 arrows, three loops.
|
|
171
|
+
const src = RULE30;
|
|
172
|
+
it("renders all three loop labels, inside the canvas, clear of every other chip", () => {
|
|
173
|
+
const r = renderDiagram(src);
|
|
174
|
+
assert.ok(r);
|
|
175
|
+
for (const l of ["00000", "10101", "01010/01011"]) {
|
|
176
|
+
assert.ok(r.svg.includes(`>${l}<`), `${l} is drawn`);
|
|
177
|
+
}
|
|
178
|
+
const placed = layoutDiagram(ok(src));
|
|
179
|
+
const loops = placed.edges.filter((e) => e.selfLoop);
|
|
180
|
+
assert.equal(loops.length, 3);
|
|
181
|
+
const others = placed.edges.filter((e) => !e.selfLoop && e.labelPos).map(chipRect);
|
|
182
|
+
for (const loop of loops) {
|
|
183
|
+
const chip = chipRect(loop);
|
|
184
|
+
assertInside(placed, chip, loop.from);
|
|
185
|
+
for (const o of others) {
|
|
186
|
+
assert.ok(!intersects(chip, o), `${loop.from}'s chip overlaps another label`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
// The finding that moved the loops (claude-djdja7g6:13): the checks above hold a
|
|
192
|
+
// loop's chip against other *chips*, and the Rule 30 test passed while three
|
|
193
|
+
// edges ran straight through 101's label. A chip an edge passes through reads
|
|
194
|
+
// as that edge's label, so what has to hold is chip against *path*.
|
|
195
|
+
describe("no edge runs through a loop's label", () => {
|
|
196
|
+
/** Points along an edge as drawn — `curveBasis`, sampled. */
|
|
197
|
+
function along(points) {
|
|
198
|
+
if (points.length < 3)
|
|
199
|
+
return points;
|
|
200
|
+
// B-spline, uniform, clamped the way d3's curveBasis is: sample densely.
|
|
201
|
+
const p = [points[0], points[0], ...points, points[points.length - 1], points[points.length - 1]];
|
|
202
|
+
const out = [];
|
|
203
|
+
for (let i = 0; i + 3 < p.length; i++) {
|
|
204
|
+
for (let s = 0; s <= 20; s++) {
|
|
205
|
+
const t = s / 20, t2 = t * t, t3 = t2 * t;
|
|
206
|
+
const b0 = (1 - 3 * t + 3 * t2 - t3) / 6, b1 = (4 - 6 * t2 + 3 * t3) / 6;
|
|
207
|
+
const b2 = (1 + 3 * t + 3 * t2 - 3 * t3) / 6, b3 = t3 / 6;
|
|
208
|
+
out.push({
|
|
209
|
+
x: b0 * p[i].x + b1 * p[i + 1].x + b2 * p[i + 2].x + b3 * p[i + 3].x,
|
|
210
|
+
y: b0 * p[i].y + b1 * p[i + 1].y + b2 * p[i + 2].y + b3 * p[i + 3].y,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
const inside = (q, r) => q.x > r.left && q.x < r.right && q.y > r.top && q.y < r.bottom;
|
|
217
|
+
function crossings(src) {
|
|
218
|
+
const placed = layoutDiagram(ok(src));
|
|
219
|
+
const out = [];
|
|
220
|
+
for (const loop of placed.edges.filter((e) => e.selfLoop && e.label)) {
|
|
221
|
+
const chip = chipRect(loop);
|
|
222
|
+
for (const o of placed.edges) {
|
|
223
|
+
if (o === loop)
|
|
224
|
+
continue;
|
|
225
|
+
if (along(o.points).some((q) => inside(q, chip)))
|
|
226
|
+
out.push(`${o.from}→${o.to} crosses ${loop.from}'s loop label`);
|
|
227
|
+
}
|
|
228
|
+
for (const n of placed.nodes) {
|
|
229
|
+
const r = { left: n.x - n.width / 2, right: n.x + n.width / 2, top: n.y - n.height / 2, bottom: n.y + n.height / 2 };
|
|
230
|
+
if (intersects(chip, r))
|
|
231
|
+
out.push(`${loop.from}'s loop label covers node ${n.id}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
it("the smallest case: an edge into a node with a labelled loop", () => {
|
|
237
|
+
for (const dir of ["TD", "BT", "LR", "RL"]) {
|
|
238
|
+
assert.deepEqual(crossings(`flowchart ${dir}\n B --> A\n A -->|"loop"| A`), [], dir);
|
|
239
|
+
assert.deepEqual(crossings(`flowchart ${dir}\n A -->|"loop"| A\n A --> C\n B --> A`), [], `${dir}, in and out`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
it("two loop-bearing nodes side by side, with labels wider than the nodes", () => {
|
|
243
|
+
for (const dir of ["TD", "LR"]) {
|
|
244
|
+
assert.deepEqual(crossings(`flowchart ${dir}\n R --> A\n R --> B\n A -->|"a rather long loop label"| A\n B -->|"another long loop label"| B`), [], dir);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
it("stacked loops with labels of different widths, in every direction", () => {
|
|
248
|
+
// #504 stacked chips by height in every direction, so beside the node a
|
|
249
|
+
// wider first label ran into the second: "a lon[second]abel".
|
|
250
|
+
for (const dir of ["TD", "BT", "LR", "RL"]) {
|
|
251
|
+
const placed = layoutDiagram(ok(`flowchart ${dir}\n A -->|"a longer first label"| A\n A -->|"second"| A\n A -->|"3"| A\n B --> A`));
|
|
252
|
+
const chips = placed.edges.filter((e) => e.selfLoop).map(chipRect);
|
|
253
|
+
for (let i = 0; i < chips.length; i++) {
|
|
254
|
+
assertInside(placed, chips[i], `${dir} loop ${i}`);
|
|
255
|
+
for (let j = i + 1; j < chips.length; j++) {
|
|
256
|
+
assert.ok(!intersects(chips[i], chips[j]), `${dir}: loop chips ${i} and ${j} overlap`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
assert.deepEqual(crossings(`flowchart ${dir}\n A -->|"a longer first label"| A\n A -->|"second"| A\n A -->|"3"| A\n B --> A`), [], dir);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
it("a loop without a label draws, reserves its bow, and places nothing", () => {
|
|
263
|
+
const placed = layoutDiagram(ok(`flowchart TD\n B --> A\n A --> A`));
|
|
264
|
+
const loop = loopOf(placed, "A");
|
|
265
|
+
assert.equal(loop.labelPos, undefined);
|
|
266
|
+
const a = placed.nodes.find((n) => n.id === "A");
|
|
267
|
+
assert.ok(loop.points.every((q) => q.x >= a.x + a.width / 2 - 1e-9), "the bow is off the right face");
|
|
268
|
+
assert.ok(Math.max(...loop.points.map((q) => q.x)) <= placed.width, "and inside the canvas");
|
|
269
|
+
});
|
|
270
|
+
it("an edge fanning into a wide-labelled loop node, in every direction", () => {
|
|
271
|
+
// Dagre ends each edge on the node's inflated box; pulled back onto the
|
|
272
|
+
// drawn face, the last leg of a steep edge used to cut the corner of a
|
|
273
|
+
// chip wider than its node (LR/RL: claude-w6c1k8h4:7).
|
|
274
|
+
for (const dir of ["TD", "BT", "LR", "RL"]) {
|
|
275
|
+
assert.deepEqual(crossings(`flowchart ${dir}\n A --> X\n B --> X\n C --> X\n X -->|"a rather long loop label"| X`), [], `${dir}, fan-in`);
|
|
276
|
+
assert.deepEqual(crossings(`flowchart ${dir}\n X -->|"a rather long loop label"| X\n X --> A\n X --> B\n X --> C`), [], `${dir}, fan-out`);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
it("each label sits just outside its own arc, stacked or not", () => {
|
|
280
|
+
// The apex is 23/24 of the control depth, not 5/6: with the wrong ratio an
|
|
281
|
+
// outer arc of a stack ran through its own label.
|
|
282
|
+
for (const dir of ["TD", "BT", "LR", "RL"]) {
|
|
283
|
+
const placed = layoutDiagram(ok(`flowchart ${dir}\n A -->|"one"| A\n A -->|"a middling label"| A\n A -->|"3"| A\n B --> A`));
|
|
284
|
+
const a = placed.nodes.find((n) => n.id === "A");
|
|
285
|
+
const vertical = dir === "LR" || dir === "RL"; // loops hang below
|
|
286
|
+
for (const loop of placed.edges.filter((e) => e.selfLoop)) {
|
|
287
|
+
const chip = chipRect(loop);
|
|
288
|
+
const arc = along(loop.points);
|
|
289
|
+
assert.ok(!arc.some((q) => inside(q, chip)), `${dir}: "${loop.label}" is crossed by its own arc`);
|
|
290
|
+
const apex = vertical
|
|
291
|
+
? Math.max(...arc.map((q) => q.y)) - (a.y + a.height / 2)
|
|
292
|
+
: Math.max(...arc.map((q) => q.x)) - (a.x + a.width / 2);
|
|
293
|
+
const near = vertical ? chip.top - (a.y + a.height / 2) : chip.left - (a.x + a.width / 2);
|
|
294
|
+
assert.ok(near - apex >= 1 && near - apex <= 6, `${dir}: "${loop.label}" sits ${(near - apex).toFixed(1)}px past its apex`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
it("the Rule 30 board", () => {
|
|
299
|
+
const src = RULE30;
|
|
300
|
+
assert.deepEqual(crossings(src), []);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
//# sourceMappingURL=diagram-self-loop-labels.test.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { after, describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { createEmbedder, l2Normalize, meanPool, VECTOR_DIM, } from "../memory/embedder.js";
|
|
7
|
+
import { EMBEDDING_GOLDEN } from "./fixtures/embedding-golden.js";
|
|
8
|
+
/** Cosine of two unit-ish vectors. */
|
|
9
|
+
function cosine(a, b) {
|
|
10
|
+
let dot = 0;
|
|
11
|
+
for (let i = 0; i < a.length; i++)
|
|
12
|
+
dot += a[i] * b[i];
|
|
13
|
+
return dot;
|
|
14
|
+
}
|
|
15
|
+
describe("pooling math", () => {
|
|
16
|
+
it("mean-pools only the unmasked positions", () => {
|
|
17
|
+
// Two positions of a 2-dim hidden state; the second is padding.
|
|
18
|
+
const hidden = Float32Array.from([1, 2, 100, 200]);
|
|
19
|
+
const pooled = meanPool(hidden, 0, 2, 2, [1, 0]);
|
|
20
|
+
assert.deepEqual(Array.from(pooled), [1, 2]);
|
|
21
|
+
});
|
|
22
|
+
it("averages rather than sums, so sequence length doesn't scale the vector", () => {
|
|
23
|
+
const hidden = Float32Array.from([1, 1, 3, 3]);
|
|
24
|
+
assert.deepEqual(Array.from(meanPool(hidden, 0, 2, 2, [1, 1])), [2, 2]);
|
|
25
|
+
});
|
|
26
|
+
it("reads the right sequence out of a batch via its base offset", () => {
|
|
27
|
+
// [batch=2, seq=1, dim=2]
|
|
28
|
+
const hidden = Float32Array.from([1, 2, 30, 40]);
|
|
29
|
+
assert.deepEqual(Array.from(meanPool(hidden, 2, 1, 2, [1])), [30, 40]);
|
|
30
|
+
});
|
|
31
|
+
it("normalizes to unit length", () => {
|
|
32
|
+
const v = Float32Array.from([3, 4]);
|
|
33
|
+
l2Normalize(v);
|
|
34
|
+
assert.ok(Math.abs(Math.hypot(v[0], v[1]) - 1) < 1e-6);
|
|
35
|
+
assert.ok(Math.abs(v[0] - 0.6) < 1e-6);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
const modelPath = join(process.env.BRANE_MODEL_CACHE || join(homedir(), ".brane", "models"), "Xenova", "bge-small-en-v1.5", "onnx", "model.onnx");
|
|
39
|
+
const haveModel = existsSync(modelPath);
|
|
40
|
+
describe("embedder", { skip: haveModel ? false : `no model at ${modelPath}` }, () => {
|
|
41
|
+
let embedder = null;
|
|
42
|
+
const get = async () => (embedder ??= await createEmbedder());
|
|
43
|
+
after(async () => {
|
|
44
|
+
await embedder?.dispose();
|
|
45
|
+
});
|
|
46
|
+
it("reproduces the vectors transformers.js produced", async () => {
|
|
47
|
+
// The reason this file exists. `memory_vec` already holds vectors from the
|
|
48
|
+
// old pipeline, so drift here silently degrades search rather than failing.
|
|
49
|
+
const e = await get();
|
|
50
|
+
const vectors = await e.embed(EMBEDDING_GOLDEN.map((g) => g.text));
|
|
51
|
+
for (let i = 0; i < EMBEDDING_GOLDEN.length; i++) {
|
|
52
|
+
const sim = cosine(vectors[i], EMBEDDING_GOLDEN[i].vector);
|
|
53
|
+
assert.ok(sim > 0.99999, `cosine ${sim} for ${JSON.stringify(EMBEDDING_GOLDEN[i].text.slice(0, 40))}`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
it("returns normalized vectors of the schema's width", async () => {
|
|
57
|
+
const [v] = await (await get()).embed(["a sentence"]);
|
|
58
|
+
assert.equal(v.length, VECTOR_DIM);
|
|
59
|
+
let norm = 0;
|
|
60
|
+
for (const x of v)
|
|
61
|
+
norm += x * x;
|
|
62
|
+
assert.ok(Math.abs(Math.sqrt(norm) - 1) < 1e-5);
|
|
63
|
+
});
|
|
64
|
+
it("embeds a text identically whether batched or alone", async () => {
|
|
65
|
+
// Padding leaks into the vector if the attention mask is ignored, and the
|
|
66
|
+
// symptom is subtle: results that shift depending on batch composition.
|
|
67
|
+
const e = await get();
|
|
68
|
+
const [alone] = await e.embed(["short"]);
|
|
69
|
+
const batched = await e.embed(["short", "a considerably longer neighbour sentence"]);
|
|
70
|
+
assert.ok(cosine(alone, batched[0]) > 0.99999);
|
|
71
|
+
});
|
|
72
|
+
it("places related text nearer than unrelated text", async () => {
|
|
73
|
+
const [a, b, c] = await (await get()).embed([
|
|
74
|
+
"the daemon streams terminal output over a websocket",
|
|
75
|
+
"tmux panes are mirrored to the browser in real time",
|
|
76
|
+
"sourdough needs a long cold fermentation",
|
|
77
|
+
]);
|
|
78
|
+
assert.ok(cosine(a, b) > cosine(a, c));
|
|
79
|
+
});
|
|
80
|
+
it("returns nothing for an empty batch without loading anything", async () => {
|
|
81
|
+
assert.deepEqual(await (await get()).embed([]), []);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
//# sourceMappingURL=embedder.test.js.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createTestDb } from "./helpers.js";
|
|
4
|
+
import { appendEvent, readEventsSince, getMaxSeq } from "../db/events.js";
|
|
5
|
+
describe("events", () => {
|
|
6
|
+
it("appends events with auto-incrementing seq", () => {
|
|
7
|
+
const db = createTestDb();
|
|
8
|
+
const e1 = appendEvent(db, "test-narrative", "narration", { content: "hello" });
|
|
9
|
+
const e2 = appendEvent(db, "test-narrative", "narration", { content: "world" });
|
|
10
|
+
assert.equal(e1.seq, 1);
|
|
11
|
+
assert.equal(e2.seq, 2);
|
|
12
|
+
assert.equal(e1.content, "hello");
|
|
13
|
+
assert.equal(e2.content, "world");
|
|
14
|
+
});
|
|
15
|
+
it("sequences are per-narrative", () => {
|
|
16
|
+
const db = createTestDb();
|
|
17
|
+
appendEvent(db, "narrative-a", "narration", { content: "a1" });
|
|
18
|
+
appendEvent(db, "narrative-a", "narration", { content: "a2" });
|
|
19
|
+
const b1 = appendEvent(db, "narrative-b", "narration", { content: "b1" });
|
|
20
|
+
assert.equal(b1.seq, 1); // starts at 1, not 3
|
|
21
|
+
});
|
|
22
|
+
it("readEventsSince returns events after cursor", () => {
|
|
23
|
+
const db = createTestDb();
|
|
24
|
+
appendEvent(db, "n1", "narration", { content: "one" });
|
|
25
|
+
appendEvent(db, "n1", "narration", { content: "two" });
|
|
26
|
+
appendEvent(db, "n1", "narration", { content: "three" });
|
|
27
|
+
const events = readEventsSince(db, "n1", 1);
|
|
28
|
+
assert.equal(events.length, 2);
|
|
29
|
+
assert.equal(events[0].content, "two");
|
|
30
|
+
assert.equal(events[1].content, "three");
|
|
31
|
+
});
|
|
32
|
+
it("readEventsSince with cursor 0 returns all events", () => {
|
|
33
|
+
const db = createTestDb();
|
|
34
|
+
appendEvent(db, "n1", "narration", { content: "one" });
|
|
35
|
+
appendEvent(db, "n1", "narration", { content: "two" });
|
|
36
|
+
const events = readEventsSince(db, "n1", 0);
|
|
37
|
+
assert.equal(events.length, 2);
|
|
38
|
+
});
|
|
39
|
+
it("readEventsSince respects limit", () => {
|
|
40
|
+
const db = createTestDb();
|
|
41
|
+
for (let i = 0; i < 10; i++) {
|
|
42
|
+
appendEvent(db, "n1", "narration", { content: `msg-${i}` });
|
|
43
|
+
}
|
|
44
|
+
const events = readEventsSince(db, "n1", 0, 3);
|
|
45
|
+
assert.equal(events.length, 3);
|
|
46
|
+
assert.equal(events[0].content, "msg-0");
|
|
47
|
+
assert.equal(events[2].content, "msg-2");
|
|
48
|
+
});
|
|
49
|
+
it("readEventsSince returns empty for unknown narrative", () => {
|
|
50
|
+
const db = createTestDb();
|
|
51
|
+
const events = readEventsSince(db, "nonexistent", 0);
|
|
52
|
+
assert.equal(events.length, 0);
|
|
53
|
+
});
|
|
54
|
+
it("getMaxSeq returns 0 for empty narrative", () => {
|
|
55
|
+
const db = createTestDb();
|
|
56
|
+
assert.equal(getMaxSeq(db, "nonexistent"), 0);
|
|
57
|
+
});
|
|
58
|
+
it("getMaxSeq returns highest seq", () => {
|
|
59
|
+
const db = createTestDb();
|
|
60
|
+
appendEvent(db, "n1", "narration", { content: "one" });
|
|
61
|
+
appendEvent(db, "n1", "narration", { content: "two" });
|
|
62
|
+
appendEvent(db, "n1", "narration", { content: "three" });
|
|
63
|
+
assert.equal(getMaxSeq(db, "n1"), 3);
|
|
64
|
+
});
|
|
65
|
+
it("stores and retrieves back-references as parsed arrays", () => {
|
|
66
|
+
const db = createTestDb();
|
|
67
|
+
const e = appendEvent(db, "n1", "narration", {
|
|
68
|
+
content: "reply",
|
|
69
|
+
re: [1, "other-narrative:5"],
|
|
70
|
+
});
|
|
71
|
+
assert.deepEqual(e.re, [1, "other-narrative:5"]);
|
|
72
|
+
const events = readEventsSince(db, "n1", 0);
|
|
73
|
+
assert.deepEqual(events[0].re, [1, "other-narrative:5"]);
|
|
74
|
+
});
|
|
75
|
+
it("source field works for all event types", () => {
|
|
76
|
+
const db = createTestDb();
|
|
77
|
+
const opSignal = appendEvent(db, "n1", "human_signal", {
|
|
78
|
+
content: "incoming",
|
|
79
|
+
source: "mobile",
|
|
80
|
+
});
|
|
81
|
+
assert.equal(opSignal.source, "mobile");
|
|
82
|
+
assert.equal(opSignal.type, "human_signal");
|
|
83
|
+
const agentMsg = appendEvent(db, "n1", "agent_signal", {
|
|
84
|
+
content: "delegation result",
|
|
85
|
+
source: "claude-abc",
|
|
86
|
+
});
|
|
87
|
+
assert.equal(agentMsg.source, "claude-abc");
|
|
88
|
+
assert.equal(agentMsg.type, "agent_signal");
|
|
89
|
+
const narration = appendEvent(db, "n1", "narration", {
|
|
90
|
+
content: "thinking",
|
|
91
|
+
source: "claude-xyz",
|
|
92
|
+
});
|
|
93
|
+
assert.equal(narration.source, "claude-xyz");
|
|
94
|
+
assert.equal(narration.type, "narration");
|
|
95
|
+
});
|
|
96
|
+
it("stores payloads as parsed arrays", () => {
|
|
97
|
+
const db = createTestDb();
|
|
98
|
+
const e = appendEvent(db, "n1", "narration", {
|
|
99
|
+
content: "with files",
|
|
100
|
+
payloads: ["path/to/file.png", "path/to/doc.md"],
|
|
101
|
+
});
|
|
102
|
+
assert.deepEqual(e.payloads, ["path/to/file.png", "path/to/doc.md"]);
|
|
103
|
+
});
|
|
104
|
+
it("omits undefined optional fields", () => {
|
|
105
|
+
const db = createTestDb();
|
|
106
|
+
const e = appendEvent(db, "n1", "narration", { content: "minimal" });
|
|
107
|
+
assert.equal(e.seq, 1);
|
|
108
|
+
assert.equal(typeof e.time, "number");
|
|
109
|
+
assert.equal(e.type, "narration");
|
|
110
|
+
assert.equal(e.content, "minimal");
|
|
111
|
+
assert.equal(e.content_type, undefined);
|
|
112
|
+
assert.equal(e.source, undefined);
|
|
113
|
+
assert.equal(e.re, undefined);
|
|
114
|
+
assert.equal(e.payloads, undefined);
|
|
115
|
+
assert.equal("narrative_id" in e, false);
|
|
116
|
+
assert.equal("event_type" in e, false);
|
|
117
|
+
assert.equal("timestamp_ms" in e, false);
|
|
118
|
+
assert.equal("sender" in e, false);
|
|
119
|
+
assert.equal("speaker" in e, false);
|
|
120
|
+
assert.equal("intensity" in e, false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
//# sourceMappingURL=events.test.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { formatSeqModeParam, formatSeqParam, parseSeqModeParam, parseSeqParam, exportFileName, seqDigest, } from "../shared/export-selection.js";
|
|
4
|
+
describe("formatSeqParam", () => {
|
|
5
|
+
it("writes ascending, deduped decimal seqs", () => {
|
|
6
|
+
assert.equal(formatSeqParam([19, 12, 14, 12]), "12,14,19");
|
|
7
|
+
});
|
|
8
|
+
it("is null when there is nothing to send", () => {
|
|
9
|
+
assert.equal(formatSeqParam([]), null);
|
|
10
|
+
assert.equal(formatSeqParam(null), null);
|
|
11
|
+
assert.equal(formatSeqParam(undefined), null);
|
|
12
|
+
});
|
|
13
|
+
it("drops values that are not seqs", () => {
|
|
14
|
+
// seq numbering starts at 1, so 0 and negatives name nothing.
|
|
15
|
+
assert.equal(formatSeqParam([0, -3, 1.5, 7]), "7");
|
|
16
|
+
assert.equal(formatSeqParam([0]), null);
|
|
17
|
+
});
|
|
18
|
+
it("takes a Set, which is the shape the chat keeps its selection in", () => {
|
|
19
|
+
assert.equal(formatSeqParam(new Set([3, 1, 2])), "1,2,3");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("parseSeqParam", () => {
|
|
23
|
+
it("reads what formatSeqParam writes", () => {
|
|
24
|
+
const seqs = [4, 17, 23];
|
|
25
|
+
assert.deepEqual(parseSeqParam(formatSeqParam(seqs)), seqs);
|
|
26
|
+
});
|
|
27
|
+
it("is empty for an absent or blank parameter", () => {
|
|
28
|
+
assert.deepEqual(parseSeqParam(null), []);
|
|
29
|
+
assert.deepEqual(parseSeqParam(undefined), []);
|
|
30
|
+
assert.deepEqual(parseSeqParam(""), []);
|
|
31
|
+
assert.deepEqual(parseSeqParam(",,"), []);
|
|
32
|
+
});
|
|
33
|
+
it("ignores junk rather than refusing the export", () => {
|
|
34
|
+
assert.deepEqual(parseSeqParam("3,abc,5"), [3, 5]);
|
|
35
|
+
assert.deepEqual(parseSeqParam("12abc"), [], "a partial number is not a seq");
|
|
36
|
+
assert.deepEqual(parseSeqParam("-1,0,2.5,2"), [2]);
|
|
37
|
+
assert.deepEqual(parseSeqParam("nonsense"), [], "unreadable means no selection, which means a full export");
|
|
38
|
+
});
|
|
39
|
+
it("sorts and dedupes whatever order it arrives in", () => {
|
|
40
|
+
assert.deepEqual(parseSeqParam("9, 2 ,9,2,5"), [2, 5, 9]);
|
|
41
|
+
});
|
|
42
|
+
it("reads plain decimal digits only — `Number` accepts more than a seq", () => {
|
|
43
|
+
// Each of these is a number to `Number()` and a message nobody picked.
|
|
44
|
+
assert.deepEqual(parseSeqParam("1e2"), []);
|
|
45
|
+
assert.deepEqual(parseSeqParam("0x10"), []);
|
|
46
|
+
assert.deepEqual(parseSeqParam("+4"), []);
|
|
47
|
+
assert.deepEqual(parseSeqParam("5.0"), []);
|
|
48
|
+
assert.deepEqual(parseSeqParam("0b11,0o7"), []);
|
|
49
|
+
assert.deepEqual(parseSeqParam("9007199254740993"), [], "past MAX_SAFE_INTEGER, Number() rounds to a different seq");
|
|
50
|
+
assert.deepEqual(parseSeqParam("007"), [7], "leading zeros are still decimal");
|
|
51
|
+
assert.deepEqual(parseSeqParam("9007199254740991"), [9007199254740991], "MAX_SAFE_INTEGER itself is a seq");
|
|
52
|
+
assert.deepEqual(parseSeqParam("1e2,3"), [3], "a rejected part drops alone; the rest of the list stands");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("exportFileName", () => {
|
|
56
|
+
it("a full export keeps the name sibling exports link to", () => {
|
|
57
|
+
assert.equal(exportFileName("claude-aaaaaaaa"), "claude-aaaaaaaa.html");
|
|
58
|
+
assert.equal(exportFileName("claude-aaaaaaaa", null), "claude-aaaaaaaa.html");
|
|
59
|
+
});
|
|
60
|
+
it("an excerpt carries its count and a digest of its seqs", () => {
|
|
61
|
+
assert.match(exportFileName("claude-aaaaaaaa", { seqs: [2, 5, 9], total: 47 }), /^claude-aaaaaaaa\.3-of-47\.[0-9a-f]{6}\.html$/);
|
|
62
|
+
});
|
|
63
|
+
it("two excerpts of the same size are two names — `only` and `except` of a half-selection included", () => {
|
|
64
|
+
// seqs [1,4] of four messages: `only` exports 1,4 and `except` exports 2,3.
|
|
65
|
+
// Both are 2-of-4; the count alone gave them one name.
|
|
66
|
+
const only = exportFileName("claude-aaaaaaaa", { seqs: [1, 4], total: 4 });
|
|
67
|
+
const except = exportFileName("claude-aaaaaaaa", { seqs: [2, 3], total: 4 });
|
|
68
|
+
assert.notEqual(only, except);
|
|
69
|
+
});
|
|
70
|
+
it("the digest depends on the set, not the order it was tapped in", () => {
|
|
71
|
+
assert.equal(seqDigest([9, 2, 5]), seqDigest([2, 5, 9]));
|
|
72
|
+
assert.notEqual(seqDigest([1, 23]), seqDigest([12, 3]), "the separator keeps 1,23 and 12,3 apart");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe("the selection's mode", () => {
|
|
76
|
+
it("reads what it writes", () => {
|
|
77
|
+
assert.equal(parseSeqModeParam(formatSeqModeParam("except")), "except");
|
|
78
|
+
assert.equal(parseSeqModeParam(formatSeqModeParam("only")), "only");
|
|
79
|
+
});
|
|
80
|
+
it("leaves the default off the URL", () => {
|
|
81
|
+
// An ordinary excerpt's link stays the one brane built before a complement
|
|
82
|
+
// was possible — the parameter appears only when it changes something.
|
|
83
|
+
assert.equal(formatSeqModeParam("only"), null);
|
|
84
|
+
assert.equal(formatSeqModeParam(null), null);
|
|
85
|
+
assert.equal(formatSeqModeParam(undefined), null);
|
|
86
|
+
assert.equal(formatSeqModeParam("except"), "except");
|
|
87
|
+
});
|
|
88
|
+
it("inverts an export only on the exact word", () => {
|
|
89
|
+
// The tolerance runs one way on purpose: a garbled parameter must never
|
|
90
|
+
// hand the operator the complement of the document they asked for.
|
|
91
|
+
assert.equal(parseSeqModeParam("except"), "except");
|
|
92
|
+
assert.equal(parseSeqModeParam(" except "), "except");
|
|
93
|
+
for (const junk of ["Except", "EXCEPT", "exclude", "excluded", "!", "1", "true", "", " "]) {
|
|
94
|
+
assert.equal(parseSeqModeParam(junk), "only", `${JSON.stringify(junk)} is not "except"`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
it("is `only` when absent, which is what every export was", () => {
|
|
98
|
+
assert.equal(parseSeqModeParam(null), "only");
|
|
99
|
+
assert.equal(parseSeqModeParam(undefined), "only");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=export-selection.test.js.map
|