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,166 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { DISTINCT_AT_L, EXCLUDED_HUE_DEG, MAX_ANCHORS, MIN_ANCHORS, MIN_ANCHOR_DE, MIN_HUE_SEP_DEG, MIN_SCHEME_DISTANCE, SCHEME_FAMILIES, abAt, anchorsDistinct, bandFade, fitToGamut, hueChromaAt, hueExcluded, hueSeparationDeg, inSrgbGamut, nextScheme, packAnchors, sampleScheme, schemeDistance, } from "../shared/membrane-palette.js";
|
|
4
|
+
// Each rule here came from looking at a contact sheet (claude-1cwe8u9p:22–33):
|
|
5
|
+
// the old generator's sixteen taps read as one rainbow; the first rework was
|
|
6
|
+
// too uniform; then "at least three anchors", "no repeats", "not two cyans".
|
|
7
|
+
// These pin the rules, not the look — a sheet is how the look gets judged.
|
|
8
|
+
function mulberry32(a) {
|
|
9
|
+
return () => {
|
|
10
|
+
a |= 0;
|
|
11
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
12
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
13
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
14
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const DEG = 180 / Math.PI;
|
|
18
|
+
const SAMPLES = 400;
|
|
19
|
+
function samples(seed, n = SAMPLES) {
|
|
20
|
+
const rng = mulberry32(seed);
|
|
21
|
+
let prev = null;
|
|
22
|
+
return Array.from({ length: n }, () => (prev = nextScheme(prev, rng)));
|
|
23
|
+
}
|
|
24
|
+
describe("sampleScheme", () => {
|
|
25
|
+
const all = samples(1);
|
|
26
|
+
it("uses at least three anchors, and no more than the shader holds", () => {
|
|
27
|
+
for (const s of all) {
|
|
28
|
+
assert.ok(s.hues.length >= MIN_ANCHORS && s.hues.length <= MAX_ANCHORS, `${s.family}: ${s.hues.length}`);
|
|
29
|
+
assert.equal(s.chromas.length, s.hues.length);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
it("never uses two anchors that would render alike", () => {
|
|
33
|
+
for (const s of all) {
|
|
34
|
+
const degs = s.hues.map((h) => h * DEG);
|
|
35
|
+
for (let i = 0; i < degs.length; i++) {
|
|
36
|
+
for (let j = i + 1; j < degs.length; j++) {
|
|
37
|
+
assert.ok(hueSeparationDeg(degs[i], degs[j]) >= MIN_HUE_SEP_DEG, `${s.family}: ${degs[i]}° vs ${degs[j]}°`);
|
|
38
|
+
for (const L of DISTINCT_AT_L) {
|
|
39
|
+
const [, a1, b1] = fitToGamut(L, 0.3, s.hues[i]);
|
|
40
|
+
const [, a2, b2] = fitToGamut(L, 0.3, s.hues[j]);
|
|
41
|
+
assert.ok(Math.hypot(a1 - a2, b1 - b2) >= MIN_ANCHOR_DE);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
it("keeps the thin film's banding: 2.4–3.7 cycles, L from about 0.35 to 0.95", () => {
|
|
48
|
+
for (const s of all) {
|
|
49
|
+
const [cL, , mid, amp] = s.light;
|
|
50
|
+
assert.ok(cL >= 2.4 && cL <= 3.7);
|
|
51
|
+
assert.ok(mid - amp >= 0.29 && mid - amp <= 0.41, `trough ${mid - amp}`);
|
|
52
|
+
assert.ok(mid + amp >= 0.89 && mid + amp <= 1.0, `crest ${mid + amp}`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
it("puts no anchor between amber and green (claude-u6qlei0j:8), and still uses both sides of the band", () => {
|
|
56
|
+
let below = 0, above = 0;
|
|
57
|
+
for (const s of all) {
|
|
58
|
+
for (const h of s.hues.map((r) => r * DEG)) {
|
|
59
|
+
assert.ok(!hueExcluded(h), `${s.family}: ${h.toFixed(1)}°`);
|
|
60
|
+
assert.ok(h < EXCLUDED_HUE_DEG[0] || h > EXCLUDED_HUE_DEG[1]);
|
|
61
|
+
if (h < EXCLUDED_HUE_DEG[0] && h > EXCLUDED_HUE_DEG[0] - 20)
|
|
62
|
+
below++;
|
|
63
|
+
if (h > EXCLUDED_HUE_DEG[1] && h < EXCLUDED_HUE_DEG[1] + 20)
|
|
64
|
+
above++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
assert.ok(below > 0 && above > 0, "the oranges and the greens either side of the band are not lost with it");
|
|
68
|
+
});
|
|
69
|
+
it("passes through every anchor within one unit of thickness", () => {
|
|
70
|
+
for (const s of all)
|
|
71
|
+
assert.ok(s.hue[0] >= 1, `hue rate ${s.hue[0]}`);
|
|
72
|
+
});
|
|
73
|
+
it("reaches every family", () => {
|
|
74
|
+
const seen = new Set(all.map((s) => s.family));
|
|
75
|
+
assert.deepEqual([...seen].sort(), [...SCHEME_FAMILIES].sort());
|
|
76
|
+
});
|
|
77
|
+
it("is reproducible from its random source", () => {
|
|
78
|
+
assert.deepEqual(sampleScheme(mulberry32(42)), sampleScheme(mulberry32(42)));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe("anchorsDistinct", () => {
|
|
82
|
+
it("rejects two cyans that are 30° apart on paper", () => {
|
|
83
|
+
// sRGB is narrow around cyan: at these hues the displayable colors nearly
|
|
84
|
+
// coincide, which is the case the angle test alone let through.
|
|
85
|
+
assert.ok(hueSeparationDeg(185, 215) >= MIN_HUE_SEP_DEG);
|
|
86
|
+
assert.equal(anchorsDistinct([185, 215, 40]), false);
|
|
87
|
+
});
|
|
88
|
+
it("accepts hues that stay apart once displayed", () => {
|
|
89
|
+
assert.equal(anchorsDistinct([30, 150, 270]), true);
|
|
90
|
+
});
|
|
91
|
+
it("rejects a repeated hue outright", () => {
|
|
92
|
+
assert.equal(anchorsDistinct([100, 100, 250]), false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe("nextScheme", () => {
|
|
96
|
+
it("moves each keyframe well away from the last", () => {
|
|
97
|
+
const all = samples(2);
|
|
98
|
+
for (let i = 1; i < all.length; i++) {
|
|
99
|
+
assert.ok(schemeDistance(all[i - 1], all[i]) >= MIN_SCHEME_DISTANCE);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
it("counts a permutation of the same anchors as a different scheme", () => {
|
|
103
|
+
const s = sampleScheme(mulberry32(7));
|
|
104
|
+
const order = s.hues.map((_, i) => i).reverse();
|
|
105
|
+
const reversed = { ...s, hues: order.map((i) => s.hues[i]), chromas: order.map((i) => s.chromas[i]) };
|
|
106
|
+
assert.ok(schemeDistance(s, reversed) > 0.02, "reversed order is not the same film");
|
|
107
|
+
assert.equal(schemeDistance(s, s), 0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe("gamut", () => {
|
|
111
|
+
it("fits every anchor to a displayable color at every lightness the bands reach", () => {
|
|
112
|
+
for (const s of samples(3, 60)) {
|
|
113
|
+
for (const h of s.hues) {
|
|
114
|
+
for (let L = 0.3; L <= 0.96; L += 0.06)
|
|
115
|
+
assert.ok(inSrgbGamut(...fitToGamut(L, 0.32, h)));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe("packAnchors", () => {
|
|
121
|
+
it("repeats the first anchor after the last, so the lookup wraps without a modulo", () => {
|
|
122
|
+
const s = sampleScheme(mulberry32(9));
|
|
123
|
+
const packed = packAnchors(s);
|
|
124
|
+
const n = s.hues.length;
|
|
125
|
+
assert.equal(packed.length, (MAX_ANCHORS + 1) * 2);
|
|
126
|
+
assert.equal(packed[2 * n], Math.fround(s.hues[0]));
|
|
127
|
+
assert.equal(packed[2 * n + 1], Math.fround(s.chromas[0]));
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe("no color from the band, ever (claude-u6qlei0j:8, :17)", () => {
|
|
131
|
+
it("joins anchors by a straight line through OKLab: green to magenta fades through gray, not through yellow", () => {
|
|
132
|
+
const s = { ...sampleScheme(mulberry32(5)), hues: [145 / DEG, 330 / DEG], chromas: [0.3, 0.3] };
|
|
133
|
+
// Midway through the turn (w = 0.5 makes it linear over the whole segment).
|
|
134
|
+
const [, C] = hueChromaAt(s, 0.25, 0.5);
|
|
135
|
+
assert.ok(C < 0.06, `nearly gray in the middle, chroma ${C.toFixed(3)}`);
|
|
136
|
+
// The line does pass the band (145° to 330° the short way is through
|
|
137
|
+
// 60°), but only near the middle where there is almost no chroma, and
|
|
138
|
+
// the band takes the rest: nothing in its core, only the ramps' remnant.
|
|
139
|
+
for (let x = 0; x < 1; x += 1 / 512) {
|
|
140
|
+
const [h, c] = hueChromaAt(s, x, 0.5);
|
|
141
|
+
const hd = ((h * DEG) + 360) % 360;
|
|
142
|
+
if (hd >= 75 && hd <= 115)
|
|
143
|
+
assert.ok(c < 1e-9, `x ${x.toFixed(3)}: ${hd.toFixed(1)}° at chroma ${c.toFixed(3)}`);
|
|
144
|
+
if (hueExcluded(hd))
|
|
145
|
+
assert.ok(c <= 0.06 * (1 - bandFade(hd)) + 1e-9, `x ${x.toFixed(3)}: ${hd.toFixed(1)}° at chroma ${c.toFixed(3)}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
it("what still lands in the band is neutral, ramped inside the band so an anchor just outside keeps its color", () => {
|
|
149
|
+
assert.equal(bandFade(59), 0);
|
|
150
|
+
assert.equal(bandFade(131), 0);
|
|
151
|
+
assert.equal(bandFade(95), 1);
|
|
152
|
+
assert.ok(bandFade(67) > 0 && bandFade(67) < 1);
|
|
153
|
+
for (const s of samples(4, 200)) {
|
|
154
|
+
for (let x = 0; x < 1; x += 1 / 256) {
|
|
155
|
+
const [a, b] = abAt(s, x);
|
|
156
|
+
const hd = ((Math.atan2(b, a) * DEG) + 360) % 360;
|
|
157
|
+
const C = Math.hypot(a, b);
|
|
158
|
+
if (hd >= 75 && hd <= 115)
|
|
159
|
+
assert.ok(C < 1e-9, `${s.family} x ${x.toFixed(3)}: ${hd.toFixed(1)}° at ${C.toFixed(3)}`);
|
|
160
|
+
if (hueExcluded(hd))
|
|
161
|
+
assert.ok(C <= 0.32 * (1 - bandFade(hd)) + 1e-9);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
//# sourceMappingURL=membrane-palette.test.js.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { BLOCK, CELL, JITTER, SEAM, TERRAIN_ROWS, TERRAIN_TEXELS, VIEW_MARGIN, blend, blockCells, blockRows, cell, cellAt, cellScheme, cellsInView, centreCell, createTerrain, packCell, paintCells, schemesInView, seededRng, toWorld, } from "../shared/membrane-terrain.js";
|
|
4
|
+
import { MIN_SCHEME_DISTANCE, SCHEME_TEXELS, nextScheme, packScheme, schemeDistance } from "../shared/membrane-palette.js";
|
|
5
|
+
// The terrain exists so that a hold-and-drag scrolls the base film through
|
|
6
|
+
// color space and a drag back finds the same colors (claude-xaj5pb9b:3–5),
|
|
7
|
+
// and its cells are irregular so the eye finds no grid in it
|
|
8
|
+
// (claude-xaj5pb9b:16). These pin the three properties the module header
|
|
9
|
+
// promises — stable, very different, bounded — not the look of any one cell.
|
|
10
|
+
const same = (a, b) => assert.deepEqual(a, b);
|
|
11
|
+
const k = ([i, j]) => `${i},${j}`;
|
|
12
|
+
/** A random walk of drags, materializing blocks as the engine would. */
|
|
13
|
+
function walk(seed, steps = 40) {
|
|
14
|
+
const t = createTerrain(seed);
|
|
15
|
+
const rng = seededRng(seed + 100);
|
|
16
|
+
let drag = [0, 0];
|
|
17
|
+
for (let n = 0; n < steps; n++) {
|
|
18
|
+
blockRows(t, drag);
|
|
19
|
+
drag = [drag[0] + (rng() - 0.5) * 1.5, drag[1] + (rng() - 0.5) * 1.5];
|
|
20
|
+
}
|
|
21
|
+
return t;
|
|
22
|
+
}
|
|
23
|
+
describe("cell", () => {
|
|
24
|
+
it("is the same cell every time, within a terrain and across terrains of one seed", () => {
|
|
25
|
+
const t1 = createTerrain(7), t2 = createTerrain(7);
|
|
26
|
+
same(cell(t1, 3, -2), cell(t1, 3, -2));
|
|
27
|
+
same(cell(t1, 3, -2), cell(t2, 3, -2));
|
|
28
|
+
// Once shown, a cell never changes, whatever is drawn around it after.
|
|
29
|
+
const before = cell(t1, 0, 0);
|
|
30
|
+
for (const [i, j] of blockCells([0, 0]))
|
|
31
|
+
cell(t1, i, j);
|
|
32
|
+
same(cell(t1, 0, 0), before);
|
|
33
|
+
});
|
|
34
|
+
it("differs by seed, and by cell", () => {
|
|
35
|
+
const a = cellScheme(createTerrain(1), 0, 0), b = cellScheme(createTerrain(2), 0, 0);
|
|
36
|
+
assert.ok(schemeDistance(a, b) > 0);
|
|
37
|
+
const t = createTerrain(1);
|
|
38
|
+
assert.ok(schemeDistance(cellScheme(t, 0, 0), cellScheme(t, 5, 5)) > 0);
|
|
39
|
+
});
|
|
40
|
+
it("puts its seed point inside its own grid cell, JITTER from the centre at most", () => {
|
|
41
|
+
const t = walk(3);
|
|
42
|
+
for (const [key, c] of t.cells) {
|
|
43
|
+
const [i, j] = key.split(",").map(Number);
|
|
44
|
+
assert.ok(Math.abs(c.seed[0] / CELL - (i + 0.5)) <= JITTER + 1e-9, `${key}: x ${c.seed[0]}`);
|
|
45
|
+
assert.ok(Math.abs(c.seed[1] / CELL - (j + 0.5)) <= JITTER + 1e-9, `${key}: y ${c.seed[1]}`);
|
|
46
|
+
assert.equal(cellAt(c.seed[0]), i);
|
|
47
|
+
assert.equal(cellAt(c.seed[1]), j);
|
|
48
|
+
}
|
|
49
|
+
assert.ok(JITTER < 0.5);
|
|
50
|
+
});
|
|
51
|
+
it("is far from every one of its eight neighbors, whichever order they were drawn in", () => {
|
|
52
|
+
for (let seed = 0; seed < 8; seed++) {
|
|
53
|
+
const t = walk(seed);
|
|
54
|
+
for (const [key, c] of t.cells) {
|
|
55
|
+
const [i, j] = key.split(",").map(Number);
|
|
56
|
+
for (const [di, dj] of [[1, 0], [0, 1], [1, 1], [1, -1]]) {
|
|
57
|
+
const n = t.cells.get(`${i + di},${j + dj}`);
|
|
58
|
+
if (n)
|
|
59
|
+
assert.ok(schemeDistance(c.scheme, n.scheme) >= MIN_SCHEME_DISTANCE, `seed ${seed}: ${key} vs ${i + di},${j + dj}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe("blend", () => {
|
|
66
|
+
it("gives the nearest cell the largest share, shares to every cell within SEAM of it, none beyond, summing to one", () => {
|
|
67
|
+
const t = createTerrain(4);
|
|
68
|
+
const rng = seededRng(5);
|
|
69
|
+
for (let n = 0; n < 300; n++) {
|
|
70
|
+
const w = [(rng() - 0.5) * 10, (rng() - 0.5) * 10];
|
|
71
|
+
const shares = blend(t, w);
|
|
72
|
+
const dist = (c) => Math.hypot(w[0] - cell(t, ...c).seed[0], w[1] - cell(t, ...c).seed[1]);
|
|
73
|
+
const d1 = dist(shares[0].cell);
|
|
74
|
+
for (const c of t.cells.values())
|
|
75
|
+
assert.ok(Math.hypot(w[0] - c.seed[0], w[1] - c.seed[1]) >= d1 - 1e-12);
|
|
76
|
+
assert.ok(Math.abs(shares.reduce((a, x) => a + x.weight, 0) - 1) < 1e-9);
|
|
77
|
+
for (const { cell: c, weight } of shares) {
|
|
78
|
+
assert.ok(weight > 0 && weight <= shares[0].weight + 1e-12);
|
|
79
|
+
assert.ok(dist(c) - d1 < SEAM);
|
|
80
|
+
}
|
|
81
|
+
// Every cell within SEAM of the nearest is in.
|
|
82
|
+
const ci = cellAt(w[0]), cj = cellAt(w[1]);
|
|
83
|
+
for (let dj = -1; dj <= 1; dj++)
|
|
84
|
+
for (let di = -1; di <= 1; di++) {
|
|
85
|
+
if (dist([ci + di, cj + dj]) - d1 < SEAM)
|
|
86
|
+
assert.ok(shares.some(({ cell: c }) => c[0] === ci + di && c[1] === cj + dj));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
it("is continuous: a small step never changes the color's weights by more than a little", () => {
|
|
91
|
+
// The failure this replaces: blending only the two nearest jumped
|
|
92
|
+
// wherever the runner-up changed (claude-xaj5pb9b:21).
|
|
93
|
+
const t = createTerrain(6);
|
|
94
|
+
const rng = seededRng(7);
|
|
95
|
+
const weightsOf = (w) => new Map(blend(t, w).map(({ cell: c, weight }) => [`${c[0]},${c[1]}`, weight]));
|
|
96
|
+
for (let n = 0; n < 2000; n++) {
|
|
97
|
+
const w = [(rng() - 0.5) * 6, (rng() - 0.5) * 6];
|
|
98
|
+
const step = 0.002;
|
|
99
|
+
const a = weightsOf(w), b = weightsOf([w[0] + step, w[1] + step * 0.3]);
|
|
100
|
+
for (const key of new Set([...a.keys(), ...b.keys()])) {
|
|
101
|
+
assert.ok(Math.abs((a.get(key) ?? 0) - (b.get(key) ?? 0)) < 0.05, `at ${w}: ${key} jumps`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe("the block", () => {
|
|
107
|
+
it("is BLOCK² rows, row-major around the centre cell", () => {
|
|
108
|
+
assert.equal(TERRAIN_ROWS, BLOCK * BLOCK);
|
|
109
|
+
const cells = blockCells([4, -1]);
|
|
110
|
+
assert.equal(cells.length, TERRAIN_ROWS);
|
|
111
|
+
const r = (BLOCK - 1) / 2;
|
|
112
|
+
same(cells[0], [4 - r, -1 - r]);
|
|
113
|
+
same(cells[(TERRAIN_ROWS - 1) / 2], [4, -1]);
|
|
114
|
+
same(cells[TERRAIN_ROWS - 1], [4 + r, -1 + r]);
|
|
115
|
+
// The shader's row for (di, dj) is (dj + R) * BLOCK + (di + R).
|
|
116
|
+
same(cells[(1 + r) * BLOCK + (-1 + r)], [3, 0]);
|
|
117
|
+
assert.equal(blockRows(createTerrain(1), [0, 0]).length, TERRAIN_ROWS);
|
|
118
|
+
});
|
|
119
|
+
it("holds every cell the window can show, and every cell their lookups touch, at any drag", () => {
|
|
120
|
+
const rng = seededRng(11);
|
|
121
|
+
const t = createTerrain(12);
|
|
122
|
+
for (let n = 0; n < 300; n++) {
|
|
123
|
+
const drag = [(rng() - 0.5) * 20, (rng() - 0.5) * 20];
|
|
124
|
+
const block = new Set(blockCells(centreCell(drag)).map(k));
|
|
125
|
+
for (const c of cellsInView(t, drag))
|
|
126
|
+
assert.ok(block.has(k(c)), `drag ${drag}: cell ${k(c)}`);
|
|
127
|
+
// The shader searches the nine cells around a point's own; the widened
|
|
128
|
+
// window's corners are the farthest points it will ask about.
|
|
129
|
+
for (const px of [-0.5, 0.5])
|
|
130
|
+
for (const py of [-0.5, 0.5]) {
|
|
131
|
+
for (const m of [-VIEW_MARGIN, VIEW_MARGIN]) {
|
|
132
|
+
const ci = cellAt(toWorld(px, drag[0]) + m), cj = cellAt(toWorld(py, drag[1]) + m);
|
|
133
|
+
for (let dj = -1; dj <= 1; dj++)
|
|
134
|
+
for (let di = -1; di <= 1; di++)
|
|
135
|
+
assert.ok(block.has(`${ci + di},${cj + dj}`), `drag ${drag}: lookup ${ci + di},${cj + dj}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
it("scrolls with the drag: a drag of one cell moves the centre one cell, the other way", () => {
|
|
141
|
+
same(centreCell([0, 0]), [cellAt(0), cellAt(0)]);
|
|
142
|
+
same(centreCell([CELL, 0]), [centreCell([0, 0])[0] - 1, centreCell([0, 0])[1]]);
|
|
143
|
+
same(centreCell([0, -2 * CELL]), [centreCell([0, 0])[0], centreCell([0, 0])[1] + 2]);
|
|
144
|
+
});
|
|
145
|
+
it("packs a cell as its scheme's texels and then its seed point", () => {
|
|
146
|
+
assert.equal(TERRAIN_TEXELS, SCHEME_TEXELS + 1);
|
|
147
|
+
const c = cell(createTerrain(2), 1, 1);
|
|
148
|
+
const row = packCell(c);
|
|
149
|
+
assert.equal(row.length, TERRAIN_TEXELS * 4);
|
|
150
|
+
same([...row.slice(0, SCHEME_TEXELS * 4)], [...packScheme(c.scheme)]);
|
|
151
|
+
same([...row.slice(SCHEME_TEXELS * 4)], [Math.fround(c.seed[0]), Math.fround(c.seed[1]), 0, 0]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe("paintCells", () => {
|
|
155
|
+
it("leaves a wash's scheme on the map, seeds kept, where a drag away and back finds it", () => {
|
|
156
|
+
const t = createTerrain(5);
|
|
157
|
+
const drag = [0.3, -0.2];
|
|
158
|
+
const seeds = new Map([...t.cells].map(([key, c]) => [key, c.seed]));
|
|
159
|
+
const won = nextScheme(schemesInView(t, drag), seededRng(9));
|
|
160
|
+
paintCells(t, cellsInView(t, drag), won);
|
|
161
|
+
for (const s of schemesInView(t, drag))
|
|
162
|
+
same(s, won);
|
|
163
|
+
for (const [key, seed] of seeds)
|
|
164
|
+
same(t.cells.get(key)?.seed, seed);
|
|
165
|
+
blockRows(t, [drag[0] + 5, drag[1] + 5]); // away
|
|
166
|
+
for (const s of schemesInView(t, drag))
|
|
167
|
+
same(s, won); // and back
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
//# sourceMappingURL=membrane-terrain.test.js.map
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createTestDb } from "./helpers.js";
|
|
4
|
+
import { appendEvent } from "../db/events.js";
|
|
5
|
+
import { parseDuration, timeSince } from "../commands/memory.js";
|
|
6
|
+
// Helper to insert a narrative row
|
|
7
|
+
function insertNarrative(db, id, opts = {}) {
|
|
8
|
+
db.prepare(`INSERT INTO narratives (id, brane_id, agent_type, role, status, title, summary, created_at)
|
|
9
|
+
VALUES (?, 'brane-test', ?, ?, ?, ?, ?, ?)`).run(id, opts.agent_type ?? "claude", opts.role ?? "worker", opts.status ?? "active", opts.title ?? null, opts.summary ?? null, opts.created_at ?? Date.now());
|
|
10
|
+
}
|
|
11
|
+
// The core log query extracted for testing (mirrors memoryLog's SQL)
|
|
12
|
+
function queryLog(db, sinceMs, opts = {}) {
|
|
13
|
+
let sql = `
|
|
14
|
+
SELECT
|
|
15
|
+
n.id,
|
|
16
|
+
n.agent_type,
|
|
17
|
+
n.role,
|
|
18
|
+
n.status,
|
|
19
|
+
n.title,
|
|
20
|
+
n.summary,
|
|
21
|
+
MAX(e.time) as last_activity,
|
|
22
|
+
COUNT(e.seq) as event_count
|
|
23
|
+
FROM narratives n
|
|
24
|
+
JOIN events e ON e.narrative_id = n.id
|
|
25
|
+
WHERE e.time > ?
|
|
26
|
+
`;
|
|
27
|
+
const params = [sinceMs];
|
|
28
|
+
if (opts.status) {
|
|
29
|
+
sql += ` AND n.status = ?`;
|
|
30
|
+
params.push(opts.status);
|
|
31
|
+
}
|
|
32
|
+
if (opts.nids) {
|
|
33
|
+
const placeholders = Array.from(opts.nids)
|
|
34
|
+
.map(() => "?")
|
|
35
|
+
.join(", ");
|
|
36
|
+
sql += ` AND n.id IN (${placeholders})`;
|
|
37
|
+
params.push(...opts.nids);
|
|
38
|
+
}
|
|
39
|
+
sql += `
|
|
40
|
+
GROUP BY n.id
|
|
41
|
+
ORDER BY last_activity DESC
|
|
42
|
+
LIMIT ?
|
|
43
|
+
`;
|
|
44
|
+
params.push(opts.limit ?? 20);
|
|
45
|
+
return db.prepare(sql).all(...params);
|
|
46
|
+
}
|
|
47
|
+
describe("parseDuration", () => {
|
|
48
|
+
it("parses milliseconds", () => {
|
|
49
|
+
assert.equal(parseDuration("500ms"), 500);
|
|
50
|
+
});
|
|
51
|
+
it("parses seconds", () => {
|
|
52
|
+
assert.equal(parseDuration("30s"), 30_000);
|
|
53
|
+
});
|
|
54
|
+
it("parses minutes", () => {
|
|
55
|
+
assert.equal(parseDuration("5m"), 5 * 60 * 1000);
|
|
56
|
+
});
|
|
57
|
+
it("parses hours", () => {
|
|
58
|
+
assert.equal(parseDuration("2h"), 2 * 60 * 60 * 1000);
|
|
59
|
+
});
|
|
60
|
+
it("parses days", () => {
|
|
61
|
+
assert.equal(parseDuration("3d"), 3 * 24 * 60 * 60 * 1000);
|
|
62
|
+
});
|
|
63
|
+
it("parses weeks", () => {
|
|
64
|
+
assert.equal(parseDuration("1w"), 7 * 24 * 60 * 60 * 1000);
|
|
65
|
+
});
|
|
66
|
+
it("throws on invalid input", () => {
|
|
67
|
+
assert.throws(() => parseDuration("abc"), /Invalid duration/);
|
|
68
|
+
assert.throws(() => parseDuration("10x"), /Invalid duration/);
|
|
69
|
+
assert.throws(() => parseDuration(""), /Invalid duration/);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe("timeSince", () => {
|
|
73
|
+
it("formats seconds", () => {
|
|
74
|
+
const result = timeSince(Date.now() - 30_000);
|
|
75
|
+
assert.match(result, /^\d+s ago$/);
|
|
76
|
+
});
|
|
77
|
+
it("formats minutes", () => {
|
|
78
|
+
const result = timeSince(Date.now() - 5 * 60 * 1000);
|
|
79
|
+
assert.match(result, /^\d+m ago$/);
|
|
80
|
+
});
|
|
81
|
+
it("formats hours", () => {
|
|
82
|
+
const result = timeSince(Date.now() - 3 * 60 * 60 * 1000);
|
|
83
|
+
assert.match(result, /^\d+h ago$/);
|
|
84
|
+
});
|
|
85
|
+
it("formats days", () => {
|
|
86
|
+
const result = timeSince(Date.now() - 2 * 24 * 60 * 60 * 1000);
|
|
87
|
+
assert.match(result, /^\d+d ago$/);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe("memory log query", () => {
|
|
91
|
+
it("returns narratives sorted by last activity descending", () => {
|
|
92
|
+
const db = createTestDb();
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
insertNarrative(db, "n-old", { title: "Old one" });
|
|
95
|
+
insertNarrative(db, "n-new", { title: "New one" });
|
|
96
|
+
appendEvent(db, "n-old", "narration", { content: "old event" });
|
|
97
|
+
// Manually insert a newer event for n-new
|
|
98
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run("n-new", 1, now + 1000, "narration", "new event");
|
|
99
|
+
const rows = queryLog(db, now - 60_000);
|
|
100
|
+
assert.equal(rows.length, 2);
|
|
101
|
+
assert.equal(rows[0].id, "n-new");
|
|
102
|
+
assert.equal(rows[1].id, "n-old");
|
|
103
|
+
});
|
|
104
|
+
it("filters by time window", () => {
|
|
105
|
+
const db = createTestDb();
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
insertNarrative(db, "n-recent");
|
|
108
|
+
insertNarrative(db, "n-stale");
|
|
109
|
+
// Recent event
|
|
110
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run("n-recent", 1, now - 1000, "narration", "recent");
|
|
111
|
+
// Old event (2 hours ago)
|
|
112
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run("n-stale", 1, now - 2 * 60 * 60 * 1000, "narration", "stale");
|
|
113
|
+
// Query with 1h window
|
|
114
|
+
const rows = queryLog(db, now - 60 * 60 * 1000);
|
|
115
|
+
assert.equal(rows.length, 1);
|
|
116
|
+
assert.equal(rows[0].id, "n-recent");
|
|
117
|
+
});
|
|
118
|
+
it("filters by status", () => {
|
|
119
|
+
const db = createTestDb();
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
insertNarrative(db, "n-active", { status: "active" });
|
|
122
|
+
insertNarrative(db, "n-ended", { status: "ended" });
|
|
123
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run("n-active", 1, now, "narration", "hi");
|
|
124
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run("n-ended", 1, now, "narration", "bye");
|
|
125
|
+
const rows = queryLog(db, now - 60_000, { status: "active" });
|
|
126
|
+
assert.equal(rows.length, 1);
|
|
127
|
+
assert.equal(rows[0].id, "n-active");
|
|
128
|
+
});
|
|
129
|
+
it("filters by narrative ID set (search)", () => {
|
|
130
|
+
const db = createTestDb();
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
insertNarrative(db, "n-match");
|
|
133
|
+
insertNarrative(db, "n-other");
|
|
134
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run("n-match", 1, now, "narration", "relevant");
|
|
135
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run("n-other", 1, now, "narration", "unrelated");
|
|
136
|
+
const rows = queryLog(db, now - 60_000, {
|
|
137
|
+
nids: new Set(["n-match"]),
|
|
138
|
+
});
|
|
139
|
+
assert.equal(rows.length, 1);
|
|
140
|
+
assert.equal(rows[0].id, "n-match");
|
|
141
|
+
});
|
|
142
|
+
it("respects limit", () => {
|
|
143
|
+
const db = createTestDb();
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
for (let i = 0; i < 5; i++) {
|
|
146
|
+
insertNarrative(db, `n-${i}`);
|
|
147
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run(`n-${i}`, 1, now + i, "narration", `event ${i}`);
|
|
148
|
+
}
|
|
149
|
+
const rows = queryLog(db, now - 60_000, { limit: 3 });
|
|
150
|
+
assert.equal(rows.length, 3);
|
|
151
|
+
});
|
|
152
|
+
it("counts events per narrative", () => {
|
|
153
|
+
const db = createTestDb();
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
insertNarrative(db, "n-busy");
|
|
156
|
+
for (let i = 1; i <= 5; i++) {
|
|
157
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run("n-busy", i, now, "narration", `event ${i}`);
|
|
158
|
+
}
|
|
159
|
+
const rows = queryLog(db, now - 60_000);
|
|
160
|
+
assert.equal(rows.length, 1);
|
|
161
|
+
assert.equal(rows[0].event_count, 5);
|
|
162
|
+
});
|
|
163
|
+
it("includes title and summary from narrative", () => {
|
|
164
|
+
const db = createTestDb();
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
insertNarrative(db, "n-titled", {
|
|
167
|
+
title: "Auth Module",
|
|
168
|
+
summary: "Working on JWT",
|
|
169
|
+
});
|
|
170
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run("n-titled", 1, now, "narration", "doing auth");
|
|
171
|
+
const rows = queryLog(db, now - 60_000);
|
|
172
|
+
assert.equal(rows[0].title, "Auth Module");
|
|
173
|
+
assert.equal(rows[0].summary, "Working on JWT");
|
|
174
|
+
});
|
|
175
|
+
it("returns empty when no narratives have recent events", () => {
|
|
176
|
+
const db = createTestDb();
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
insertNarrative(db, "n-old");
|
|
179
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run("n-old", 1, now - 48 * 60 * 60 * 1000, "narration", "ancient");
|
|
180
|
+
const rows = queryLog(db, now - 24 * 60 * 60 * 1000);
|
|
181
|
+
assert.equal(rows.length, 0);
|
|
182
|
+
});
|
|
183
|
+
it("LIKE fallback search finds matching narratives", () => {
|
|
184
|
+
const db = createTestDb();
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
insertNarrative(db, "n-auth");
|
|
187
|
+
insertNarrative(db, "n-db");
|
|
188
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run("n-auth", 1, now, "narration", "implementing JWT authentication");
|
|
189
|
+
db.prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?, ?, ?, ?, ?)").run("n-db", 1, now, "narration", "database migration complete");
|
|
190
|
+
// Simulate LIKE search fallback
|
|
191
|
+
const likeRows = db
|
|
192
|
+
.prepare(`SELECT DISTINCT narrative_id FROM events WHERE content LIKE ? LIMIT 1000`)
|
|
193
|
+
.all("%JWT%");
|
|
194
|
+
const nids = new Set(likeRows.map((r) => r.narrative_id));
|
|
195
|
+
assert.equal(nids.size, 1);
|
|
196
|
+
assert.ok(nids.has("n-auth"));
|
|
197
|
+
const rows = queryLog(db, now - 60_000, { nids });
|
|
198
|
+
assert.equal(rows.length, 1);
|
|
199
|
+
assert.equal(rows[0].id, "n-auth");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
//# sourceMappingURL=memory-log.test.js.map
|