brane 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +362 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +91 -0
- package/DESIGN.md +402 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +205 -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 +116 -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 +332 -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 +1094 -0
- package/dist/commands/token.js +153 -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 +6349 -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 +633 -0
- package/dist/db/tokens.js +583 -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/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 +82 -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 +197 -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/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 +49 -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 +262 -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 +85 -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 +137 -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 +115 -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/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +587 -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 +581 -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/tls.js +148 -0
- package/dist/tmux.js +202 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +141 -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 +53 -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 +211 -0
- package/docs/token-rotation.md +413 -0
- package/docs/view-urls.md +114 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -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 +48 -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,220 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createTestDb } from "./helpers.js";
|
|
4
|
+
import { appendEvent, readEvents } from "../db/events.js";
|
|
5
|
+
/**
|
|
6
|
+
* Populate a narrative with N events labeled msg-0 through msg-(N-1).
|
|
7
|
+
* Returns the seq of the last event inserted.
|
|
8
|
+
*/
|
|
9
|
+
function populate(db, nid, count) {
|
|
10
|
+
let lastSeq = 0;
|
|
11
|
+
for (let i = 0; i < count; i++) {
|
|
12
|
+
const e = appendEvent(db, nid, "narration", { content: `msg-${i}` });
|
|
13
|
+
lastSeq = e.seq;
|
|
14
|
+
}
|
|
15
|
+
return lastSeq;
|
|
16
|
+
}
|
|
17
|
+
describe("readEvents", () => {
|
|
18
|
+
// ── after (replaces heard) ──────────────────────────────────────
|
|
19
|
+
describe("after", () => {
|
|
20
|
+
it("returns events after a cursor (like heard)", () => {
|
|
21
|
+
const db = createTestDb();
|
|
22
|
+
populate(db, "n1", 5);
|
|
23
|
+
const events = readEvents(db, "n1", { after: 2 });
|
|
24
|
+
assert.equal(events.length, 3);
|
|
25
|
+
assert.equal(events[0].content, "msg-2"); // seq 3
|
|
26
|
+
assert.equal(events[2].content, "msg-4"); // seq 5
|
|
27
|
+
});
|
|
28
|
+
it("after: 0 returns all events", () => {
|
|
29
|
+
const db = createTestDb();
|
|
30
|
+
populate(db, "n1", 3);
|
|
31
|
+
const events = readEvents(db, "n1", { after: 0 });
|
|
32
|
+
assert.equal(events.length, 3);
|
|
33
|
+
});
|
|
34
|
+
it("after beyond max seq returns empty", () => {
|
|
35
|
+
const db = createTestDb();
|
|
36
|
+
populate(db, "n1", 3);
|
|
37
|
+
const events = readEvents(db, "n1", { after: 100 });
|
|
38
|
+
assert.equal(events.length, 0);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
// ── before ──────────────────────────────────────────────────────
|
|
42
|
+
describe("before", () => {
|
|
43
|
+
it("returns events before a cursor", () => {
|
|
44
|
+
const db = createTestDb();
|
|
45
|
+
populate(db, "n1", 5);
|
|
46
|
+
const events = readEvents(db, "n1", { before: 4 });
|
|
47
|
+
assert.equal(events.length, 3); // seq 1, 2, 3
|
|
48
|
+
assert.equal(events[0].content, "msg-0");
|
|
49
|
+
assert.equal(events[2].content, "msg-2");
|
|
50
|
+
});
|
|
51
|
+
it("before: 1 returns empty (nothing before seq 1)", () => {
|
|
52
|
+
const db = createTestDb();
|
|
53
|
+
populate(db, "n1", 3);
|
|
54
|
+
const events = readEvents(db, "n1", { before: 1 });
|
|
55
|
+
assert.equal(events.length, 0);
|
|
56
|
+
});
|
|
57
|
+
it("before beyond max seq returns all events", () => {
|
|
58
|
+
const db = createTestDb();
|
|
59
|
+
populate(db, "n1", 3);
|
|
60
|
+
const events = readEvents(db, "n1", { before: 100 });
|
|
61
|
+
assert.equal(events.length, 3);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
// ── after + before (bounded range) ─────────────────────────────
|
|
65
|
+
describe("after + before", () => {
|
|
66
|
+
it("returns events in a bounded range", () => {
|
|
67
|
+
const db = createTestDb();
|
|
68
|
+
populate(db, "n1", 10);
|
|
69
|
+
const events = readEvents(db, "n1", { after: 3, before: 7 });
|
|
70
|
+
assert.equal(events.length, 3); // seq 4, 5, 6
|
|
71
|
+
assert.equal(events[0].content, "msg-3");
|
|
72
|
+
assert.equal(events[2].content, "msg-5");
|
|
73
|
+
});
|
|
74
|
+
it("empty range returns empty", () => {
|
|
75
|
+
const db = createTestDb();
|
|
76
|
+
populate(db, "n1", 10);
|
|
77
|
+
// after: 5, before: 6 → only seq > 5 AND seq < 6 → nothing
|
|
78
|
+
const events = readEvents(db, "n1", { after: 5, before: 6 });
|
|
79
|
+
assert.equal(events.length, 0);
|
|
80
|
+
});
|
|
81
|
+
it("adjacent cursors return single event", () => {
|
|
82
|
+
const db = createTestDb();
|
|
83
|
+
populate(db, "n1", 10);
|
|
84
|
+
// after: 4, before: 6 → seq 5 only
|
|
85
|
+
const events = readEvents(db, "n1", { after: 4, before: 6 });
|
|
86
|
+
assert.equal(events.length, 1);
|
|
87
|
+
assert.equal(events[0].content, "msg-4");
|
|
88
|
+
});
|
|
89
|
+
it("inverted range (after >= before) returns empty", () => {
|
|
90
|
+
const db = createTestDb();
|
|
91
|
+
populate(db, "n1", 5);
|
|
92
|
+
const events = readEvents(db, "n1", { after: 5, before: 3 });
|
|
93
|
+
assert.equal(events.length, 0);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
// ── tail ────────────────────────────────────────────────────────
|
|
97
|
+
describe("tail", () => {
|
|
98
|
+
it("returns the last N events", () => {
|
|
99
|
+
const db = createTestDb();
|
|
100
|
+
populate(db, "n1", 10);
|
|
101
|
+
const events = readEvents(db, "n1", { tail: 3 });
|
|
102
|
+
assert.equal(events.length, 3);
|
|
103
|
+
assert.equal(events[0].content, "msg-7"); // seq 8
|
|
104
|
+
assert.equal(events[2].content, "msg-9"); // seq 10
|
|
105
|
+
});
|
|
106
|
+
it("tail larger than total returns all events", () => {
|
|
107
|
+
const db = createTestDb();
|
|
108
|
+
populate(db, "n1", 3);
|
|
109
|
+
const events = readEvents(db, "n1", { tail: 100 });
|
|
110
|
+
assert.equal(events.length, 3);
|
|
111
|
+
});
|
|
112
|
+
it("tail: 1 returns only the last event", () => {
|
|
113
|
+
const db = createTestDb();
|
|
114
|
+
populate(db, "n1", 5);
|
|
115
|
+
const events = readEvents(db, "n1", { tail: 1 });
|
|
116
|
+
assert.equal(events.length, 1);
|
|
117
|
+
assert.equal(events[0].content, "msg-4");
|
|
118
|
+
});
|
|
119
|
+
it("tail: 0 returns empty", () => {
|
|
120
|
+
const db = createTestDb();
|
|
121
|
+
populate(db, "n1", 5);
|
|
122
|
+
const events = readEvents(db, "n1", { tail: 0 });
|
|
123
|
+
assert.equal(events.length, 0);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
// ── tail + before (backward pagination) ────────────────────────
|
|
127
|
+
describe("tail + before", () => {
|
|
128
|
+
it("returns last N events before a cursor", () => {
|
|
129
|
+
const db = createTestDb();
|
|
130
|
+
populate(db, "n1", 10);
|
|
131
|
+
// Last 3 before seq 8 → seq 5, 6, 7
|
|
132
|
+
const events = readEvents(db, "n1", { tail: 3, before: 8 });
|
|
133
|
+
assert.equal(events.length, 3);
|
|
134
|
+
assert.equal(events[0].content, "msg-4"); // seq 5
|
|
135
|
+
assert.equal(events[2].content, "msg-6"); // seq 7
|
|
136
|
+
});
|
|
137
|
+
it("backward pagination walks through entire history", () => {
|
|
138
|
+
const db = createTestDb();
|
|
139
|
+
populate(db, "n1", 10);
|
|
140
|
+
// Page 1: last 3 → seq 8, 9, 10
|
|
141
|
+
const page1 = readEvents(db, "n1", { tail: 3 });
|
|
142
|
+
assert.equal(page1.length, 3);
|
|
143
|
+
assert.equal(page1[0].seq, 8);
|
|
144
|
+
// Page 2: last 3 before seq 8 → seq 5, 6, 7
|
|
145
|
+
const page2 = readEvents(db, "n1", { tail: 3, before: page1[0].seq });
|
|
146
|
+
assert.equal(page2.length, 3);
|
|
147
|
+
assert.equal(page2[0].seq, 5);
|
|
148
|
+
// Page 3: last 3 before seq 5 → seq 2, 3, 4
|
|
149
|
+
const page3 = readEvents(db, "n1", { tail: 3, before: page2[0].seq });
|
|
150
|
+
assert.equal(page3.length, 3);
|
|
151
|
+
assert.equal(page3[0].seq, 2);
|
|
152
|
+
// Page 4: last 3 before seq 2 → seq 1 (partial page)
|
|
153
|
+
const page4 = readEvents(db, "n1", { tail: 3, before: page3[0].seq });
|
|
154
|
+
assert.equal(page4.length, 1);
|
|
155
|
+
assert.equal(page4[0].seq, 1);
|
|
156
|
+
// Page 5: before seq 1 → empty (done)
|
|
157
|
+
const page5 = readEvents(db, "n1", { tail: 3, before: page4[0].seq });
|
|
158
|
+
assert.equal(page5.length, 0);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
// ── after + tail (last N after a cursor) ───────────────────────
|
|
162
|
+
describe("after + tail", () => {
|
|
163
|
+
it("returns the last N events after a cursor", () => {
|
|
164
|
+
const db = createTestDb();
|
|
165
|
+
populate(db, "n1", 10);
|
|
166
|
+
// Events after seq 2, but only the last 3 → seq 8, 9, 10
|
|
167
|
+
const events = readEvents(db, "n1", { after: 2, tail: 3 });
|
|
168
|
+
assert.equal(events.length, 3);
|
|
169
|
+
assert.equal(events[0].content, "msg-7"); // seq 8
|
|
170
|
+
assert.equal(events[2].content, "msg-9"); // seq 10
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// ── after + before + tail (bounded + limited) ──────────────────
|
|
174
|
+
describe("after + before + tail", () => {
|
|
175
|
+
it("returns last N events within a bounded range", () => {
|
|
176
|
+
const db = createTestDb();
|
|
177
|
+
populate(db, "n1", 20);
|
|
178
|
+
// Range: seq 6-14 (after 5, before 15), tail 3 → seq 12, 13, 14
|
|
179
|
+
const events = readEvents(db, "n1", { after: 5, before: 15, tail: 3 });
|
|
180
|
+
assert.equal(events.length, 3);
|
|
181
|
+
assert.equal(events[0].seq, 12);
|
|
182
|
+
assert.equal(events[2].seq, 14);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
// ── no parameters ──────────────────────────────────────────────
|
|
186
|
+
describe("defaults", () => {
|
|
187
|
+
it("no parameters returns all events (like after: 0)", () => {
|
|
188
|
+
const db = createTestDb();
|
|
189
|
+
populate(db, "n1", 5);
|
|
190
|
+
const events = readEvents(db, "n1", {});
|
|
191
|
+
assert.equal(events.length, 5);
|
|
192
|
+
});
|
|
193
|
+
it("empty narrative returns empty", () => {
|
|
194
|
+
const db = createTestDb();
|
|
195
|
+
const events = readEvents(db, "nonexistent", {});
|
|
196
|
+
assert.equal(events.length, 0);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
// ── ordering ───────────────────────────────────────────────────
|
|
200
|
+
describe("ordering", () => {
|
|
201
|
+
it("always returns events in ascending seq order", () => {
|
|
202
|
+
const db = createTestDb();
|
|
203
|
+
populate(db, "n1", 10);
|
|
204
|
+
// tail queries internally sort DESC then reverse
|
|
205
|
+
const events = readEvents(db, "n1", { tail: 5 });
|
|
206
|
+
for (let i = 1; i < events.length; i++) {
|
|
207
|
+
assert.ok(events[i].seq > events[i - 1].seq, `seq ${events[i].seq} should be > ${events[i - 1].seq}`);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
it("after+before returns in ascending order", () => {
|
|
211
|
+
const db = createTestDb();
|
|
212
|
+
populate(db, "n1", 10);
|
|
213
|
+
const events = readEvents(db, "n1", { after: 2, before: 8 });
|
|
214
|
+
for (let i = 1; i < events.length; i++) {
|
|
215
|
+
assert.ok(events[i].seq > events[i - 1].seq);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
//# sourceMappingURL=read-events.test.js.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `brane meld --remote`: a remote machine (Fly, a VPS) cannot show a
|
|
3
|
+
* presence prompt, so the device token is minted on the operator's machine
|
|
4
|
+
* and the remote gets only its hash (`BRANE_DEVICE_TOKEN_SHA256`), adopted as
|
|
5
|
+
* its daemon starts. The remote sends back only its address (`brane meld
|
|
6
|
+
* --connection`).
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdtempSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { createTestDb } from "./helpers.js";
|
|
14
|
+
import { bootstrapTokens, DbTokenStore, REMOTE_MELD_LABEL, parseTokenHashes } from "../db/tokens.js";
|
|
15
|
+
import { TokenRing, UNUSED_TOKEN_LAPSE_MS, hashToken, mintRemoteToken } from "../daemon/token-ring.js";
|
|
16
|
+
import { buildMeldUrl, fillHash, meldLink, readMeldReply, refusesRemoteMeld } from "../commands/meld.js";
|
|
17
|
+
const T0 = Date.UTC(2026, 9, 5, 12);
|
|
18
|
+
function setup() {
|
|
19
|
+
const db = createTestDb();
|
|
20
|
+
const tokenFile = join(mkdtempSync(join(tmpdir(), "brane-remote-meld-")), ".daemon_token");
|
|
21
|
+
let n = 0;
|
|
22
|
+
return { db, tokenFile, mint: () => `local-${++n}` };
|
|
23
|
+
}
|
|
24
|
+
describe("the remote token's hash", () => {
|
|
25
|
+
it("is the SHA-256 the daemon checks tokens by", () => {
|
|
26
|
+
const { token, hash } = mintRemoteToken();
|
|
27
|
+
assert.equal(hash, hashToken(token));
|
|
28
|
+
assert.match(hash, /^[0-9a-f]{64}$/);
|
|
29
|
+
assert.ok(token.length >= 40, "32 random bytes");
|
|
30
|
+
assert.notEqual(mintRemoteToken().token, token);
|
|
31
|
+
});
|
|
32
|
+
it("goes into the --send command wherever it says {hash}, and nowhere else", () => {
|
|
33
|
+
const h = hashToken("t");
|
|
34
|
+
assert.equal(fillHash("fly secrets set BRANE_DEVICE_TOKEN_SHA256={hash} -a x", h), `fly secrets set BRANE_DEVICE_TOKEN_SHA256=${h} -a x`);
|
|
35
|
+
assert.equal(fillHash("a={hash} b={hash}", h), `a=${h} b=${h}`);
|
|
36
|
+
assert.throws(() => fillHash("fly secrets set BRANE_DEVICE_TOKEN_SHA256=oops", h), /\{hash\}/);
|
|
37
|
+
});
|
|
38
|
+
it("is read from the secret strictly, dropping anything malformed", () => {
|
|
39
|
+
const h = hashToken("t");
|
|
40
|
+
assert.deepEqual(parseTokenHashes(undefined), []);
|
|
41
|
+
assert.deepEqual(parseTokenHashes(""), []);
|
|
42
|
+
assert.deepEqual(parseTokenHashes(` ${h.toUpperCase()} , not-a-hash, ${h}`), [h]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("adopting a remote hash at startup", () => {
|
|
46
|
+
it("makes the token on the operator's machine a working device token, and the hash only", () => {
|
|
47
|
+
const { db, tokenFile, mint } = setup();
|
|
48
|
+
const { token, hash } = mintRemoteToken();
|
|
49
|
+
const boot = bootstrapTokens(db, { tokenFile, mint, configuredHashes: [hash], now: T0 });
|
|
50
|
+
assert.deepEqual(boot.adopted, [REMOTE_MELD_LABEL]);
|
|
51
|
+
const ring = new TokenRing(new DbTokenStore(db), () => "unused");
|
|
52
|
+
const auth = ring.authenticate(token, T0 + 1);
|
|
53
|
+
assert.equal(auth.ok && auth.record.kind, "device");
|
|
54
|
+
assert.equal(auth.ok && auth.record.label, REMOTE_MELD_LABEL);
|
|
55
|
+
assert.ok(!JSON.stringify(db.prepare("SELECT * FROM tokens").all()).includes(token));
|
|
56
|
+
});
|
|
57
|
+
it("adopts a hash once: the secret stays set across restarts, and a revoked one stays revoked", () => {
|
|
58
|
+
const { db, tokenFile, mint } = setup();
|
|
59
|
+
const { token, hash } = mintRemoteToken();
|
|
60
|
+
bootstrapTokens(db, { tokenFile, mint, configuredHashes: [hash], now: T0 });
|
|
61
|
+
const store = new DbTokenStore(db);
|
|
62
|
+
const row = store.live("device").find((r) => r.label === REMOTE_MELD_LABEL);
|
|
63
|
+
store.revoke([row.id], T0 + 1);
|
|
64
|
+
const again = bootstrapTokens(db, { tokenFile, mint, configuredHashes: [hash], now: T0 + 2 });
|
|
65
|
+
assert.deepEqual(again.adopted, []);
|
|
66
|
+
const ring = new TokenRing(store, () => "unused");
|
|
67
|
+
assert.equal(ring.authenticate(token, T0 + 3).ok, false);
|
|
68
|
+
});
|
|
69
|
+
it("lapses like any QR if nobody scans it", () => {
|
|
70
|
+
const { db, tokenFile, mint } = setup();
|
|
71
|
+
const { token, hash } = mintRemoteToken();
|
|
72
|
+
bootstrapTokens(db, { tokenFile, mint, configuredHashes: [hash], now: T0 });
|
|
73
|
+
const ring = new TokenRing(new DbTokenStore(db), () => "unused");
|
|
74
|
+
assert.equal(ring.authenticate(token, T0 + UNUSED_TOKEN_LAPSE_MS + 1).ok, false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("the remote daemon's address", () => {
|
|
78
|
+
const c = { daemonHost: "daemon-x.tail.ts.net:8443", scheme: "https", useLocal: false, isLoopbackFallback: false, tlsDegraded: false };
|
|
79
|
+
it("is found among whatever else the --ask command printed", () => {
|
|
80
|
+
const out = `Connecting to fdaa::3... complete\n${JSON.stringify({ ready: true, ...c })}\r\n`;
|
|
81
|
+
assert.deepEqual(readMeldReply(out), { kind: "address", connection: c });
|
|
82
|
+
});
|
|
83
|
+
it("is waited for while the machine restarts, its daemon starts, or Tailscale has no name yet", () => {
|
|
84
|
+
assert.equal(readMeldReply(null).kind, "wait");
|
|
85
|
+
assert.deepEqual(readMeldReply('{"ready":false,"reason":"no daemon is running yet"}\n'), {
|
|
86
|
+
kind: "wait",
|
|
87
|
+
reason: "no daemon is running yet",
|
|
88
|
+
});
|
|
89
|
+
// A QR for 127.0.0.1 would be useless, and a wrong one costs a restart.
|
|
90
|
+
const loopback = readMeldReply(JSON.stringify({ ready: true, ...c, daemonHost: "127.0.0.1:8443", isLoopbackFallback: true }));
|
|
91
|
+
assert.deepEqual(loopback, { kind: "wait", reason: "the remote machine has no Tailscale name yet" });
|
|
92
|
+
});
|
|
93
|
+
it("calls an answer with no JSON line an older brane, which ran a plain meld", () => {
|
|
94
|
+
assert.deepEqual(readMeldReply("\nScan this QR code to meld:\n█▀▀▀▀▀█\n"), { kind: "old" });
|
|
95
|
+
assert.deepEqual(readMeldReply('{"daemonHost": 1}'), { kind: "old" });
|
|
96
|
+
});
|
|
97
|
+
it("makes the same URL brane meld would have, with the token minted elsewhere", () => {
|
|
98
|
+
const db = createTestDb();
|
|
99
|
+
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('daemon_tls', 'true')").run();
|
|
100
|
+
for (const local of [false, true]) {
|
|
101
|
+
const full = buildMeldUrl({ db, daemon: { port: "8443", token: "tok" }, tailscaleHost: "h.ts.net", local });
|
|
102
|
+
const { url: _u, ...connection } = buildMeldUrl({ db, daemon: { port: "8443", token: "" }, tailscaleHost: "h.ts.net", local });
|
|
103
|
+
assert.equal(meldLink(connection, "tok"), full.url);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe("refusesRemoteMeld", () => {
|
|
108
|
+
it("refuses in an agent's session, which would read the printed token", () => {
|
|
109
|
+
assert.match(refusesRemoteMeld({ BRANE_NARRATIVE_SECRET: "x" }) ?? "", /outside an agent session/);
|
|
110
|
+
});
|
|
111
|
+
it("runs from the operator's shell", () => {
|
|
112
|
+
assert.equal(refusesRemoteMeld({}), null);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=remote-meld.test.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, before, after } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { buildReviveBody, formatReviveResult } from "../commands/revive.js";
|
|
5
|
+
import { daemonRequest } from "../daemon/client.js";
|
|
6
|
+
describe("buildReviveBody", () => {
|
|
7
|
+
it("sends nothing at all when no flags are given", () => {
|
|
8
|
+
// The daemon's defaults (LIVE-loop prompt, same agent) must stay in
|
|
9
|
+
// charge of everything the operator didn't ask about.
|
|
10
|
+
assert.deepEqual(buildReviveBody({}), {});
|
|
11
|
+
});
|
|
12
|
+
it("maps --no-prompt onto the API's snake_case no_prompt", () => {
|
|
13
|
+
assert.deepEqual(buildReviveBody({ "no-prompt": true }), { no_prompt: true });
|
|
14
|
+
});
|
|
15
|
+
it("omits no_prompt when the flag is absent rather than sending false", () => {
|
|
16
|
+
assert.deepEqual(buildReviveBody({ prompt: "carry on" }), { prompt: "carry on" });
|
|
17
|
+
});
|
|
18
|
+
it("passes --agent through for the agent-switch path", () => {
|
|
19
|
+
assert.deepEqual(buildReviveBody({ agent: "goose", prompt: "hi" }), {
|
|
20
|
+
agent: "goose",
|
|
21
|
+
prompt: "hi",
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe("formatReviveResult", () => {
|
|
26
|
+
it("names the revived session and its agent", () => {
|
|
27
|
+
assert.equal(formatReviveResult("claude-abc12345", {
|
|
28
|
+
status: "revived",
|
|
29
|
+
nid: "claude-abc12345",
|
|
30
|
+
agent: "claude",
|
|
31
|
+
}), "Revived claude-abc12345 (claude)");
|
|
32
|
+
});
|
|
33
|
+
it("reports an already-running session without claiming to have restarted it", () => {
|
|
34
|
+
assert.equal(formatReviveResult("claude-abc12345", {
|
|
35
|
+
status: "already_running",
|
|
36
|
+
nid: "claude-abc12345",
|
|
37
|
+
agent: "claude",
|
|
38
|
+
}), "Session claude-abc12345 is already running.");
|
|
39
|
+
});
|
|
40
|
+
it("names the twin, not the narrative it continues from", () => {
|
|
41
|
+
// The distinction the old in-process CLI could not make: an absorbed or
|
|
42
|
+
// agent-switched revive spawns a *new* narrative, and printing the
|
|
43
|
+
// requested id would point the operator at the wrong pane.
|
|
44
|
+
assert.equal(formatReviveResult("chatgpt-0f9e8d7c", {
|
|
45
|
+
status: "continued",
|
|
46
|
+
nid: "goose-11223344",
|
|
47
|
+
agent: "goose",
|
|
48
|
+
}), "Continued chatgpt-0f9e8d7c as goose-11223344 (goose)");
|
|
49
|
+
});
|
|
50
|
+
it("degrades quietly when an older daemon doesn't report the agent", () => {
|
|
51
|
+
assert.equal(formatReviveResult("claude-abc12345", { status: "revived", nid: "claude-abc12345" }), "Revived claude-abc12345");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("revive over daemonRequest", () => {
|
|
55
|
+
let server;
|
|
56
|
+
let endpoint;
|
|
57
|
+
const seen = [];
|
|
58
|
+
before(async () => {
|
|
59
|
+
server = createServer((req, res) => {
|
|
60
|
+
let body = "";
|
|
61
|
+
req.on("data", (c) => (body += c));
|
|
62
|
+
req.on("end", () => {
|
|
63
|
+
seen.push({ method: req.method ?? "", url: req.url ?? "", body });
|
|
64
|
+
if (req.url?.startsWith("/api/revive/claude-abc12345")) {
|
|
65
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
66
|
+
res.end(JSON.stringify({ status: "revived", nid: "claude-abc12345", agent: "claude" }));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
70
|
+
res.end(JSON.stringify({ error: "Unknown narrative: nope" }));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
75
|
+
const port = server.address().port;
|
|
76
|
+
endpoint = { baseUrl: `http://127.0.0.1:${port}`, token: "tok", port: String(port) };
|
|
77
|
+
});
|
|
78
|
+
after(async () => {
|
|
79
|
+
await new Promise((r) => server.close(() => r()));
|
|
80
|
+
});
|
|
81
|
+
it("POSTs the nid in the path and the flags in the body", async () => {
|
|
82
|
+
const result = await daemonRequest(endpoint, `/api/revive/${encodeURIComponent("claude-abc12345")}`, { method: "POST", body: buildReviveBody({ prompt: "resume the audit" }) });
|
|
83
|
+
assert.equal(result.status, "revived");
|
|
84
|
+
assert.equal(result.agent, "claude");
|
|
85
|
+
const req = seen.find((r) => r.url === "/api/revive/claude-abc12345");
|
|
86
|
+
assert.equal(req?.method, "POST");
|
|
87
|
+
assert.deepEqual(JSON.parse(req?.body ?? "{}"), { prompt: "resume the audit" });
|
|
88
|
+
});
|
|
89
|
+
it("surfaces the daemon's unknown-narrative error verbatim", async () => {
|
|
90
|
+
await assert.rejects(() => daemonRequest(endpoint, "/api/revive/nope", { method: "POST", body: {} }), /^Error: Unknown narrative: nope$/);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
//# sourceMappingURL=revive-cli.test.js.map
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createTestDb } from "./helpers.js";
|
|
4
|
+
import { MIN_SEMANTIC_SCORE, initMemorySchema, searchHybrid, upsertChunks, } from "../memory/storage.js";
|
|
5
|
+
// Synthetic vectors throughout — no embedding model. The floor is a numeric
|
|
6
|
+
// boundary, and pinning it needs similarities chosen to the second decimal,
|
|
7
|
+
// which is exactly what a real embedder won't give you twice. (The
|
|
8
|
+
// model-backed end-to-end path stays covered by search.test.ts.)
|
|
9
|
+
const DIM = 384;
|
|
10
|
+
/** A unit vector along `axis`. Distinct axes are orthogonal, so cosine
|
|
11
|
+
* similarity between them is 0; blending toward the query axis dials
|
|
12
|
+
* similarity to any value we want. */
|
|
13
|
+
function axisVector(axis) {
|
|
14
|
+
const v = new Float32Array(DIM);
|
|
15
|
+
v[axis] = 1;
|
|
16
|
+
return v;
|
|
17
|
+
}
|
|
18
|
+
/** A unit vector whose cosine similarity to `axisVector(0)` is `target`.
|
|
19
|
+
* Built by rotating within the (0, axis) plane, so the result stays unit
|
|
20
|
+
* length and sqlite-vec's cosine distance comes back as 1 - target. */
|
|
21
|
+
function vectorWithSimilarity(target, axis = 1) {
|
|
22
|
+
const v = new Float32Array(DIM);
|
|
23
|
+
v[0] = target;
|
|
24
|
+
v[axis] = Math.sqrt(Math.max(0, 1 - target * target));
|
|
25
|
+
return v;
|
|
26
|
+
}
|
|
27
|
+
const QUERY = axisVector(0);
|
|
28
|
+
function seed(chunks) {
|
|
29
|
+
const db = createTestDb();
|
|
30
|
+
initMemorySchema(db);
|
|
31
|
+
chunks.forEach((c, i) => {
|
|
32
|
+
db.prepare(`INSERT OR IGNORE INTO narratives (id, brane_id, agent_type, role, created_at, status)
|
|
33
|
+
VALUES (?, 'brane-test', 'claude', 'worker', 0, 'active')`).run(c.nid);
|
|
34
|
+
upsertChunks(db, [{
|
|
35
|
+
id: `${c.nid}:${i + 1}`,
|
|
36
|
+
narrative_id: c.nid,
|
|
37
|
+
seq: i + 1,
|
|
38
|
+
content: c.content,
|
|
39
|
+
time: 1000 + i,
|
|
40
|
+
vector: vectorWithSimilarity(c.similarity, i + 1),
|
|
41
|
+
}]);
|
|
42
|
+
});
|
|
43
|
+
return db;
|
|
44
|
+
}
|
|
45
|
+
function byNid(results, nid) {
|
|
46
|
+
return results.find((r) => r.narrative_id === nid);
|
|
47
|
+
}
|
|
48
|
+
describe("searchHybrid — semantic relevance floor", () => {
|
|
49
|
+
it("drops an unrelated chunk that a single co-occurring term boosted", () => {
|
|
50
|
+
// The real failure this exists to prevent: a query term appears in an
|
|
51
|
+
// otherwise-unrelated chunk, earning +0.5, which lifted it above genuinely
|
|
52
|
+
// relevant matches.
|
|
53
|
+
const db = seed([
|
|
54
|
+
{ nid: "claude-relevant", content: "the follow flag stays armed after the correction", similarity: 0.92 },
|
|
55
|
+
{ nid: "claude-unrelated", content: "a correction-clock runs slower than the capture-clock", similarity: 0.55 },
|
|
56
|
+
]);
|
|
57
|
+
const results = searchHybrid(db, QUERY, "correction to the follow flag", 10);
|
|
58
|
+
assert.ok(byNid(results, "claude-relevant"), "the relevant chunk survives");
|
|
59
|
+
assert.equal(byNid(results, "claude-unrelated"), undefined, "a bare term co-occurring in unrelated text is not enough to qualify");
|
|
60
|
+
});
|
|
61
|
+
it("keeps a weakly-similar chunk when every query term matched it", () => {
|
|
62
|
+
// Exact-keyword evidence outranks similarity: an FTS hit means *all* terms
|
|
63
|
+
// are present, so a rare identifier still wins even if the embedder shrugs.
|
|
64
|
+
const db = seed([
|
|
65
|
+
{ nid: "claude-lexical", content: "followFlagRef is disarmed in cancelPendingPins", similarity: 0.2 },
|
|
66
|
+
]);
|
|
67
|
+
const results = searchHybrid(db, QUERY, "followFlagRef cancelPendingPins", 10);
|
|
68
|
+
const hit = byNid(results, "claude-lexical");
|
|
69
|
+
assert.ok(hit, "an all-terms match survives a similarity well under the floor");
|
|
70
|
+
assert.equal(hit.match, "keyword");
|
|
71
|
+
});
|
|
72
|
+
it("keeps an exact phrase match regardless of similarity", () => {
|
|
73
|
+
const db = seed([
|
|
74
|
+
{ nid: "claude-phrase", content: "correctness resting on commit ordering is the real defect", similarity: 0.1 },
|
|
75
|
+
]);
|
|
76
|
+
const results = searchHybrid(db, QUERY, '"correctness resting on commit ordering"', 10);
|
|
77
|
+
const hit = byNid(results, "claude-phrase");
|
|
78
|
+
assert.ok(hit, "phrase matches bypass the floor");
|
|
79
|
+
assert.equal(hit.match, "phrase");
|
|
80
|
+
});
|
|
81
|
+
it("keeps a chunk with no keyword overlap when similarity clears the floor", () => {
|
|
82
|
+
const db = seed([
|
|
83
|
+
{ nid: "claude-semantic", content: "entirely different wording about the same subject", similarity: 0.88 },
|
|
84
|
+
]);
|
|
85
|
+
const results = searchHybrid(db, QUERY, "zzzz yyyy", 10);
|
|
86
|
+
const hit = byNid(results, "claude-semantic");
|
|
87
|
+
assert.ok(hit, "pure semantic relevance still qualifies");
|
|
88
|
+
assert.equal(hit.match, "semantic");
|
|
89
|
+
});
|
|
90
|
+
it("never returns empty when nothing clears the floor — marks results weak", () => {
|
|
91
|
+
// The bound on the floor's risk: a loosely-remembered search degrades to
|
|
92
|
+
// low-confidence results, never to a blank page.
|
|
93
|
+
const db = seed([
|
|
94
|
+
{ nid: "claude-loose", content: "vaguely adjacent material with no shared terms", similarity: 0.6 },
|
|
95
|
+
]);
|
|
96
|
+
const results = searchHybrid(db, QUERY, "zzzz yyyy", 10);
|
|
97
|
+
assert.equal(results.length, 1, "the only candidate is still returned");
|
|
98
|
+
assert.equal(results[0].match, "weak", "and is labelled as not having qualified");
|
|
99
|
+
});
|
|
100
|
+
it("drops weak rows only when a qualified row replaces them", () => {
|
|
101
|
+
// The precise guarantee: dropping is conditional on something better
|
|
102
|
+
// surviving. Same weak chunk as above, now with a qualified peer.
|
|
103
|
+
const weak = { nid: "claude-loose", content: "vaguely adjacent material with no shared terms", similarity: 0.6 };
|
|
104
|
+
const alone = searchHybrid(seed([weak]), QUERY, "zzzz yyyy", 10);
|
|
105
|
+
assert.equal(alone.length, 1, "alone, the weak row is kept");
|
|
106
|
+
const withPeer = searchHybrid(seed([weak, { nid: "claude-strong", content: "clearly on topic", similarity: 0.95 }]), QUERY, "zzzz yyyy", 10);
|
|
107
|
+
assert.ok(byNid(withPeer, "claude-strong"), "the qualified row is returned");
|
|
108
|
+
assert.equal(byNid(withPeer, "claude-loose"), undefined, "and displaces the weak one");
|
|
109
|
+
});
|
|
110
|
+
it("reports the semantic score separately from the fused rank", () => {
|
|
111
|
+
// Without this a caller cannot tell "ranked because it means the same
|
|
112
|
+
// thing" from "ranked because a common word appeared".
|
|
113
|
+
const db = seed([
|
|
114
|
+
{ nid: "claude-scored", content: "on topic and containing zzzz", similarity: 0.9 },
|
|
115
|
+
]);
|
|
116
|
+
const results = searchHybrid(db, QUERY, "zzzz", 10);
|
|
117
|
+
const hit = byNid(results, "claude-scored");
|
|
118
|
+
assert.ok(hit.semantic !== undefined, "semantic score is exposed");
|
|
119
|
+
assert.ok(Math.abs(hit.semantic - 0.9) < 0.02, `semantic score reflects similarity, not the boost (got ${hit.semantic})`);
|
|
120
|
+
// `score` is a fused *rank*, not a similarity, so its magnitude is not
|
|
121
|
+
// comparable to `semantic` — under reciprocal-rank fusion a lone row scores
|
|
122
|
+
// around 1/61. What has to hold is that the two report different things:
|
|
123
|
+
// keyword evidence lifts the fused rank without touching the similarity the
|
|
124
|
+
// floor reads.
|
|
125
|
+
const withoutKeyword = byNid(searchHybrid(db, QUERY, "qqqq", 10), "claude-scored");
|
|
126
|
+
assert.ok(hit.score > withoutKeyword.score, `the keyword hit lifts the fused rank (${hit.score} vs ${withoutKeyword.score})`);
|
|
127
|
+
assert.equal(withoutKeyword.semantic, hit.semantic, "while similarity is unchanged");
|
|
128
|
+
});
|
|
129
|
+
it("keeps keyword strength distinguishable instead of clipping it", () => {
|
|
130
|
+
// The old `min(1, -bm25/10)` normalization clipped: on a real corpus every
|
|
131
|
+
// match from bm25 -10 through -20 came back as exactly 1.0, so a tight hit
|
|
132
|
+
// and an incidental all-terms hit in a long document were indistinguishable
|
|
133
|
+
// by the time the floor saw them. bm25 discounts for document length; that
|
|
134
|
+
// signal has to survive normalization to be worth anything.
|
|
135
|
+
const term = "followFlagRef";
|
|
136
|
+
const filler = "unrelated narration text that goes on at some length. ".repeat(40);
|
|
137
|
+
const db = seed([
|
|
138
|
+
{ nid: "claude-tight", content: `${term} disarmed`, similarity: 0.1 },
|
|
139
|
+
{ nid: "claude-diffuse", content: `${filler} ${term} ${filler}`, similarity: 0.1 },
|
|
140
|
+
]);
|
|
141
|
+
const results = searchHybrid(db, QUERY, term, 10);
|
|
142
|
+
const tight = byNid(results, "claude-tight");
|
|
143
|
+
const diffuse = byNid(results, "claude-diffuse");
|
|
144
|
+
assert.ok(tight?.lexical !== undefined && diffuse?.lexical !== undefined, "both matches report a keyword strength");
|
|
145
|
+
assert.ok(tight.lexical > diffuse.lexical, `a tight match scores above a diffuse one (tight=${tight.lexical}, diffuse=${diffuse.lexical})`);
|
|
146
|
+
assert.ok(tight.lexical < 1, "and neither saturates at the top of the range");
|
|
147
|
+
});
|
|
148
|
+
it("gates on similarity alone, not on the boosted score", () => {
|
|
149
|
+
// A chunk sitting just under the floor still collects keyword credit — one
|
|
150
|
+
// of the two query terms appears in it — which is why the gate has to read
|
|
151
|
+
// `semantic` rather than the fused `score`. Under the old additive scoring
|
|
152
|
+
// the +0.5 term bonus carried such a row past any threshold worth setting;
|
|
153
|
+
// under rank fusion the coverage multiplier does the same in miniature. The
|
|
154
|
+
// gate is unchanged by either, which is the point.
|
|
155
|
+
const justUnder = MIN_SEMANTIC_SCORE - 0.05;
|
|
156
|
+
const db = seed([
|
|
157
|
+
{ nid: "claude-boosted", content: "unrelated text mentioning zzzz once", similarity: justUnder },
|
|
158
|
+
]);
|
|
159
|
+
const boosted = byNid(searchHybrid(db, QUERY, "zzzz qqqq", 10), "claude-boosted");
|
|
160
|
+
assert.equal(boosted?.match, "weak", "keyword credit does not promote it past the floor");
|
|
161
|
+
// ...and it really was credited: the same row, queried without the term it
|
|
162
|
+
// contains, ranks lower.
|
|
163
|
+
const plain = byNid(searchHybrid(db, QUERY, "qqqq wwww", 10), "claude-boosted");
|
|
164
|
+
assert.ok((boosted?.score ?? 0) > (plain?.score ?? 0), `the term hit raised its rank (${boosted?.score} vs ${plain?.score})`);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe("searchHybrid — quoted phrases are requirements", () => {
|
|
168
|
+
it("excludes a semantically similar row that lacks the phrase", () => {
|
|
169
|
+
// The leak this closes: FTS enforces the phrase on its own arm, but the
|
|
170
|
+
// vector arm runs independently and used to union in rows that never
|
|
171
|
+
// contained it — so quoting ranked rather than restricted.
|
|
172
|
+
const db = seed([
|
|
173
|
+
{ nid: "claude-has", content: "correctness resting on commit ordering is the defect", similarity: 0.3 },
|
|
174
|
+
{ nid: "claude-similar", content: "the commit ordering here is a bit subtle", similarity: 0.95 },
|
|
175
|
+
]);
|
|
176
|
+
const results = searchHybrid(db, QUERY, '"correctness resting on commit ordering"', 10);
|
|
177
|
+
assert.ok(byNid(results, "claude-has"), "the row containing the phrase is returned");
|
|
178
|
+
assert.equal(byNid(results, "claude-similar"), undefined, "high similarity does not substitute for the exact phrase");
|
|
179
|
+
});
|
|
180
|
+
it("returns nothing when no row contains the phrase", () => {
|
|
181
|
+
// Deliberately unlike the relevance floor, which never empties a result
|
|
182
|
+
// set: quoting is an exact instruction, and zero results is the true answer.
|
|
183
|
+
const db = seed([
|
|
184
|
+
{ nid: "claude-close", content: "commit ordering matters for correctness", similarity: 0.97 },
|
|
185
|
+
]);
|
|
186
|
+
const results = searchHybrid(db, QUERY, '"correctness resting on commit ordering"', 10);
|
|
187
|
+
assert.equal(results.length, 0, "no near-misses, no weak fallback");
|
|
188
|
+
});
|
|
189
|
+
it("requires every phrase when several are quoted", () => {
|
|
190
|
+
const db = seed([
|
|
191
|
+
{ nid: "claude-both", content: "the follow flag went stale and cancelPendingPins ran", similarity: 0.2 },
|
|
192
|
+
{ nid: "claude-one", content: "the follow flag went stale, nothing else happened", similarity: 0.9 },
|
|
193
|
+
]);
|
|
194
|
+
const results = searchHybrid(db, QUERY, '"follow flag went stale" "cancelPendingPins ran"', 10);
|
|
195
|
+
assert.ok(byNid(results, "claude-both"), "a row with both phrases is returned");
|
|
196
|
+
assert.equal(byNid(results, "claude-one"), undefined, "one of two phrases is not enough");
|
|
197
|
+
});
|
|
198
|
+
it("leaves unquoted queries unrestricted", () => {
|
|
199
|
+
const db = seed([
|
|
200
|
+
{ nid: "claude-semantic", content: "totally different wording", similarity: 0.9 },
|
|
201
|
+
]);
|
|
202
|
+
const results = searchHybrid(db, QUERY, "follow flag went stale", 10);
|
|
203
|
+
assert.ok(byNid(results, "claude-semantic"), "bare terms still match on meaning alone");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
//# sourceMappingURL=search-floor.test.js.map
|