From 2aea2d3da19b8ad69ec2b18fcb670bd41c2dce50 Mon Sep 17 00:00:00 2001
From: flujo-app <300233937+flujo-app@users.noreply.github.com>
Date: Sat, 3 Oct 2026 15:00:21 -0500
Subject: [PATCH 1/9] Add bounded FACTORY swarm Observatory viewer and full
hierarchy navigation
---
docs/SWARM.md | 74 ++++++++++
index.html | 1 +
package.json | 1 +
src/main.ts | 3 +
src/swarm/main.ts | 274 ++++++++++++++++++++++++++++++++++++++
src/swarm/model.ts | 177 ++++++++++++++++++++++++
src/swarm/observatory.css | 126 ++++++++++++++++++
src/swarm/preview.ts | 24 ++++
src/swarm/renderer.ts | 213 +++++++++++++++++++++++++++++
src/swarm/swarm.css | 1 +
swarm.html | 15 +++
test/swarm.test.mjs | 92 +++++++++++++
vite.config.ts | 1 +
13 files changed, 1002 insertions(+)
create mode 100644 docs/SWARM.md
create mode 100644 src/swarm/main.ts
create mode 100644 src/swarm/model.ts
create mode 100644 src/swarm/observatory.css
create mode 100644 src/swarm/preview.ts
create mode 100644 src/swarm/renderer.ts
create mode 100644 src/swarm/swarm.css
create mode 100644 swarm.html
create mode 100644 test/swarm.test.mjs
diff --git a/docs/SWARM.md b/docs/SWARM.md
new file mode 100644
index 0000000..f12f14b
--- /dev/null
+++ b/docs/SWARM.md
@@ -0,0 +1,74 @@
+# Brain Observatory
+
+`swarm.html` is the multipage FACTORY viewer built with the normal Vite build.
+Brain Online embeds it under `/viewer/` from the upstream Brain image and supplies
+authenticated observations. Its normal Brain viewer links to `/factory/swarm`
+when hosted at `/viewer/`; standalone hosts link to `./swarm.html`.
+
+The component accepts public v1 snapshots from an explicit operator source
+registry. It does not discover factories, receive credentials, execute work,
+or infer authority links between sources. All identities include the registry
+source and factory. The read-only index preserves every recorded cell, task,
+effect, evidence digest and local parent relationship while rendering a bounded
+subset. Search, breadcrumbs and 128-child pages navigate the complete index.
+
+## Hosting
+
+Embedded hosts pass `?embed=1&channel=<16..128 character channel>` and exchange
+same-origin `brain-swarm/1` messages with the exact parent/frame:
+
+- viewer → parent: `ready` or `select` with the channel and scoped selection;
+- parent → viewer: `observe` with channel, increasing integer sequence and model;
+- parent → viewer: `ping`; viewer repeats `ready`.
+
+The model has schemaVersion 1, scope `operator-source-registry`, commands false,
+observedAt and at most 32 sources. Each source has id, label, factoryId,
+observed/stale/unavailable status, a public canonical FACTORY snapshot or null,
+and optional metadata events. `model.ts` positively allowlists these facts;
+private extensions are dropped and unsupported execution capability is rejected.
+Never put a bearer credential in a URL, iframe message or client environment.
+
+A standalone deployment must supply its own authenticated same-origin JSON bridge
+at `/api/factory/swarm` (or the `source` parameter's same-origin absolute path).
+The Brain manager does not automatically expose credentialed FACTORY reads.
+Standalone polling uses same-origin credentials, no-store reads and a five-second
+delay after each observation. An unavailable bridge shows an empty/unavailable
+world; there is no real-data-to-fixture fallback.
+
+`swarm.html?preview=1&cells=10000` explicitly generates a 10,000-cell design
+fixture. Normal preview defaults to 54 cells across three independent sources.
+The fixture label remains visible. Brain Online has its own explicit preview
+host and uses the same upstream renderer.
+
+## Scale and rendering
+
+The full index uses iterative hierarchy layout, deterministic positions and
+scoped keys. It handles deep chains and wide parents without recursion or a
+force simulation. Visible nodes are capped at 600, labels at 24, and navigation
+lists/search results are bounded. Ancestor/sibling context and child pages keep
+hidden records reachable. A source overview has no cross-source authority edges.
+
+The WebGL renderer batches node points and delegation/reference lines, disposes
+replaced geometry, suspends hidden documents, renders idle views only on demand,
+and caps evidenced activity at 30 frames/second. Reduced motion disables pulse
+and animated camera flight. Context loss switches to the 2D renderer, which
+uses the same index and scoped selection. Rendering diagnostics report CPU
+submission time separately from draw-call and frame counts; they do not prove
+GPU timing or physical worker health.
+
+Fresh observations do not establish worker activity. A cell needs an active
+controller, ready lifecycle, recent heartbeat, an unexpired running lease and
+no owned unknown outcome. Completed/cancelled operations do not animate as
+running work. Pause, effect drain and worker quiescence remain separate facts.
+The selected inspector preserves task, candidate and review digests and states
+that quiescence is unverified.
+
+## Development and verification
+
+Run `npm ci`, `npm run build` and `npm run test:swarm`. Tests use the actual model
+and preview modules and cover duplicate record IDs across authorities, complete
+10,000-deep/wide navigation, stable layout, stale/lease/terminal activity gates,
+positive allowlists and missing-source behavior. Copy the built `dist` artifact
+to the consumer's ignored viewer directory for local integration; keep source
+in this repository. Qualify desktop/mobile composition and 3D/2D navigation in
+the browser against the built artifact before publishing the Brain image.
diff --git a/index.html b/index.html
index cccf657..c9b2388 100644
--- a/index.html
+++ b/index.html
@@ -20,6 +20,7 @@
brain
+
⬡ swarm
⬡ brains
diff --git a/package.json b/package.json
index 0399f6c..3c861fc 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"standalone": "npm run build && npm install --prefix manager && npm run standalone --prefix manager",
"release": "node scripts/release.mjs",
"test:release": "node --test test/release-arguments.test.mjs",
+ "test:swarm": "node --test test/swarm.test.mjs",
"dockerbuild": "gh workflow run brain-release.yml -R flujo-app/brain --ref main"
},
"keywords": [
diff --git a/src/main.ts b/src/main.ts
index 602f228..d46b8e8 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -10,6 +10,9 @@ import { AiDock } from './ui/aichat';
import type { BrainGraph, NodeChatMessage } from './types';
const VIEW_KEY = 'brain-view';
+// Brain Online supplies its authenticated observation host when serving /viewer/.
+const swarmLink = document.getElementById('swarm-link') as HTMLAnchorElement | null;
+if (swarmLink && /^\/viewer\/(?:index\.html)?$/.test(location.pathname)) swarmLink.href = '/factory/swarm';
function setBadge(text: string, connected: boolean) {
const badge = document.getElementById('source-badge');
diff --git a/src/swarm/main.ts b/src/swarm/main.ts
new file mode 100644
index 0000000..fcf074a
--- /dev/null
+++ b/src/swarm/main.ts
@@ -0,0 +1,274 @@
+import './swarm.css';
+import './observatory.css';
+import { activities, nodeKey, parseSwarm, SwarmIndex, type Node, type Selection, type Source, type Swarm } from './model';
+import { SwarmRenderer } from './renderer';
+import { swarmPreview } from './preview';
+
+const $ = (id: string): HTMLElement => document.getElementById(id)!;
+const element = (tag: T, text?: string, className?: string): HTMLElementTagNameMap[T] => {
+ const result = document.createElement(tag); if (text !== undefined) result.textContent = text; if (className) result.className = className; return result;
+};
+const button = (text: string, action: () => void, pressed = false): HTMLButtonElement => { const result = element('button', text); result.type = 'button'; result.setAttribute('aria-pressed', String(pressed)); result.onclick = action; return result; };
+const timestamp = (value: string): string => new Intl.DateTimeFormat(undefined, { timeZone: 'America/Bogota', dateStyle: 'medium', timeStyle: 'medium' }).format(new Date(value));
+const taskLabel = (status: string) => status === 'completed' ? 'operation completed' : status;
+const empty = (): Swarm => ({ schemaVersion: 1, scope: 'operator-source-registry', observedAt: new Date().toISOString(), commands: false, sources: [] });
+const query = new URLSearchParams(location.search), embedded = query.get('embed') === '1', channel = query.get('channel') ?? '';
+const preview = !embedded && query.get('preview') === '1';
+let swarm = empty(), index = new SwarmIndex(swarm), selection: Selection | null = null, page = 0, frameSequence = 0, lastStats = 0;
+let renderer: SwarmRenderer;
+let mode: '3d' | '2d' = query.get('view') === '2d' || ((navigator.hardwareConcurrency ?? 8) <= 4) ? '2d' : '3d';
+let lastActivity = '', follow = false, firstObservation = true;
+const selectionHash = (): Selection | null => {
+ const hash = new URLSearchParams(location.hash.slice(1)), sourceId = hash.get('source'), kind = hash.get('kind'), id = hash.get('id');
+ return sourceId && id && ['source', 'cell', 'task', 'effect'].includes(kind ?? '') ? { sourceId, kind: kind as Selection['kind'], id } : null;
+};
+
+function validSelection(value: Selection): boolean {
+ const source = index.sources.get(value.sourceId); if (!source) return false;
+ return value.kind === 'source' ? value.id === source.id : value.kind === 'cell' ? source.snapshot?.snapshot.cells.some(cell => cell.id === value.id) === true : value.kind === 'task' ? source.snapshot?.snapshot.tasks.some(task => task.id === value.id) === true : source.snapshot?.snapshot.effects.some(effect => effect.key === value.id) === true;
+}
+function select(next: Selection | null, move = true): void {
+ if (next && !validSelection(next)) return;
+ selection = next; page = 0;
+ document.body.classList.toggle('has-selection', !!next);
+ const hash = next ? new URLSearchParams({ source: next.sourceId, kind: next.kind, id: next.id }).toString() : '';
+ history.replaceState(null, '', `${location.pathname}${location.search}${hash ? `#${hash}` : ''}`);
+ redraw(); if (move) frameSelection();
+ if (embedded) window.parent.postMessage({ protocol: 'brain-swarm/1', type: 'select', channel, selection: next }, location.origin);
+}
+function sourceStatus(source: Source): string {
+ const age = source.snapshot ? Date.now() - Date.parse(source.snapshot.observedAt) : Infinity;
+ return source.status === 'observed' && !swarm.sample && age > 15000 ? 'stale' : source.status;
+}
+function focusNode(): Node | undefined { return selection ? index.nodeFor(selection) : undefined; }
+function frameSelection(): void {
+ const node = focusNode();
+ if (node && (!node.cell || node.cell.role === 'coordinator')) renderer.frame(undefined, node.source.id);
+ else renderer.frame(node?.point);
+}
+
+function createRenderer(): void {
+ renderer?.dispose();
+ const canvas = element('canvas'); canvas.id = 'swarm-canvas'; canvas.tabIndex = 0; canvas.setAttribute('aria-label', 'Spatial swarm. Use navigation buttons to select cells; Home fits the whole swarm.');
+ $('swarm-canvas').replaceWith(canvas);
+ renderer = new SwarmRenderer(canvas, $('swarm-labels'), mode); mode = renderer.mode;
+ (document.getElementById('swarm-mode') as HTMLSelectElement).value = mode;
+ renderer.onPick = node => select(node.selection);
+ renderer.onStats = stats => {
+ const diagnostics = $('swarm-diagnostics');
+ Object.assign(diagnostics.dataset, { mode: stats.mode, visible: String(stats.visible), indexed: String(index.nodes.size), labels: String(stats.labels), draws: String(stats.draws), frames: String(stats.submissions), cpuMs: stats.cpuMs.toFixed(3) });
+ if (performance.now() - lastStats < 500) return; lastStats = performance.now();
+ diagnostics.textContent = `${stats.mode.toUpperCase()} · ${stats.visible} drawn / ${index.nodes.size} indexed nodes · ${stats.labels} labels · ${stats.draws} GL draw calls · ${stats.cpuMs.toFixed(2)} ms CPU submission · ${stats.submissions} frames`;
+ };
+ redraw(); frameSelection();
+}
+
+function paintNavigation(): void {
+ const sourceList = $('source-list'); sourceList.replaceChildren();
+ for (const source of index.sources.values()) {
+ const item = button(source.label, () => select({ sourceId: source.id, kind: 'source', id: source.id }), selection?.sourceId === source.id);
+ item.className = `source-record source-${sourceStatus(source)}`;
+ item.append(element('small', `${sourceStatus(source)} · ${source.snapshot?.snapshot.cells.length ?? 0} cells`)); item.title = source.factoryId; sourceList.append(item);
+ }
+ const focus = focusNode(), children = focus ? index.children.get(focus.key) ?? [] : [];
+ $('branch-heading').textContent = focus ? `${focus.label} · ${children.length} children` : 'Select a source to explore its delegation';
+ $('branch-list').replaceChildren(); $('branch-pages').replaceChildren();
+ for (const key of children.slice(page * 128, (page + 1) * 128)) {
+ const node = index.nodes.get(key)!;
+ const item = button(node.label, () => select(node.selection), key === focus?.key);
+ item.append(element('small', `${node.cell?.role ?? 'source'} · ${node.cell?.status ?? sourceStatus(node.source)} · ${index.children.get(key)?.length ?? 0} children`));
+ $('branch-list').append(item);
+ }
+ if (children.length > 128) {
+ const previous = button('Previous children', () => { page--; redraw(); }); previous.disabled = page === 0;
+ const next = button('Next children', () => { page++; redraw(); }); next.disabled = (page + 1) * 128 >= children.length;
+ $('branch-pages').append(previous, element('p', `Children ${page * 128 + 1}–${Math.min(children.length, (page + 1) * 128)} of ${children.length}`), next);
+ }
+ const path = $('swarm-path'); path.replaceChildren(button('Swarm', () => select(null)));
+ if (focus) {
+ const ancestors = index.ancestors(focus.key), shown = ancestors.length > 6 ? [ancestors[0]!, ...ancestors.slice(-4)] : ancestors;
+ if (ancestors.length > 6) path.append(element('span', `${ancestors.length - 5} intermediate levels`));
+ for (const node of shown) path.append(element('span', '/'), button(node.label, () => select(node.selection), node.key === focus.key));
+ }
+ paintSearch();
+}
+function paintSearch(): void {
+ const query = (document.getElementById('swarm-search') as HTMLInputElement).value, results = $('search-results'); results.replaceChildren();
+ const matches = index.search(query); if (!query.trim()) return;
+ results.append(element('p', `${matches.length} matches across all indexed sources. Refine the search to reach a specific identity.`));
+ for (const match of matches.slice(0, 60)) {
+ const source = index.sources.get(match.sourceId)!;
+ const item = button(`${match.id} · ${source.label} · ${match.kind}`, () => select(match)); item.title = `${source.factoryId} / ${match.id}`; results.append(item);
+ }
+}
+function evidenceList(container: HTMLElement, pairs: Array<[string, string]>): void {
+ const list = element('dl'); for (const [key, value] of pairs) list.append(element('dt', key), element('dd', value)); container.append(list);
+}
+function paintInspector(): void {
+ const container = $('swarm-inspect'); container.replaceChildren();
+ if (!selection) { container.append(element('h2', 'A constellation of work.'), element('p', 'Move from registered authorities to their coordinators and delegated cells. Search reaches every recorded cell, task and external effect.'), element('p', 'This view follows observations. It cannot start, stop or resume workers.'), element('p', 'Select a source to inspect its provenance and recorded work.')); return; }
+ const source = index.sources.get(selection.sourceId)!;
+ container.append(element('div', `${sourceStatus(source)} · ${source.label}`, 'source-state'), element('h2', selection.kind === 'source' ? source.label : selection.id));
+ evidenceList(container, [['Factory identity', source.factoryId], ['Registry source', source.id]]);
+ const envelope = source.snapshot;
+ if (!envelope) { container.append(element('p', 'This registered source has no available observation. No cells or execution state are inferred.', 'evidence-note')); return; }
+ const state = envelope.snapshot;
+ if (sourceStatus(source) !== 'observed') container.append(element('p', 'Retained observation. Activity is unconfirmed until this authority is read successfully again.', 'evidence-note'));
+ if (selection.kind === 'source') {
+ container.append(element('p', state.control.mission));
+ evidenceList(container, [['Scope', envelope.scope], ['Observed build', envelope.buildRevision], ['Controller revision', String(envelope.revision)], ['Snapshot read', timestamp(envelope.observedAt)], ['Controller admission', `${state.control.status} · epoch ${state.control.epoch}`], ['Recorded work', `${state.tasks.length} tasks · ${state.effects.length} effects`], ['Worker quiescence', 'unverified']]);
+ container.append(element('h3', 'Recorded tasks'));
+ appendTasks(container, source, state.tasks);
+ container.append(element('p', 'Provider host identity, worker health and final paid billing are not established by this contract.', 'evidence-note'));
+ } else if (selection.kind === 'cell') {
+ const cell = state.cells.find(cell => cell.id === selection!.id)!;
+ container.append(element('p', cell.purpose));
+ evidenceList(container, [['Role / lifecycle', `${cell.role} / ${cell.status}`], ['Recorded parent', cell.parentId ?? 'Local root'], ['Heartbeat', timestamp(cell.heartbeat)], ['Activity evidence', activities(source, Date.now(), swarm.sample).get(cell.id) ?? 'idle'], ['Logical allocation', `$${(cell.allocationCents / 100).toFixed(2)} · logical-allocation`], ['Worker quiescence', 'unverified']]);
+ container.append(element('h3', 'Owned tasks')); appendTasks(container, source, state.tasks.filter(task => task.owner === cell.id));
+ container.append(element('h3', 'Owned external effects')); appendEffects(container, source, state.effects.filter(effect => effect.owner === cell.id));
+ if (cell.parentId && !state.cells.some(parent => parent.id === cell.parentId)) container.append(element('p', 'The parent is absent from this observation. Its position does not prove a delegation link.', 'evidence-note'));
+ } else if (selection.kind === 'task') {
+ const task = state.tasks.find(task => task.id === selection!.id)!;
+ if (task.status === 'completed') container.append(element('p', 'Operation completed. Software review and delivery are not established by this status.', 'evidence-note'));
+ if (task.status === 'cancelled') container.append(element('p', 'Cancelled: abandoned work or unmet acceptance. Candidate and review hashes remain historical evidence.', 'evidence-note'));
+ evidenceList(container, [['Task status', taskLabel(task.status)], ['Project / branch', `${task.projectId} / ${task.branch}`], ['Attempt', String(task.attempt)], ['Lease expiry', task.leaseExpiry ? timestamp(task.leaseExpiry) : 'None recorded'], ['Specification SHA256', task.specDigest], ['Candidate SHA256', task.candidate?.sha256 ?? 'None recorded'], ['Review verdict', task.review ? task.review.accepted ? 'Recorded accepted review' : 'Recorded rejected review' : 'None recorded'], ['Review evidence SHA256', task.review?.evidenceDigest ?? 'None recorded'], ['Reviewed candidate SHA256', task.review?.candidateDigest ?? 'None recorded'], ['Reviewed spec SHA256', task.review?.specDigest ?? 'None recorded'], ['Review attempt', task.review ? String(task.review.attempt) : 'None recorded']]);
+ if (task.owner) container.append(button(`Inspect owner · ${task.owner}`, () => select({ sourceId: source.id, kind: 'cell', id: task.owner! })));
+ appendEffects(container, source, state.effects.filter(effect => effect.taskId === task.id));
+ } else {
+ const effect = state.effects.find(effect => effect.key === selection!.id)!;
+ evidenceList(container, [['Kind / outcome', `${effect.kind} / ${effect.state}`], ['Scope', `${effect.scope} / ${effect.scopeId}`], ['Owner epochs', `${effect.ownerEpoch} / controller ${effect.controlEpoch}`], ['Created', timestamp(effect.createdAt)], ['Updated', timestamp(effect.updatedAt)], ['Request SHA256', effect.requestDigest]]);
+ container.append(button(`Inspect owner · ${effect.owner}`, () => select({ sourceId: source.id, kind: 'cell', id: effect.owner })));
+ if (effect.taskId) container.append(button(`Inspect task · ${effect.taskId}`, () => select({ sourceId: source.id, kind: 'task', id: effect.taskId! })));
+ if (effect.state === 'unknown') container.append(element('p', 'The external outcome is unknown. Do not infer success, retry eligibility or stopped workers.', 'evidence-note'));
+ }
+ if (index.issues.length) container.append(element('p', index.issues.slice(0, 3).join('. '), 'evidence-note'));
+}
+function appendTasks(container: HTMLElement, source: Source, tasks: NonNullable['snapshot']['tasks']): void {
+ if (!tasks.length) container.append(element('p', 'No tasks recorded.'));
+ for (const task of tasks.slice(0, 50)) container.append(button(`${task.id} · ${taskLabel(task.status)}`, () => select({ sourceId: source.id, kind: 'task', id: task.id })));
+ if (tasks.length > 50) container.append(element('p', `${tasks.length} tasks indexed. Search reaches each full identity.`));
+}
+function appendEffects(container: HTMLElement, source: Source, effects: NonNullable['snapshot']['effects']): void {
+ for (const effect of effects.slice(0, 50)) container.append(button(`${effect.key} · ${effect.state}`, () => select({ sourceId: source.id, kind: 'effect', id: effect.key })));
+ if (effects.length > 50) container.append(element('p', `${effects.length} effects indexed. Search reaches each full identity.`));
+}
+function paintTracking(): void {
+ const source = selection ? index.sources.get(selection.sourceId) : null;
+ $('tracking-scope').textContent = source ? `${source.label} · recorded controller events` : 'Choose a source';
+ const container = $('tracking-list'); container.replaceChildren();
+ if (!source?.events.length) { container.append(element('p', 'No event metadata has been collected in this view. Current topology comes from snapshots; events do not reconstruct missing history.')); return; }
+ for (const event of source.events.slice(-30).reverse()) {
+ const row = element('div', undefined, 'tracking-row'); const time = element('time', timestamp(event.observedAt)); time.dateTime = event.observedAt;
+ row.append(time, element('span', `${event.seq} · ${event.type}`), element('span', event.subject)); container.append(row);
+ }
+}
+function redraw(): void {
+ const focus = focusNode(), visible = index.visible(focus?.key ?? null, page);
+ renderer?.setNodes(visible, focus?.key ?? null);
+ document.body.classList.toggle('has-selection', !!selection);
+ paintNavigation(); paintInspector(); paintTracking();
+ const cells = swarm.sources.reduce((sum, source) => sum + (source.snapshot?.snapshot.cells.length ?? 0), 0), tasks = swarm.sources.reduce((sum, source) => sum + (source.snapshot?.snapshot.tasks.length ?? 0), 0);
+ $('swarm-counts').textContent = `${swarm.sources.length} registered sources · ${cells.toLocaleString()} recorded cells · ${tasks.toLocaleString()} tasks`;
+ const stale = swarm.sources.filter(source => sourceStatus(source) !== 'observed').length;
+ $('swarm-status').textContent = swarm.sample ? `Design preview · sample data · ${cells.toLocaleString()} cells` : swarm.sources.length ? `Live tracking · five-second observations · ${stale} stale/unavailable sources` : 'Observation unavailable · no registered facts';
+ $('swarm-status').classList.toggle('sample-notice', !!swarm.sample);
+}
+function observe(value: unknown): void {
+ const wasFirst = firstObservation;
+ const next = parseSwarm(value), previous = new Map(swarm.sources.map(source => [source.id, source]));
+ // Standalone and embedded readers both preserve each authority's own floor.
+ next.sources = next.sources.map(source => {
+ const old = previous.get(source.id);
+ if (!old || old.factoryId !== source.factoryId) return source;
+ if (!source.snapshot || (old.snapshot && source.snapshot.revision < old.snapshot.revision)) return { ...source, snapshot: old.snapshot, status: old.snapshot ? 'stale' : 'unavailable', events: old.events };
+ return source;
+ });
+ swarm = next; index = new SwarmIndex(next);
+ if (selection && !validSelection(selection)) selection = null;
+ if (firstObservation) { const wanted = selectionHash(); if (wanted && validSelection(wanted)) selection = wanted; firstObservation = false; }
+ const active = [...index.nodes.values()].filter(node => node.activity === 'recent').map(node => node.key).sort().join('|');
+ const changedActivity = active && active !== lastActivity; lastActivity = active;
+ redraw();
+ if (follow && changedActivity) { const node = [...index.nodes.values()].find(node => node.activity === 'recent'); if (node) select(node.selection); }
+ else if (wasFirst) frameSelection();
+}
+
+createRenderer();
+($('swarm-search') as HTMLInputElement).oninput = paintSearch;
+($('swarm-mode') as HTMLSelectElement).onchange = event => { mode = (event.target as HTMLSelectElement).value as '3d' | '2d'; createRenderer(); };
+($('swarm-follow') as HTMLInputElement).onchange = event => { follow = (event.target as HTMLInputElement).checked; };
+$('swarm-home').onclick = () => select(null);
+$('swarm-close').onclick = () => select(null, false);
+$('swarm-explore').onclick = () => { const hidden = document.body.classList.toggle('navigation-hidden'); $('swarm-explore').setAttribute('aria-expanded', String(!hidden)); };
+$('swarm-find').onclick = () => { document.body.classList.remove('navigation-hidden'); document.body.classList.add('search-open'); $('swarm-search').focus(); };
+window.addEventListener('keydown', event => {
+ if (event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement) return;
+ if (event.key === '/' || ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k')) { event.preventDefault(); document.body.classList.remove('navigation-hidden'); document.body.classList.add('search-open'); $('swarm-search').focus(); }
+ if (event.key === 'Escape' || event.key === 'Backspace') { const node = focusNode(), parent = node?.parent && index.nodes.get(node.parent); event.preventDefault(); select(parent ? parent.selection : null); }
+ if ((event.key === '[' || event.key === ']') && swarm.sources.length) {
+ const position = Math.max(0, swarm.sources.findIndex(source => source.id === selection?.sourceId)), source = swarm.sources[(position + (event.key === '[' ? -1 : 1) + swarm.sources.length) % swarm.sources.length]!;
+ event.preventDefault(); select({ sourceId: source.id, kind: 'source', id: source.id });
+ }
+ if (event.target === $('swarm-canvas') && selection) {
+ const node = focusNode();
+ if (event.key === 'ArrowUp') { const parent = node?.parent && index.nodes.get(node.parent); if (parent) { event.preventDefault(); select(parent.selection); } }
+ if (event.key === 'ArrowDown') { const child = node && index.nodes.get(index.children.get(node.key)?.[0] ?? ''); if (child) { event.preventDefault(); select(child.selection); } }
+ if ((event.key === 'ArrowLeft' || event.key === 'ArrowRight') && node?.parent) {
+ const siblings = index.children.get(node.parent) ?? [], current = siblings.indexOf(node.key), next = siblings[(current + (event.key === 'ArrowLeft' ? -1 : 1) + siblings.length) % siblings.length], sibling = next && index.nodes.get(next);
+ if (sibling) { event.preventDefault(); select(sibling.selection); }
+ }
+ }
+});
+window.addEventListener('hashchange', () => { const wanted = selectionHash(); if (!wanted || validSelection(wanted)) select(wanted); });
+$('swarm-stage').addEventListener('swarm-renderer-lost', () => { mode = '2d'; createRenderer(); $('swarm-status').textContent += ' · WebGL unavailable; 2D map active'; });
+const expiryTimer = setInterval(() => {
+ // Reclassify evidence in linear time; no force-layout/rebuild on every frame.
+ let changed = false;
+ for (const source of index.sources.values()) {
+ if (source.status === 'observed' && sourceStatus(source) === 'stale') { source.status = 'stale'; changed = true; }
+ const activity = activities(source, Date.now(), swarm.sample);
+ for (const cell of source.snapshot?.snapshot.cells ?? []) { const node = index.nodes.get(nodeKey(source, 'cell', cell.id)); if (node && node.activity !== (activity.get(cell.id) ?? 'idle')) { node.activity = activity.get(cell.id) ?? 'idle'; changed = true; } }
+ }
+ if (changed) redraw();
+}, 2000);
+
+if (embedded) {
+ if (!/^[a-zA-Z0-9_-]{16,128}$/.test(channel) || window.parent === window) $('swarm-status').textContent = 'Invalid viewer host channel';
+ else {
+ window.addEventListener('message', event => {
+ if (event.source !== window.parent || event.origin !== location.origin || event.data?.protocol !== 'brain-swarm/1' || event.data.channel !== channel) return;
+ if (event.data.type === 'ping') { window.parent.postMessage({ protocol: 'brain-swarm/1', type: 'ready', channel }, location.origin); return; }
+ if (event.data.type !== 'observe' || !Number.isSafeInteger(event.data.sequence) || event.data.sequence <= frameSequence) return;
+ try { observe(event.data.model); frameSequence = event.data.sequence; }
+ catch { $('swarm-status').textContent = 'Host observation rejected · existing facts retained'; }
+ });
+ window.parent.postMessage({ protocol: 'brain-swarm/1', type: 'ready', channel }, location.origin);
+ }
+} else if (preview) {
+ const count = Number(query.get('cells') ?? 54);
+ observe(swarmPreview(Number.isSafeInteger(count) && count >= 3 && count <= 10000 ? count : 54));
+} else {
+ // Same-origin registered bridge only; never accept a remote destination/token.
+ const sourcePath = query.get('source') ?? '/api/factory/swarm';
+ let stopped = false, timer: ReturnType, controller: AbortController | null = null;
+ const path = new URL(sourcePath, location.origin);
+ if (!sourcePath.startsWith('/') || sourcePath.startsWith('//') || path.origin !== location.origin || path.search || path.hash) $('swarm-status').textContent = 'A same-origin read bridge is required';
+ else {
+ const poll = async () => {
+ if (document.hidden) { timer = setTimeout(() => void poll(), 5000); return; }
+ controller = new AbortController(); const deadline = setTimeout(() => controller?.abort(), 12000);
+ try {
+ const response = await fetch(path.pathname, { cache: 'no-store', signal: controller.signal, credentials: 'same-origin' });
+ if ([401, 403, 404].includes(response.status)) { observe(empty()); throw new Error('Observation access unavailable'); }
+ if (!response.ok) throw new Error('Swarm bridge unavailable');
+ const body = await response.text(); if (body.length > 32 * 1024 * 1024) throw new Error('Swarm observation exceeds its bound');
+ if (!stopped) observe(JSON.parse(body));
+ } catch {
+ if (!stopped) { swarm.sources = swarm.sources.map(source => ({ ...source, status: source.snapshot ? 'stale' : 'unavailable' })); index = new SwarmIndex(swarm); redraw(); $('swarm-status').textContent = 'Observation unavailable · retained facts are stale'; }
+ } finally { clearTimeout(deadline); if (!stopped) timer = setTimeout(() => void poll(), 5000); }
+ };
+ void poll();
+ }
+ window.addEventListener('pagehide', () => { stopped = true; controller?.abort(); clearTimeout(timer); });
+}
+window.addEventListener('pagehide', () => { clearInterval(expiryTimer); renderer.dispose(); });
diff --git a/src/swarm/model.ts b/src/swarm/model.ts
new file mode 100644
index 0000000..a2edccc
--- /dev/null
+++ b/src/swarm/model.ts
@@ -0,0 +1,177 @@
+/** Public read-only transport, shared structurally with the Brain Online adapter.
+ * No provider locations, credentials, commands or arbitrary navigation URLs. */
+export interface Cell { id: string; parentId: string | null; depth: number; role: 'coordinator' | 'developer' | 'verifier' | 'watcher'; status: 'reserved' | 'ready' | 'retired'; purpose: string; heartbeat: string; allocationCents: number; budgetBasis: 'logical-allocation' }
+export interface Task { id: string; projectId: string; branch: string; status: 'ready' | 'running' | 'review' | 'verified' | 'delivered' | 'rejected' | 'completed' | 'cancelled'; owner: string | null; attempt: number; leaseExpiry: string | null; specDigest: string; candidate: { sha256: string } | null; review: { accepted: boolean; reviewerId: string; evidenceDigest: string; candidateDigest: string; specDigest: string; attempt: number } | null }
+export interface Effect { key: string; kind: 'provision' | 'flow_call' | 'retire' | 'delivery'; state: 'accepted' | 'running' | 'unknown' | 'succeeded' | 'not_applied'; owner: string; taskId: string | null; scope: string; scopeId: string; ownerEpoch: number; controlEpoch: number; createdAt: string; updatedAt: string; requestDigest: string }
+export interface Change { seq: number; type: string; subject: string; observedAt: string }
+export interface Snapshot {
+ schemaVersion: 1; factoryId: string; revision: number; cursor: string; observedAt: string; scope: 'local-coordinator'; buildRevision: string;
+ capabilities: { snapshot: true; events: true; commands: false };
+ snapshot: { control: { status: 'active' | 'paused'; epoch: number; mission: string }; cells: Cell[]; tasks: Task[]; effects: Effect[]; workerQuiescence: 'unverified' };
+}
+export interface Source { id: string; label: string; factoryId: string; status: 'observed' | 'stale' | 'unavailable'; snapshot: Snapshot | null; events: Change[] }
+export interface Swarm { schemaVersion: 1; scope: 'operator-source-registry'; observedAt: string; commands: false; sources: Source[]; sample?: boolean }
+export interface Selection { sourceId: string; kind: 'source' | 'cell' | 'task' | 'effect'; id: string }
+export interface Point { x: number; y: number; z: number }
+export interface Node { key: string; selection: Selection; source: Source; cell: Cell | null; label: string; parent: string | null; point: Point; activity: 'idle' | 'recent' | 'uncertain' }
+
+const record = (value: unknown): Record => { if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid observation.'); return value as Record; };
+const text = (value: unknown, max = 128): string => { if (typeof value !== 'string' || value.length > max) throw new Error('Invalid observation text.'); return value; };
+const id = (value: unknown): string => { const result = text(value); if (!result) throw new Error('Missing identity.'); return result; };
+const num = (value: unknown): number => { if (!Number.isSafeInteger(value) || (value as number) < 0) throw new Error('Invalid observation sequence.'); return value as number; };
+const date = (value: unknown): string => { const result = text(value, 64); if (!/^\d{4}-\d{2}-\d{2}T/.test(result) || !Number.isFinite(Date.parse(result))) throw new Error('Invalid observation time.'); return result; };
+const digest = (value: unknown): string => { const result = text(value); if (!/^[a-f0-9]{64}$/.test(result)) throw new Error('Invalid evidence hash.'); return result; };
+const choice = (value: unknown, values: readonly T[]): T => { if (!values.includes(value as T)) throw new Error('Unsupported observation value.'); return value as T; };
+const array = (value: unknown, max: number): unknown[] => { if (!Array.isArray(value) || value.length > max) throw new Error('Observation exceeds its bounds.'); return value; };
+const optionalId = (value: unknown): string | null => value === null ? null : id(value);
+const unique = (values: string[]) => { if (new Set(values).size !== values.length) throw new Error('Duplicate observation identity.'); };
+
+/** Positive allowlist even when running outside Brain Online's validated host. */
+export function parseSwarm(value: unknown): Swarm {
+ const input = record(value);
+ if (input.schemaVersion !== 1 || input.scope !== 'operator-source-registry' || input.commands !== false) throw new Error('Unsupported swarm observation.');
+ const sources = array(input.sources, 32).map(value => {
+ const source = record(value), sourceId = id(source.id), factoryId = id(source.factoryId);
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(sourceId)) throw new Error('Invalid registered source.');
+ let snapshot: Snapshot | null = null;
+ if (source.snapshot !== null) {
+ const envelope = record(source.snapshot), state = record(envelope.snapshot), control = record(state.control), caps = record(envelope.capabilities);
+ if (envelope.schemaVersion !== 1 || envelope.factoryId !== factoryId || envelope.scope !== 'local-coordinator' || caps.commands !== false || caps.snapshot !== true || caps.events !== true || state.workerQuiescence !== 'unverified') throw new Error('Unsupported FACTORY authority.');
+ const cells = array(state.cells, 10000).map(value => {
+ const cell = record(value);
+ return { id: id(cell.id), parentId: optionalId(cell.parentId), depth: num(cell.depth),
+ role: choice(cell.role, ['coordinator', 'developer', 'verifier', 'watcher']), status: choice(cell.status, ['reserved', 'ready', 'retired']),
+ heartbeat: date(cell.heartbeat), purpose: text(cell.purpose, 512), allocationCents: num(cell.allocationCents), budgetBasis: choice(cell.budgetBasis, ['logical-allocation']) };
+ });
+ const tasks = array(state.tasks, 10000).map(value => {
+ const task = record(value), review = task.review === null ? null : record(task.review);
+ if (review && typeof review.accepted !== 'boolean') throw new Error('Invalid review.');
+ return { id: id(task.id), projectId: id(task.projectId), branch: text(task.branch, 256),
+ status: choice(task.status, ['ready', 'running', 'review', 'verified', 'delivered', 'rejected', 'completed', 'cancelled']),
+ attempt: num(task.attempt), owner: optionalId(task.owner), leaseExpiry: task.leaseExpiry === null ? null : date(task.leaseExpiry), specDigest: digest(task.specDigest),
+ candidate: task.candidate === null ? null : { sha256: digest(record(task.candidate).sha256) },
+ review: review ? { accepted: review.accepted as boolean, reviewerId: id(review.reviewerId), evidenceDigest: digest(review.evidenceDigest), candidateDigest: digest(review.candidateDigest), specDigest: digest(review.specDigest), attempt: num(review.attempt) } : null };
+ });
+ const effects = array(state.effects, 10000).map(value => {
+ const effect = record(value);
+ return { key: id(effect.key), kind: choice(effect.kind, ['provision', 'flow_call', 'retire', 'delivery']), state: choice(effect.state, ['accepted', 'running', 'unknown', 'succeeded', 'not_applied']),
+ owner: id(effect.owner), taskId: optionalId(effect.taskId), scope: choice(effect.scope, ['task', 'project', 'cleanup']), scopeId: id(effect.scopeId),
+ ownerEpoch: num(effect.ownerEpoch), controlEpoch: num(effect.controlEpoch), createdAt: date(effect.createdAt), updatedAt: date(effect.updatedAt), requestDigest: digest(effect.requestDigest) };
+ });
+ unique(cells.map(cell => cell.id)); unique(tasks.map(task => task.id)); unique(effects.map(effect => effect.key));
+ const cursor = text(envelope.cursor, 32), buildRevision = text(envelope.buildRevision, 40);
+ if (!/^[A-Za-z0-9_-]{1,32}$/.test(cursor) || !(buildRevision === 'unknown' || /^[a-f0-9]{40}$/.test(buildRevision))) throw new Error('Invalid provenance.');
+ snapshot = { schemaVersion: 1, factoryId, revision: num(envelope.revision), cursor, observedAt: date(envelope.observedAt), scope: 'local-coordinator', buildRevision,
+ capabilities: { snapshot: true, events: true, commands: false }, snapshot: { control: { status: choice(control.status, ['active', 'paused']), epoch: num(control.epoch), mission: text(control.mission, 2048) }, cells, tasks, effects, workerQuiescence: 'unverified' } };
+ }
+ const status = choice(source.status, ['observed', 'stale', 'unavailable']);
+ if ((status === 'observed' || status === 'stale') && !snapshot) throw new Error('Missing recorded source.');
+ const events = array(source.events ?? [], 1000).map(value => { const event = record(value); return { seq: num(event.seq), type: id(event.type), subject: text(event.subject), observedAt: date(event.observedAt) }; });
+ return { id: sourceId, label: id(source.label), factoryId, status, snapshot, events };
+ });
+ unique(sources.map(source => source.id));
+ return { schemaVersion: 1, scope: 'operator-source-registry', commands: false, observedAt: date(input.observedAt), sources, sample: input.sample === true };
+}
+
+export const nodeKey = (source: Pick, kind: string, localId: string) => JSON.stringify([source.id, source.factoryId, kind, localId]);
+export const selectionKey = (source: Source, selection: Selection) => nodeKey(source, selection.kind, selection.id);
+const hash = (value: string) => { let result = 2166136261; for (let i = 0; i < value.length; i++) result = Math.imul(result ^ value.charCodeAt(i), 16777619); return (result >>> 0) / 4294967296; };
+
+/** O(cells + tasks + effects), including expiry independent of read time. */
+export function activities(source: Source, now: number, sample = false): Map {
+ const state = source.snapshot?.snapshot, result = new Map();
+ if (!state) return result;
+ const running = new Map();
+ for (const task of state.tasks) if (task.status === 'running' && task.owner) running.set(task.owner, Math.min(running.get(task.owner) ?? Infinity, task.leaseExpiry ? Date.parse(task.leaseExpiry) : 0));
+ const unknown = new Set(state.effects.filter(effect => effect.state === 'unknown').map(effect => effect.owner));
+ const observedAge = now - Date.parse(source.snapshot!.observedAt);
+ for (const cell of state.cells) {
+ const expiry = running.get(cell.id), heartbeatAge = now - Date.parse(cell.heartbeat);
+ result.set(cell.id, expiry === undefined ? 'idle' : source.status !== 'observed' || (!sample && (observedAge > 15000 || observedAge < -5000)) || state.control.status !== 'active' || cell.status !== 'ready' || heartbeatAge < 0 || heartbeatAge > 60000 || expiry <= now || unknown.has(cell.id) ? 'uncertain' : 'recent');
+ }
+ return result;
+}
+
+/** Full searchable index, stable source clusters and hierarchy. No force solver. */
+export class SwarmIndex {
+ readonly nodes = new Map();
+ readonly children = new Map();
+ readonly sources = new Map();
+ readonly issues: string[] = [];
+ constructor(readonly swarm: Swarm, now = Date.now()) {
+ const sorted = [...swarm.sources].sort((a, b) => a.id.localeCompare(b.id));
+ sorted.forEach((source, index) => {
+ this.sources.set(source.id, source);
+ const angle = index * 2.3999632297, radius = sorted.length === 1 ? 0 : 95 * Math.sqrt(index + 1);
+ const sourceKey = nodeKey(source, 'source', source.id), centre = { x: Math.cos(angle) * radius, y: 0, z: Math.sin(angle) * radius };
+ this.nodes.set(sourceKey, { key: sourceKey, source, cell: null, selection: { sourceId: source.id, kind: 'source', id: source.id }, label: source.label, parent: null, point: { ...centre, y: -18 }, activity: 'idle' });
+ const cells = source.snapshot?.snapshot.cells ?? [], byId = new Map(cells.map(cell => [cell.id, cell]));
+ const activity = activities(source, now, swarm.sample);
+ const resolved = new Map(), visiting = new Set();
+ // Iterative ancestor traversal handles a 10,000-level chain without stack overflow.
+ const locate = (cell: Cell): Point => {
+ if (resolved.has(cell.id)) return resolved.get(cell.id)!;
+ const path: Cell[] = []; let cursor: Cell | undefined = cell;
+ while (cursor && !resolved.has(cursor.id) && !visiting.has(cursor.id)) {
+ visiting.add(cursor.id); path.push(cursor); cursor = cursor.parentId ? byId.get(cursor.parentId) : undefined;
+ }
+ if (cursor && visiting.has(cursor.id)) this.issues.push(`${source.label}: delegation cycle at ${cursor.id}`);
+ let point = cursor && resolved.get(cursor.id) || centre;
+ for (const next of path.reverse()) {
+ const phase = hash(nodeKey(source, 'cell', next.id)) * Math.PI * 2;
+ const distance = next.parentId ? Math.max(8, 38 / Math.sqrt(Math.min(next.depth + 1, 12))) : 0;
+ point = { x: point.x + Math.cos(phase) * distance, y: point.y + Math.sin(phase * 1.7) * distance * .4 + (next.parentId ? 5 : 0), z: point.z + Math.sin(phase) * distance };
+ resolved.set(next.id, point); visiting.delete(next.id);
+ }
+ return resolved.get(cell.id)!;
+ };
+ for (const cell of cells) {
+ const key = nodeKey(source, 'cell', cell.id), parent = cell.parentId && byId.has(cell.parentId) ? nodeKey(source, 'cell', cell.parentId) : sourceKey;
+ if (cell.parentId && !byId.has(cell.parentId)) this.issues.push(`${source.label}: parent of ${cell.id} is not observed`);
+ this.nodes.set(key, { key, source, cell, selection: { sourceId: source.id, kind: 'cell', id: cell.id }, label: cell.id, parent, point: locate(cell), activity: activity.get(cell.id) ?? 'idle' });
+ const children = this.children.get(parent) ?? []; children.push(key); this.children.set(parent, children);
+ }
+ });
+ for (const children of this.children.values()) children.sort();
+ }
+ ancestors(key: string): Node[] {
+ const path: Node[] = [], seen = new Set(); let node = this.nodes.get(key);
+ while (node && !seen.has(node.key)) { seen.add(node.key); path.push(node); node = node.parent ? this.nodes.get(node.parent) : undefined; }
+ return path.reverse();
+ }
+ /** Every indexed identity remains navigable; only the drawing is bounded. */
+ visible(focus: string | null, page = 0, limit = 600): Node[] {
+ if (this.nodes.size <= limit) return [...this.nodes.values()];
+ const keys = new Set([...this.nodes.values()].filter(node => !node.cell).map(node => node.key));
+ if (focus) {
+ for (const node of this.ancestors(focus).slice(-32)) keys.add(node.key);
+ const nearby = this.children.get(focus) ?? [];
+ for (const key of nearby.slice(page * 128, (page + 1) * 128)) keys.add(key);
+ const parent = this.nodes.get(focus)?.parent;
+ if (parent) for (const key of (this.children.get(parent) ?? []).slice(0, 128)) keys.add(key);
+ } else {
+ for (const source of this.sources.values()) {
+ const root = nodeKey(source, 'cell', 'root'); if (this.nodes.has(root)) keys.add(root);
+ for (const key of (this.children.get(root) ?? []).slice(0, 12)) keys.add(key);
+ }
+ }
+ return [...keys].slice(0, limit).map(key => this.nodes.get(key)!).filter(Boolean);
+ }
+ search(query: string): Selection[] {
+ const needle = query.trim().toLowerCase(), matches: Selection[] = [];
+ if (!needle) return matches;
+ for (const node of this.nodes.values()) if (`${node.label} ${node.source.label} ${node.source.factoryId} ${node.cell?.purpose ?? ''}`.toLowerCase().includes(needle)) matches.push(node.selection);
+ for (const source of this.sources.values()) {
+ for (const task of source.snapshot?.snapshot.tasks ?? []) if (`${task.id} ${task.projectId} ${task.branch}`.toLowerCase().includes(needle)) matches.push({ sourceId: source.id, kind: 'task', id: task.id });
+ for (const effect of source.snapshot?.snapshot.effects ?? []) if (`${effect.key} ${effect.kind}`.toLowerCase().includes(needle)) matches.push({ sourceId: source.id, kind: 'effect', id: effect.key });
+ }
+ return matches;
+ }
+ nodeFor(selection: Selection): Node | undefined {
+ const source = this.sources.get(selection.sourceId); if (!source) return;
+ if (selection.kind === 'cell' || selection.kind === 'source') return this.nodes.get(selectionKey(source, selection));
+ const state = source.snapshot?.snapshot;
+ const owner = selection.kind === 'task' ? state?.tasks.find(task => task.id === selection.id)?.owner : state?.effects.find(effect => effect.key === selection.id)?.owner;
+ return this.nodes.get(nodeKey(source, owner ? 'cell' : 'source', owner ?? source.id));
+ }
+}
diff --git a/src/swarm/observatory.css b/src/swarm/observatory.css
new file mode 100644
index 0000000..f5c2125
--- /dev/null
+++ b/src/swarm/observatory.css
@@ -0,0 +1,126 @@
+/* Observatory art direction: source plates and a wide field, with instruments
+ * floating over it. All motion and semantic colouring still follow evidence. */
+:root {
+ --ink:#0a0e14; --panel:#10151d; --rule:#26313d; --text:#e8edf4;
+ --muted:#a7b2c2; --quiet:#7d8798; --signal:#7fd1de;
+ --serif:ui-serif,'Iowan Old Style','Palatino Linotype',Palatino,Georgia,serif;
+ --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
+ background:var(--ink); color:var(--text);
+}
+body{background:var(--ink)}
+button,input,select{border-radius:2px}
+button:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--signal);outline-offset:2px}
+button{background:transparent;border-color:var(--rule)}
+button:hover{background:#172431;border-color:#506675}
+.swarm-top{height:64px;padding:0 24px;border-color:var(--rule);background:#0c1017;gap:20px}
+.swarm-brand{font-family:var(--serif);font-size:21px;letter-spacing:-.04em}
+.swarm-brand b{color:#b9d8e7;font-weight:400}
+.swarm-top button{font-size:11px;padding:6px 9px;color:var(--muted)}
+#swarm-status{font-family:var(--mono);font-size:10px;color:var(--muted);text-align:right}
+.read-only{font-family:var(--mono);font-size:9px;letter-spacing:.08em;color:var(--quiet);border-color:var(--rule);border-radius:2px}
+.swarm-shell{display:block;position:relative;height:calc(100vh - 108px);min-height:640px}
+.swarm-space{position:absolute;inset:0;background:radial-gradient(ellipse at 50% 48%,#1a273940,transparent 58%),radial-gradient(ellipse at 72% 55%,#252b4233,transparent 45%)}
+#swarm-stage{height:100%;min-height:0}
+.space-toolbar{position:absolute;top:18px;left:310px;right:24px;z-index:8;border:0;padding:0;gap:14px;align-items:flex-start}
+.has-selection .space-toolbar{right:382px}
+.space-controls{font-size:10px;gap:10px;color:var(--muted)}
+.space-controls button,.space-controls select{font-size:10px;padding:5px 7px}
+.follow-control{color:var(--quiet);font-size:10px}
+#swarm-path{font-family:var(--mono);font-size:10px}
+#swarm-path button{font-size:10px;padding:4px 7px;border-color:transparent;max-width:140px}
+#swarm-path button[aria-pressed=true]{color:var(--signal);background:transparent;border-bottom-color:var(--signal)}
+.swarm-navigation{position:absolute;z-index:10;left:20px;top:18px;bottom:76px;width:266px;border:1px solid var(--rule);padding:20px 17px;background:#10151de8;overflow:auto}
+.navigation-hidden .swarm-navigation{display:none}
+.navigation-hidden .space-toolbar{left:24px}
+.navigation-hidden .space-caption{left:32px}
+.section-title{font:9px var(--mono);letter-spacing:.15em;color:var(--quiet);margin-bottom:18px}
+.search-label{font-size:10px;color:var(--muted)}
+input[type=search]{font-size:11px;background:#0b1119;border-color:#2c3645;padding:10px;color:var(--text)}
+#source-list{margin:22px 0}
+#source-list button{font:15px var(--serif);padding:11px 8px 10px 12px;border:0;border-left:2px solid #7fd1de;border-bottom:1px solid var(--rule);margin:0;border-radius:0;background:transparent}
+#source-list button:nth-child(2){border-left-color:#9bb8e8}
+#source-list button:nth-child(3){border-left-color:#c2a7e8}
+#source-list button:nth-child(4){border-left-color:#e8a7b8}
+#source-list button:nth-child(5){border-left-color:#e8c98a}
+#source-list button small{font:10px var(--mono);color:var(--quiet);margin-top:5px}
+#source-list button[aria-pressed=true]{background:#1a2936;color:#e8f7fb}
+#source-list button.source-stale,#source-list button.source-unavailable{background:repeating-linear-gradient(45deg,transparent 0 4px,#a7b2c208 4px 5px);color:var(--muted)}
+#branch-list{max-height:36vh}
+#branch-list button{font-size:11px;border:0;border-bottom:1px solid #202b35;border-radius:0;background:transparent;padding:9px 6px;margin:0}
+#branch-list button small{font:9px var(--mono);color:var(--quiet)}
+#branch-pages{font:10px var(--mono);color:var(--quiet)}
+#branch-pages button{font-size:10px;margin-top:8px}
+#search-results button{border:0;border-bottom:1px solid var(--rule);font-size:11px;border-radius:0;padding:8px 4px}
+.scope-note{font-size:10px;color:var(--quiet);border-top:1px dotted #33414c;padding-top:14px;line-height:1.6}
+.space-caption{left:320px;bottom:92px;color:var(--muted);font:11px var(--mono)}
+.observatory-title{font:38px var(--serif);letter-spacing:-.025em;color:#dee8f2;margin-bottom:10px}
+.has-selection .observatory-title{font-size:27px}
+.space-caption p{font:10px var(--mono);color:var(--quiet);margin-top:8px}
+.swarm-label{font:10px var(--mono);color:#97adbb;text-shadow:0 1px 4px #050a10}
+.swarm-label.source-label{font:17px var(--serif);letter-spacing:.015em;color:#c7e0e7}
+.swarm-label.selected{color:#eafcff}
+.swarm-inspector{display:none;position:absolute;z-index:12;right:20px;top:76px;bottom:76px;width:340px;padding:22px;background:#10151df2;border:1px solid var(--rule);overflow:auto}
+.has-selection .swarm-inspector{display:block}
+.swarm-inspector>.section-title{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--rule);padding-bottom:14px}
+#swarm-close{font-size:18px;padding:0 5px;color:var(--quiet);border:0}
+.swarm-inspector h2{font:26px/1.15 var(--serif);letter-spacing:-.025em;margin:18px 0}
+.swarm-inspector h3{font:15px var(--serif);color:#d3e5e9;border-top:1px solid var(--rule);padding-top:18px;margin-top:26px}
+.swarm-inspector p{font-size:12px;color:var(--muted);line-height:1.65}
+.swarm-inspector dl{display:block;font:11px/1.5 var(--mono)}
+.swarm-inspector dt{color:var(--quiet);padding-top:12px;margin:0;border-top:1px solid #25303a}
+.swarm-inspector dd{margin:3px 0 12px;color:#d0dae5}
+.swarm-inspector button:not(#swarm-close){display:block;width:100%;text-align:left;font-size:11px;border:0;border-bottom:1px solid var(--rule);border-radius:0;padding:10px 0;margin:0;color:#bbd9df}
+.source-state{font:9px var(--mono);letter-spacing:.1em;color:var(--signal);line-height:1.6}
+.evidence-note{border:0!important;border-left:2px solid #d7a86b!important;background:#2b251d60!important;border-radius:0;padding:11px 13px;color:#d4bc96!important}
+.swarm-timeline{position:absolute;bottom:20px;left:310px;z-index:14;width:min(490px,calc(100% - 710px));padding:14px 16px;border:1px solid var(--rule);background:#10151dec}
+.swarm-timeline summary{margin:0;cursor:pointer;font:9px var(--mono);letter-spacing:.12em;color:var(--muted)}
+.swarm-timeline summary span{font-size:9px;color:var(--quiet);letter-spacing:0}
+#tracking-list{font-family:var(--mono);font-size:10px;max-height:180px;padding-top:10px}
+.swarm-measurements{position:absolute;right:20px;bottom:27px;z-index:16;font:9px var(--mono);color:var(--quiet);max-width:300px;text-align:right}
+.swarm-measurements summary{cursor:pointer}
+.swarm-diagnostics{font:9px/1.8 var(--mono);border:1px solid var(--rule);background:#10151df2;margin-top:10px;padding:10px;color:var(--muted);text-align:left}
+.sample-notice{color:#dfba80!important}
+footer{height:44px;position:relative;border-color:var(--rule);font:9px/1.65 var(--mono);color:var(--quiet);padding:14px 24px;background:#0c1017}
+@media(max-width:1120px){
+ .swarm-navigation{width:220px;left:14px;padding:18px 14px}
+ .space-toolbar{left:252px;right:18px}
+ .has-selection .space-toolbar{right:316px}
+ .swarm-inspector{width:278px;right:14px;padding:18px;top:100px}
+ .space-caption{left:256px;bottom:100px}.observatory-title{font-size:32px}
+ .swarm-timeline{left:252px;width:calc(100% - 568px)}
+}
+@media(max-width:800px){
+ .swarm-top{height:auto;min-height:76px;gap:12px;padding:12px 16px;position:sticky;top:0;z-index:30}
+ #swarm-status{order:4;flex-basis:100%;text-align:left;font-size:9px}
+ .swarm-brand{font-size:19px}.read-only{margin-left:auto}.swarm-top button{font-size:10px}
+ .swarm-shell{height:auto;min-height:0;display:block}
+ .swarm-space{position:relative;height:600px}
+ #swarm-stage{height:100%}
+ .space-toolbar,.has-selection .space-toolbar{top:14px;left:14px;right:14px;justify-content:space-between;gap:10px}
+ .space-controls{gap:6px;font-size:9px}.space-controls label{font-size:9px}
+ .swarm-navigation{left:14px;right:14px;top:92px;bottom:auto;width:auto;max-height:220px;padding:10px 12px;background:#10151ddb}
+ .swarm-navigation>.section-title:first-child,.search-label,#search-results,.scope-note{display:none}
+ .search-open .search-label,.search-open #search-results{display:block}
+ .search-open .search-label input{margin-top:7px}
+ .search-open .swarm-navigation{max-height:360px}
+ #source-list{margin:0;display:flex;gap:10px;overflow:auto}
+ #source-list button{min-width:150px;width:auto;padding:8px 10px;font-size:14px;border-bottom:0}
+ #source-list button small{font-size:9px}
+ #branch-heading{font-size:8px;margin:10px 0 6px}
+ #branch-list{max-height:98px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px}
+ #branch-list button{font-size:10px;padding:7px 4px}
+ #branch-list button small{font-size:8px}
+ .space-caption,.navigation-hidden .space-caption{left:20px;bottom:95px;font-size:9px}.observatory-title{font-size:32px}.has-selection .observatory-title{font-size:27px}
+ .swarm-timeline{left:14px;right:14px;bottom:20px;width:auto;padding:12px;max-width:calc(100% - 28px)}
+ .swarm-measurements{right:14px;bottom:67px;font-size:8px}
+ .swarm-inspector{position:relative;right:auto;left:auto;top:auto;bottom:auto;width:auto;margin:0 14px 20px;padding:22px;max-height:none}
+ .swarm-inspector dl{display:block}.swarm-inspector dd{margin:4px 0 13px}
+ footer{height:auto;padding:14px 16px}
+}
+@media(max-width:520px){
+ .swarm-brand{font-size:18px}.swarm-top{gap:9px}#swarm-find{margin-left:auto}.read-only{display:none}
+ .swarm-space{height:540px}.space-toolbar{align-items:flex-start}.space-controls{flex-basis:100%}
+ .swarm-navigation{top:104px}.space-caption{max-width:calc(100% - 40px)}
+ .space-caption p{font-size:9px}.observatory-title{font-size:30px}
+ .swarm-inspector h2{font-size:27px}
+}
diff --git a/src/swarm/preview.ts b/src/swarm/preview.ts
new file mode 100644
index 0000000..4014341
--- /dev/null
+++ b/src/swarm/preview.ts
@@ -0,0 +1,24 @@
+import type { Swarm, Source, Cell } from './model';
+
+/** Opt-in fixtures only; never used to replace a failed live read. */
+export function swarmPreview(totalCells = 54): Swarm {
+ const now = new Date().toISOString(), future = new Date(Date.now() + 5 * 60000).toISOString(), digest = 'a'.repeat(64);
+ const labels = ['FACTORY', 'Independent review', 'Delivery watch'];
+ const sources: Source[] = labels.map((label, sourceIndex) => {
+ const count = Math.floor(totalCells / 3) + (sourceIndex < totalCells % 3 ? 1 : 0);
+ const cells: Cell[] = Array.from({ length: Math.max(1, count) }, (_, index) => ({
+ id: index === 0 ? 'root' : `cell-${index}`, parentId: index === 0 ? null : index <= 4 ? 'root' : `cell-${Math.floor((index - 1) / 4)}`,
+ depth: index === 0 ? 0 : Math.floor(Math.log(index * 3 + 1) / Math.log(4)), role: index === 0 ? 'coordinator' : index % 5 === 0 ? 'verifier' : index % 7 === 0 ? 'watcher' : 'developer',
+ status: index % 17 === 16 ? 'retired' : 'ready', heartbeat: now,
+ purpose: index === 0 ? 'Coordinate the recorded local delegation.' : `Inspect the work assigned to ${label} cell ${index}.`,
+ allocationCents: index === 0 ? 10000 : 500, budgetBasis: 'logical-allocation',
+ }));
+ const tasks = cells.length > 1 ? [{ id: 'implementation', projectId: 'flujo', branch: 'codex/sample', status: 'running' as const, owner: 'cell-1', attempt: 1, leaseExpiry: future, specDigest: digest, candidate: null, review: null },
+ { id: 'retire-operation', projectId: 'flujo', branch: 'codex/sample-retire', status: 'completed' as const, owner: cells.length > 2 ? 'cell-2' : 'root', attempt: 1, leaseExpiry: null, specDigest: digest, candidate: null, review: null },
+ { id: 'exploration', projectId: 'flujo', branch: 'codex/sample-history', status: 'cancelled' as const, owner: 'root', attempt: 2, leaseExpiry: null, specDigest: digest, candidate: { sha256: digest }, review: { accepted: false, reviewerId: 'root', evidenceDigest: digest, candidateDigest: digest, specDigest: digest, attempt: 2 } }] : [];
+ return { id: `source-${sourceIndex + 1}`, label, factoryId: `design-authority-${sourceIndex + 1}`, status: 'observed', events: [],
+ snapshot: { schemaVersion: 1, factoryId: `design-authority-${sourceIndex + 1}`, scope: 'local-coordinator', revision: 42 + sourceIndex, cursor: 'NDI', observedAt: now, buildRevision: 'unknown', capabilities: { snapshot: true, events: true, commands: false },
+ snapshot: { control: { status: sourceIndex === 1 ? 'paused' : 'active', epoch: 3, mission: 'Improve FLUJO, one accepted delivery at a time.' }, cells, tasks, effects: sourceIndex === 2 ? [{ key: 'pending-effect', kind: 'flow_call', state: 'unknown', owner: 'cell-1', taskId: 'implementation', scope: 'task', scopeId: 'implementation', ownerEpoch: 1, controlEpoch: 3, createdAt: now, updatedAt: now, requestDigest: digest }] : [], workerQuiescence: 'unverified' } } };
+ });
+ return { schemaVersion: 1, scope: 'operator-source-registry', observedAt: now, commands: false, sources, sample: true };
+}
diff --git a/src/swarm/renderer.ts b/src/swarm/renderer.ts
new file mode 100644
index 0000000..ef305bb
--- /dev/null
+++ b/src/swarm/renderer.ts
@@ -0,0 +1,213 @@
+import { BufferGeometry, Color, Float32BufferAttribute, LineBasicMaterial, LineSegments, PerspectiveCamera, Points, Scene, ShaderMaterial, Vector3, WebGLRenderer } from 'three';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import type { Node, Point } from './model';
+
+const hues = ['#7fd1de', '#9bb8e8', '#c2a7e8', '#e8a7b8', '#e8c98a', '#8fd9b6'];
+const sourceHue = (id: string) => hues[[...id].reduce((sum, char) => (sum * 31 + char.charCodeAt(0)) >>> 0, 0) % hues.length]!;
+const tint = (node: Node): string => node.source.status !== 'observed' ? '#7d8798' : node.activity === 'uncertain' ? '#e0a85c' : node.cell?.status === 'retired' ? '#7d8798' : sourceHue(node.source.id);
+const vertexShader = `attribute float aSize; attribute float aActive; attribute float aFill; varying vec3 vColor; varying float vActive; varying float vFill; uniform float uTime;
+void main(){vColor=color; vActive=aActive; vFill=aFill; vec4 p=modelViewMatrix*vec4(position,1.); float pulse=1.+aActive*.08*sin(uTime*2.); gl_PointSize=clamp(aSize*pulse*180./max(1.,-p.z),7.,56.); gl_Position=projectionMatrix*p;}`;
+const fragmentShader = `varying vec3 vColor; varying float vActive; varying float vFill;
+void main(){float d=length(gl_PointCoord-.5)*2.; if(d>1.)discard; float ring=smoothstep(.7,.8,d)*(1.-smoothstep(.86,1.,d)); float core=(1.-smoothstep(.2,.54,d))*vFill; float glow=exp(-d*d*5.)*.12*vFill; gl_FragColor=vec4(mix(vColor,vec3(.92,.98,1.),core*.6),ring*.8+core*.7+glow);}`;
+
+/** Two semantic draw batches, bounded labels, no postprocessing or idle orbit.
+ * Rendering is demand-driven; evidenced activity pulses at no more than 30fps. */
+export class SwarmRenderer {
+ private gl: WebGLRenderer | null = null;
+ private ctx: CanvasRenderingContext2D | null = null;
+ private scene = new Scene();
+ private camera = new PerspectiveCamera(50, 1, .1, 20000);
+ private controls: OrbitControls | null = null;
+ private points: Points | null = null;
+ private lines: LineSegments | null = null;
+ private nodes: Node[] = [];
+ private selected: string | null = null;
+ private frameId = 0;
+ private active = false;
+ private lastFrame = 0;
+ private width = 1;
+ private height = 1;
+ private zoom = 1;
+ private pan = { x: 0, y: 0 };
+ private projected = new Map();
+ private drag: { x: number; y: number; startX: number; startY: number; moved: boolean } | null = null;
+ private observer: ResizeObserver;
+ private disposeInput: () => void;
+ private disposed = false;
+ private labelElements: HTMLSpanElement[] = [];
+ private submissions = 0;
+ private lastSubmissionMs = 0;
+ private flight: { start: number; fromPosition: Vector3; fromTarget: Vector3; toPosition: Vector3; toTarget: Vector3 } | null = null;
+ readonly mode: '3d' | '2d';
+ onPick: (node: Node) => void = () => undefined;
+ onStats: (stats: { mode: string; visible: number; labels: number; draws: number; submissions: number; cpuMs: number }) => void = () => undefined;
+
+ constructor(private canvas: HTMLCanvasElement, private labels: HTMLElement, requested: '3d' | '2d') {
+ if (requested === '3d') {
+ try {
+ this.gl = new WebGLRenderer({ canvas, antialias: false, powerPreference: 'low-power', alpha: true });
+ this.gl.setPixelRatio(Math.min(devicePixelRatio || 1, 1.25));
+ this.gl.setClearColor(0x081216, 0);
+ this.camera.position.set(40, 120, 180);
+ this.controls = new OrbitControls(this.camera, canvas);
+ this.controls.enableDamping = false;
+ this.controls.enablePan = true;
+ this.controls.minDistance = 8;
+ this.controls.maxDistance = 10000;
+ this.controls.addEventListener('change', this.request);
+ canvas.addEventListener('webglcontextlost', this.contextLost);
+ } catch { this.gl = null; }
+ }
+ if (!this.gl) {
+ // A failed GL creation may still bind its context; replace the canvas.
+ const replacement = canvas.cloneNode(false) as HTMLCanvasElement;
+ canvas.replaceWith(replacement); this.canvas = replacement;
+ this.ctx = replacement.getContext('2d');
+ if (!this.ctx) throw new Error('No canvas renderer is available.');
+ }
+ this.mode = this.gl ? '3d' : '2d';
+ this.observer = new ResizeObserver(this.resize);
+ this.observer.observe(this.canvas.parentElement!);
+ document.addEventListener('visibilitychange', this.visibility);
+ this.disposeInput = this.wireInput();
+ this.resize();
+ }
+ private contextLost = (event: Event) => { event.preventDefault(); this.active = false; this.canvas.dispatchEvent(new CustomEvent('swarm-renderer-lost', { bubbles: true })); };
+ private visibility = () => { if (document.hidden) { cancelAnimationFrame(this.frameId); this.frameId = 0; } else this.request(); };
+ private resize = () => {
+ const rect = this.canvas.parentElement!.getBoundingClientRect();
+ this.width = Math.max(1, rect.width); this.height = Math.max(1, rect.height);
+ if (this.gl) { this.gl.setSize(this.width, this.height, false); this.camera.aspect = this.width / this.height; this.camera.updateProjectionMatrix(); }
+ else { const ratio = Math.min(devicePixelRatio || 1, 1.25); this.canvas.width = this.width * ratio; this.canvas.height = this.height * ratio; this.ctx!.setTransform(ratio, 0, 0, ratio, 0, 0); }
+ this.request();
+ };
+ setNodes(nodes: Node[], selected: string | null): void {
+ this.nodes = nodes; this.selected = selected;
+ this.active = !matchMedia('(prefers-reduced-motion: reduce)').matches && nodes.some(node => node.activity === 'recent');
+ if (this.gl) {
+ const pos: number[] = [], colors: number[] = [], sizes: number[] = [], active: number[] = [], fill: number[] = [], linkPos: number[] = [], linkColors: number[] = [];
+ const byKey = new Map(nodes.map(node => [node.key, node]));
+ // Static engraved source plates share the delegation line batch. They
+ // are reference rings, not edges or activity. No cross-authority lines.
+ for (const source of nodes.filter(node => !node.cell)) {
+ const color = new Color(sourceHue(source.source.id));
+ for (const radius of [26, 49, 72]) for (let i = 0; i < 64; i++) for (const t of [i / 64, (i + 1) / 64]) {
+ const angle = t * Math.PI * 2;
+ linkPos.push(source.point.x + Math.cos(angle) * radius, 0, source.point.z + Math.sin(angle) * radius);
+ linkColors.push(color.r * .07, color.g * .07, color.b * .07);
+ }
+ }
+ for (const node of nodes) {
+ pos.push(node.point.x, node.point.y, node.point.z);
+ const color = new Color(node.key === selected ? '#e1fff5' : tint(node)); colors.push(color.r, color.g, color.b);
+ sizes.push(!node.cell ? 18 : node.key === selected ? 17 : node.cell.role === 'coordinator' ? 13 : 9); active.push(node.activity === 'recent' ? 1 : 0);
+ fill.push(node.source.status === 'observed' && node.cell?.status !== 'retired' && node.cell?.status !== 'reserved' && node.activity !== 'uncertain' ? 1 : 0);
+ const parent = node.parent ? byKey.get(node.parent) : undefined;
+ if (parent?.cell && node.cell?.parentId) {
+ // Short curved delegation links, one shared line batch.
+ for (let i = 0; i < 8; i++) {
+ for (const t of [i / 8, (i + 1) / 8]) {
+ linkPos.push(parent.point.x + (node.point.x - parent.point.x) * t, parent.point.y + (node.point.y - parent.point.y) * t + Math.sin(t * Math.PI) * 3, parent.point.z + (node.point.z - parent.point.z) * t);
+ const intensity = node.key === selected || parent.key === selected ? .65 : .2; linkColors.push(color.r * intensity, color.g * intensity, color.b * intensity);
+ }
+ }
+ }
+ }
+ const geometry = new BufferGeometry(); geometry.setAttribute('position', new Float32BufferAttribute(pos, 3)); geometry.setAttribute('color', new Float32BufferAttribute(colors, 3)); geometry.setAttribute('aSize', new Float32BufferAttribute(sizes, 1)); geometry.setAttribute('aActive', new Float32BufferAttribute(active, 1)); geometry.setAttribute('aFill', new Float32BufferAttribute(fill, 1));
+ if (this.points) { this.points.geometry.dispose(); this.points.geometry = geometry; }
+ else { this.points = new Points(geometry, new ShaderMaterial({ vertexShader, fragmentShader, vertexColors: true, transparent: true, depthWrite: false, uniforms: { uTime: { value: 0 } } })); this.scene.add(this.points); }
+ const links = new BufferGeometry(); links.setAttribute('position', new Float32BufferAttribute(linkPos, 3)); links.setAttribute('color', new Float32BufferAttribute(linkColors, 3));
+ if (this.lines) { this.lines.geometry.dispose(); this.lines.geometry = links; }
+ else { this.lines = new LineSegments(links, new LineBasicMaterial({ vertexColors: true, transparent: true, opacity: .9, depthWrite: false })); this.scene.add(this.lines); }
+ }
+ const priority = nodes.filter(node => node.key === selected).concat(nodes.filter(node => !node.cell || node.cell.role === 'coordinator'), nodes.filter(node => node.cell && node.cell.role !== 'coordinator').sort((a, b) => a.cell!.depth - b.cell!.depth));
+ const labelled = [...new Map(priority.map(node => [node.key, node])).values()].slice(0, 24);
+ this.labels.replaceChildren(); this.labelElements = [];
+ for (const node of labelled) { const span = document.createElement('span'); span.className = `swarm-label${!node.cell ? ' source-label' : ''}${node.key === selected ? ' selected' : ''}`; span.textContent = node.label.length > 24 ? `${node.label.slice(0, 22)}…` : node.label; span.title = `${node.source.factoryId} / ${node.label}`; span.dataset.key = node.key; this.labels.append(span); this.labelElements.push(span); }
+ this.request();
+ }
+ frame(point?: Point, sourceId?: string): void {
+ const nodes = sourceId ? this.nodes.filter(node => node.source.id === sourceId) : this.nodes;
+ const center = point ?? nodes.reduce((sum, node) => ({ x: sum.x + node.point.x / Math.max(1, nodes.length), y: sum.y + node.point.y / Math.max(1, nodes.length), z: sum.z + node.point.z / Math.max(1, nodes.length) }), { x: 0, y: 0, z: 0 });
+ const radius = point ? 35 : Math.max(sourceId ? 78 : 40, ...nodes.map(node => Math.hypot(node.point.x - center.x, node.point.z - center.z)));
+ if (this.gl) {
+ const distance = radius * (this.width < this.height ? 3.8 : sourceId ? 2.5 : 1.8);
+ const target = new Vector3(center.x, center.y, center.z), position = new Vector3(center.x + distance * .16, center.y + distance * .65, center.z + distance);
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches) { this.controls!.target.copy(target); this.camera.position.copy(position); this.controls!.update(); }
+ else this.flight = { start: performance.now(), fromPosition: this.camera.position.clone(), fromTarget: this.controls!.target.clone(), toPosition: position, toTarget: target };
+ } else { this.zoom = Math.min(this.width, this.height) / (radius * 2.8); this.pan = { x: -center.x * this.zoom, y: -center.z * this.zoom }; }
+ this.request();
+ }
+ request = (): void => { if (!this.disposed && !document.hidden && !this.frameId) this.frameId = requestAnimationFrame(this.draw); };
+ private project = (point: Point): { x: number; y: number; size: number } => {
+ if (!this.gl) return { x: this.width / 2 + point.x * this.zoom + this.pan.x, y: this.height / 2 + point.z * this.zoom + this.pan.y, size: 10 };
+ const vector = new Vector3(point.x, point.y, point.z).project(this.camera);
+ return { x: (vector.x + 1) * this.width / 2, y: (1 - vector.y) * this.height / 2, size: vector.z >= 1 || vector.z < -1 ? 0 : 12 };
+ };
+ private draw = (time: number): void => {
+ this.frameId = 0;
+ if (this.disposed || document.hidden) return;
+ if ((this.active || this.flight) && time - this.lastFrame < 33) { this.request(); return; }
+ this.lastFrame = time; const start = performance.now(); this.projected.clear();
+ if (this.flight) {
+ const progress = Math.min(1, (time - this.flight.start) / 620), eased = 1 - (1 - progress) ** 3;
+ this.camera.position.lerpVectors(this.flight.fromPosition, this.flight.toPosition, eased); this.controls!.target.lerpVectors(this.flight.fromTarget, this.flight.toTarget, eased); this.controls!.update();
+ if (progress >= 1) this.flight = null;
+ }
+ for (const node of this.nodes) this.projected.set(node.key, this.project(node.point));
+ if (this.gl) { if (this.points) this.points.material.uniforms.uTime!.value = time / 1000; this.gl.render(this.scene, this.camera); }
+ else {
+ const context = this.ctx!; context.clearRect(0, 0, this.width, this.height);
+ for (const source of this.nodes.filter(node => !node.cell)) { const point = this.projected.get(source.key)!; context.strokeStyle = `${sourceHue(source.source.id)}18`; context.lineWidth = 1; for (const radius of [26, 49, 72]) { context.beginPath(); context.arc(point.x, point.y, radius * this.zoom, 0, Math.PI * 2); context.stroke(); } }
+ for (const node of this.nodes) {
+ const parent = node.parent && this.projected.get(node.parent), point = this.projected.get(node.key)!;
+ if (!parent || !node.cell?.parentId) continue;
+ context.beginPath(); context.moveTo(parent.x, parent.y); context.quadraticCurveTo((parent.x + point.x) / 2 + 7, (parent.y + point.y) / 2 - 12, point.x, point.y); context.strokeStyle = node.key === this.selected ? '#7cdcbda0' : '#739c9e36'; context.lineWidth = 1; context.stroke();
+ }
+ for (const node of this.nodes) {
+ const point = this.projected.get(node.key)!, radius = !node.cell ? 13 : node.key === this.selected ? 10 : node.cell.role === 'coordinator' ? 9 : 5;
+ const gradient = context.createRadialGradient(point.x, point.y, 0, point.x, point.y, radius * 3); gradient.addColorStop(0, `${tint(node)}d0`); gradient.addColorStop(.3, `${tint(node)}60`); gradient.addColorStop(1, `${tint(node)}00`);
+ context.fillStyle = gradient; context.beginPath(); context.arc(point.x, point.y, radius * 3, 0, Math.PI * 2); context.fill();
+ context.strokeStyle = tint(node); context.beginPath(); context.arc(point.x, point.y, radius * .75, 0, Math.PI * 2); context.stroke();
+ if (node.source.status === 'observed' && node.cell?.status !== 'retired' && node.activity !== 'uncertain') { context.fillStyle = tint(node); context.beginPath(); context.arc(point.x, point.y, radius * .35, 0, Math.PI * 2); context.fill(); }
+ if (node.key === this.selected) { context.strokeStyle = '#c6ffec'; context.beginPath(); context.arc(point.x, point.y, radius + 4, 0, Math.PI * 2); context.stroke(); }
+ }
+ }
+ const occupied = new Set();
+ for (const label of this.labelElements) {
+ const point = this.projected.get(label.dataset.key!); label.hidden = !point?.size || point.x < 0 || point.x > this.width || point.y < 0 || point.y > this.height;
+ if (point && point.y > this.height - 145) label.hidden = true;
+ if (point && !label.hidden) { const slot = `${Math.floor(point.x / 110)}:${Math.floor(point.y / 26)}`; if (occupied.has(slot)) label.hidden = true; else occupied.add(slot); }
+ if (point) label.style.transform = `translate(${point.x}px,${point.y + 17}px) translateX(-50%)`;
+ }
+ this.submissions++; this.lastSubmissionMs = performance.now() - start;
+ this.onStats({ mode: this.mode, visible: this.nodes.length, labels: this.labelElements.length, draws: this.gl?.info.render.calls ?? 0, submissions: this.submissions, cpuMs: this.lastSubmissionMs });
+ if (this.active || this.flight) this.request();
+ };
+ private wireInput(): () => void {
+ const down = (event: PointerEvent) => { this.flight = null; this.drag = { x: event.clientX, y: event.clientY, startX: event.clientX, startY: event.clientY, moved: false }; if (!this.gl) this.canvas.setPointerCapture(event.pointerId); };
+ const move = (event: PointerEvent) => {
+ if (!this.drag) return; if (Math.hypot(event.clientX - this.drag.startX, event.clientY - this.drag.startY) > 5) this.drag.moved = true;
+ if (!this.gl) { this.pan.x += event.clientX - this.drag.x; this.pan.y += event.clientY - this.drag.y; this.request(); }
+ this.drag.x = event.clientX; this.drag.y = event.clientY;
+ };
+ const up = (event: PointerEvent) => {
+ if (this.drag && !this.drag.moved) {
+ const rect = this.canvas.getBoundingClientRect(), x = event.clientX - rect.left, y = event.clientY - rect.top;
+ let best: Node | null = null, distance = 20;
+ for (const node of this.nodes) { const point = this.projected.get(node.key); if (!point?.size) continue; const next = Math.hypot(point.x - x, point.y - y); if (next < distance) { distance = next; best = node; } }
+ if (best) this.onPick(best);
+ }
+ this.drag = null;
+ };
+ const wheel = (event: WheelEvent) => { if (this.gl) return; event.preventDefault(); const multiplier = Math.exp(-event.deltaY * .001); this.zoom = Math.max(.01, Math.min(30, this.zoom * multiplier)); this.pan.x *= multiplier; this.pan.y *= multiplier; this.request(); };
+ const key = (event: KeyboardEvent) => { if (event.key === 'Home') { event.preventDefault(); this.frame(); } };
+ this.canvas.addEventListener('pointerdown', down); this.canvas.addEventListener('pointermove', move); window.addEventListener('pointerup', up); this.canvas.addEventListener('wheel', wheel, { passive: false }); this.canvas.addEventListener('keydown', key);
+ return () => { this.canvas.removeEventListener('pointerdown', down); this.canvas.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); this.canvas.removeEventListener('wheel', wheel); this.canvas.removeEventListener('keydown', key); };
+ }
+ dispose(): void {
+ this.disposed = true; cancelAnimationFrame(this.frameId); this.observer.disconnect(); this.disposeInput(); document.removeEventListener('visibilitychange', this.visibility);
+ this.canvas.removeEventListener('webglcontextlost', this.contextLost); this.controls?.removeEventListener('change', this.request); this.controls?.dispose();
+ this.points?.geometry.dispose(); this.points?.material.dispose(); this.lines?.geometry.dispose(); this.lines?.material.dispose(); this.gl?.dispose(); this.labels.replaceChildren();
+ }
+}
diff --git a/src/swarm/swarm.css b/src/swarm/swarm.css
new file mode 100644
index 0000000..3328391
--- /dev/null
+++ b/src/swarm/swarm.css
@@ -0,0 +1 @@
+:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;color:#dfeae7;background:#081216;color-scheme:dark;font-synthesis:none}*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}button,select{color:#cbded7;background:#102127;border:1px solid #2d4146;border-radius:6px}button{cursor:pointer;padding:7px 10px}button:hover,button:focus-visible{background:#19332f;border-color:#80d5b8;outline:none}button[aria-pressed=true]{color:#adf5d9;border-color:#70cbb0;background:#18352e}a{color:#9bcbbd;text-decoration:none}button:disabled{opacity:.4;cursor:default}.swarm-top{height:72px;border-bottom:1px solid #233239;display:flex;gap:24px;align-items:center;padding:0 28px}.swarm-brand{font-size:19px;color:#e0eeea;white-space:nowrap}.swarm-brand b{color:#8bdfc1}#swarm-status{font-size:12px;color:#9badab;flex:1}.read-only,.section-title{font-size:10px;letter-spacing:.14em;color:#afc3ba}.read-only{border:1px solid #344a46;border-radius:3px;padding:4px 7px}.swarm-shell{display:grid;grid-template-columns:230px minmax(0,1fr) 290px;min-height:calc(100vh - 110px)}.swarm-navigation,.swarm-inspector{padding:24px 18px;min-width:0}.swarm-navigation{border-right:1px solid #233239}.swarm-inspector{border-left:1px solid #233239;background:#0b171c}.section-title{margin-bottom:18px}.search-label{display:grid;gap:9px;font-size:11px;color:#9fb6ae}input[type=search]{width:100%;border:1px solid #30464b;border-radius:6px;padding:10px;background:#101f25;color:#ecf5f0;font-size:12px}#source-list{margin:24px 0}#source-list button,#branch-list button,#search-results button{display:block;width:100%;text-align:left;margin:6px 0;font-size:12px;overflow-wrap:anywhere}#source-list button small,#branch-list button small{display:block;margin-top:5px;color:#90a8a0;font-size:10px}#search-results{max-height:270px;overflow:auto}#search-results p{font-size:11px;color:#8caaa0}#branch-list{max-height:420px;overflow:auto}.scope-note{font-size:11px;color:#78908b;line-height:1.65;margin-top:22px}.swarm-space{min-width:0;background:radial-gradient(ellipse at 48% 42%,#18353080,transparent 53%),radial-gradient(ellipse at 73% 62%,#20334f50,transparent 50%)}.space-toolbar{padding:18px 20px;border-bottom:1px solid #203039;display:flex;flex-wrap:wrap;gap:15px;justify-content:space-between;align-items:center}#swarm-path{display:flex;flex-wrap:wrap;align-items:center;gap:5px;font-size:11px}#swarm-path button{padding:4px 7px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.space-controls{display:flex;align-items:center;gap:12px;font-size:11px;flex-wrap:wrap}.space-controls button{font-size:11px}.space-controls select{padding:5px}.follow-control{color:#9fb8b0;display:flex;align-items:center;gap:4px}#swarm-stage{position:relative;height:max(480px,calc(100vh - 245px));min-height:400px;overflow:hidden}#swarm-canvas{display:block;width:100%;height:100%;touch-action:none}#swarm-labels{position:absolute;inset:0;pointer-events:none;overflow:hidden}.swarm-label{position:absolute;top:0;left:0;font-size:10px;color:#9bbeb1;white-space:nowrap;text-shadow:0 1px 7px #081216;max-width:160px;overflow:hidden;text-overflow:ellipsis}.swarm-label.source-label{font-size:13px;letter-spacing:.05em;color:#c5e6d8}.swarm-label.selected{color:#e0fff0}.space-caption{position:absolute;bottom:22px;left:22px;font-size:12px;color:#b0cdc1;pointer-events:none}.space-caption p{font-size:10px;color:#78968a;margin-bottom:0}.swarm-diagnostics{font-size:10px;color:#7e958c;border-top:1px solid #223239;padding:9px 20px;min-height:30px;overflow-wrap:anywhere}.swarm-timeline{padding:20px;border-top:1px solid #223239}.swarm-timeline .section-title{display:flex;gap:12px;flex-wrap:wrap}.swarm-timeline .section-title span{color:#76968b;letter-spacing:0}#tracking-list{font-size:11px;line-height:1.6;max-height:150px;overflow:auto}#tracking-list p{color:#81998f}.tracking-row{display:flex;flex-wrap:wrap;gap:12px;padding:7px 0;border-bottom:1px solid #233239}.tracking-row time{color:#809c91;font-size:10px}.swarm-inspector h2{font-size:21px;font-weight:500;line-height:1.3;overflow-wrap:anywhere}.swarm-inspector h3{font-size:13px;margin-top:26px}.swarm-inspector p{font-size:12px;color:#aec2b9;line-height:1.65;overflow-wrap:anywhere}.swarm-inspector dl{font-size:11px;display:grid;grid-template-columns:1fr;gap:6px;line-height:1.5}.swarm-inspector dt{color:#819a8e;margin-top:7px}.swarm-inspector dd{margin:0;overflow-wrap:anywhere;color:#c8dcd2}.swarm-inspector code{font-size:10px;color:#aac6b6;overflow-wrap:anywhere}.evidence-note{border:1px solid #594a30;background:#342b1b33;padding:10px;border-radius:5px;color:#dac295!important}.task-link,.effect-link{display:block;width:100%;text-align:left;font-size:11px;margin:7px 0;overflow-wrap:anywhere}.sample-notice{color:#e7c288!important}.source-state{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:#8bd4b3}footer{border-top:1px solid #233239;padding:14px 24px;color:#7c958b;font-size:10px;line-height:1.6}@media(max-width:1120px){.swarm-shell{grid-template-columns:190px minmax(0,1fr) 240px}.swarm-top{padding:0 18px;gap:15px}.swarm-navigation,.swarm-inspector{padding:20px 14px}.follow-control{display:none}}@media(max-width:800px){.swarm-shell{grid-template-columns:170px minmax(0,1fr)}.swarm-inspector{grid-column:1/-1;border-left:0;border-top:1px solid #233239;display:block}.swarm-inspector dl{grid-template-columns:130px minmax(0,1fr)}.swarm-inspector dt{margin-top:0}.swarm-top{height:auto;min-height:65px;flex-wrap:wrap;padding:15px}.read-only{margin-left:auto}#swarm-status{order:3;flex-basis:100%}#swarm-stage{height:480px}.space-controls{gap:8px}.space-controls label{font-size:10px}.space-toolbar{padding:13px}.space-caption{left:12px;font-size:11px}}@media(max-width:520px){.swarm-shell{grid-template-columns:minmax(0,1fr)}.swarm-navigation{border-right:0;border-bottom:1px solid #233239;padding:16px}#source-list{display:flex;gap:7px;margin:14px 0;overflow:auto}#source-list button{min-width:140px;max-width:190px;margin:0}#branch-list{max-height:150px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px}#branch-list button{margin:0}#branch-heading{margin-bottom:10px}.scope-note{margin:12px 0 0;font-size:10px}.swarm-navigation .section-title{margin-bottom:10px}#swarm-stage{height:420px}.swarm-inspector dl{grid-template-columns:110px minmax(0,1fr)}footer{padding:12px 16px}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
diff --git a/swarm.html b/swarm.html
new file mode 100644
index 0000000..57e691a
--- /dev/null
+++ b/swarm.html
@@ -0,0 +1,15 @@
+
+
+Brain · swarm
+
+
+
+
+
+
A wider mind.
Orbit the recorded world. Enter a source. Follow its work.
LIVE TRACKING
Rendering details
+
+
+
FACTORY owns execution and recovery. Recorded activity does not establish host health. Worker quiescence: unverified.
+
+
+
diff --git a/test/swarm.test.mjs b/test/swarm.test.mjs
new file mode 100644
index 0000000..c223066
--- /dev/null
+++ b/test/swarm.test.mjs
@@ -0,0 +1,92 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { readFile, writeFile, mkdir } from 'node:fs/promises';
+import { performance } from 'node:perf_hooks';
+import ts from 'typescript';
+const generated = new URL('../.vite/swarm-tests/', import.meta.url);
+await mkdir(generated, { recursive: true });
+for (const name of ['model', 'preview']) {
+ const source = await readFile(new URL(`../src/swarm/${name}.ts`, import.meta.url), 'utf8');
+ await writeFile(new URL(`${name}.mjs`, generated), ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText);
+}
+const { parseSwarm, SwarmIndex, nodeKey, activities } = await import(new URL('model.mjs', generated));
+const { swarmPreview } = await import(new URL('preview.mjs', generated));
+
+test('same bare root/task/effect identities remain distinct across authorities and registration is separate', () => {
+ const swarm = parseSwarm(swarmPreview());
+ const index = new SwarmIndex(swarm);
+ const keys = swarm.sources.map(source => nodeKey(source, 'cell', 'root'));
+ assert.equal(new Set(keys).size, 3);
+ for (const source of swarm.sources) {
+ const root = index.nodeFor({ sourceId: source.id, kind: 'cell', id: 'root' });
+ assert.equal(root.source.factoryId, source.factoryId);
+ assert.equal(index.ancestors(root.key)[0].selection.kind, 'source');
+ assert.ok(index.ancestors(root.key).every(node => node.source.id === source.id));
+ assert.equal(index.nodeFor({ sourceId: source.id, kind: 'task', id: 'exploration' }).source.id, source.id);
+ }
+});
+test('10,000-level delegation can be indexed, searched, selected and drawn without recursive overflow or losing focus', () => {
+ const swarm = swarmPreview(10000), source = swarm.sources[0];
+ swarm.sources = [source];
+ const template = source.snapshot.snapshot.cells[0];
+ source.snapshot.snapshot.cells = Array.from({ length: 10000 }, (_, index) => ({ ...template, id: index ? `deep-${index}` : 'root', parentId: index === 0 ? null : index === 1 ? 'root' : `deep-${index - 1}`, depth: index }));
+ const start = performance.now(), index = new SwarmIndex(parseSwarm(swarm));
+ const target = index.nodeFor({ sourceId: source.id, kind: 'cell', id: 'deep-9999' });
+ assert.ok(target);
+ assert.equal(index.ancestors(target.key).length, 10001);
+ assert.equal(index.search('deep-9999')[0].id, 'deep-9999');
+ const visible = index.visible(target.key);
+ assert.ok(visible.length <= 600);
+ assert.ok(visible.some(node => node.key === target.key));
+ assert.ok(Number.isFinite(target.point.x));
+ console.log(`Deep-swarm model qualification: ${Math.round(performance.now() - start)}ms, ${index.nodes.size} indexed / ${visible.length} drawn.`);
+});
+test('10,000 siblings remain reachable across bounded child pages rather than being silently truncated', () => {
+ const swarm = swarmPreview(10000), source = swarm.sources[0]; swarm.sources = [source];
+ const template = source.snapshot.snapshot.cells[0];
+ source.snapshot.snapshot.cells = Array.from({ length: 10000 }, (_, index) => ({ ...template, id: index ? `wide-${index}` : 'root', parentId: index ? 'root' : null, depth: index ? 1 : 0 }));
+ const index = new SwarmIndex(parseSwarm(swarm)), root = nodeKey(source, 'cell', 'root'), reached = new Set();
+ for (let page = 0; page < Math.ceil(9999 / 128); page++) {
+ const visible = index.visible(root, page); assert.ok(visible.length <= 600);
+ for (const node of visible) if (node.cell?.id !== 'root' && node.cell) reached.add(node.cell.id);
+ }
+ assert.equal(reached.size, 9999);
+ assert.ok(index.search('wide-9999').some(selection => selection.id === 'wide-9999'));
+});
+test('spatial positions survive heartbeat and recorded-status updates', () => {
+ const swarm = parseSwarm(swarmPreview()), before = new SwarmIndex(swarm);
+ swarm.sources[0].snapshot.snapshot.cells[1].heartbeat = new Date(Date.now() + 1000).toISOString();
+ swarm.sources[0].snapshot.snapshot.tasks[0].status = 'cancelled';
+ const after = new SwarmIndex(swarm);
+ for (const [key, node] of before.nodes) assert.deepEqual(after.nodes.get(key).point, node.point);
+});
+test('observation freshness never renews worker evidence, and terminal states do not activate it', () => {
+ const swarm = parseSwarm(swarmPreview()), source = swarm.sources[0], now = Date.parse(source.snapshot.observedAt);
+ assert.equal(activities(source, now).get('cell-1'), 'recent');
+ assert.equal(activities(source, now + 16000).get('cell-1'), 'uncertain');
+ source.snapshot.observedAt = new Date(now + 61000).toISOString();
+ assert.equal(activities(source, now + 61000).get('cell-1'), 'uncertain');
+ source.snapshot.snapshot.tasks[0].status = 'completed';
+ assert.equal(activities(source, now + 61000).get('cell-1'), 'idle');
+ source.snapshot.snapshot.tasks[0].status = 'cancelled';
+ assert.equal(activities(source, now + 61000).get('cell-1'), 'idle');
+ source.snapshot.snapshot.tasks[0].status = 'running'; source.status = 'stale';
+ assert.equal(activities(source, now).get('cell-1'), 'uncertain');
+ source.status = 'observed'; source.snapshot.snapshot.control.status = 'paused';
+ assert.equal(activities(source, now).get('cell-1'), 'uncertain');
+});
+test('consumer drops private extensions and fails closed on capabilities/identity/duplicate records', () => {
+ const input = swarmPreview(); input.token = 'private'; input.sources[0].origin = 'private'; input.sources[0].snapshot.snapshot.tasks[2].candidate.path = 'private'; input.sources[0].snapshot.snapshot.paidBudget = { token: 'private' };
+ assert.equal(JSON.stringify(parseSwarm(input)).includes('private'), false);
+ input.sources[0].snapshot.capabilities.commands = true; assert.throws(() => parseSwarm(input));
+ input.sources[0].snapshot.capabilities.commands = false; input.sources[0].snapshot.factoryId = 'wrong'; assert.throws(() => parseSwarm(input));
+ input.sources[0].snapshot.factoryId = input.sources[0].factoryId; input.sources.push(input.sources[0]); assert.throws(() => parseSwarm(input));
+});
+test('unavailable source remains in the registry without invented cells; historical hashes are preserved', () => {
+ const input = swarmPreview(); input.sources[1].snapshot = null; input.sources[1].status = 'unavailable';
+ const swarm = parseSwarm(input), index = new SwarmIndex(swarm);
+ assert.equal(index.sources.size, 3);
+ assert.equal([...index.nodes.values()].filter(node => node.source.id === input.sources[1].id).length, 1);
+ const task = swarm.sources[0].snapshot.snapshot.tasks.find(task => task.status === 'cancelled');
+ assert.equal(task.review.candidateDigest, task.candidate.sha256);
+});
diff --git a/vite.config.ts b/vite.config.ts
index 1d0abe4..d22f250 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -12,6 +12,7 @@ export default defineConfig({
input: {
main: resolve(__dirname, 'index.html'),
lobby: resolve(__dirname, 'lobby.html'),
+ swarm: resolve(__dirname, 'swarm.html'),
},
},
},
From a3be79d2713d75e2c1c49dfdcd0e3b936170f46b Mon Sep 17 00:00:00 2001
From: flujo-app <300233937+flujo-app@users.noreply.github.com>
Date: Sat, 3 Oct 2026 15:50:45 -0500
Subject: [PATCH 2/9] Implement Observatory design and qualify scoped swarm
navigation
---
docs/SWARM.md | 25 ++-
src/swarm/main.ts | 70 +++++---
src/swarm/model.ts | 22 ++-
src/swarm/observatory.css | 338 +++++++++++++++++++++++++-------------
src/swarm/preview.ts | 2 +-
src/swarm/renderer.ts | 248 +++++++++++++++++++++-------
src/swarm/transport.ts | 15 ++
swarm.html | 54 +++++-
test/swarm.test.mjs | 40 ++++-
9 files changed, 609 insertions(+), 205 deletions(-)
create mode 100644 src/swarm/transport.ts
diff --git a/docs/SWARM.md b/docs/SWARM.md
index f12f14b..05582b8 100644
--- a/docs/SWARM.md
+++ b/docs/SWARM.md
@@ -17,15 +17,18 @@ subset. Search, breadcrumbs and 128-child pages navigate the complete index.
Embedded hosts pass `?embed=1&channel=<16..128 character channel>` and exchange
same-origin `brain-swarm/1` messages with the exact parent/frame:
-- viewer → parent: `ready` or `select` with the channel and scoped selection;
+- viewer → parent: `ready` or `select` with the channel and a selection naming
+ sourceId, factoryId, kind and id;
- parent → viewer: `observe` with channel, increasing integer sequence and model;
- parent → viewer: `ping`; viewer repeats `ready`.
The model has schemaVersion 1, scope `operator-source-registry`, commands false,
observedAt and at most 32 sources. Each source has id, label, factoryId,
observed/stale/unavailable status, a public canonical FACTORY snapshot or null,
-and optional metadata events. `model.ts` positively allowlists these facts;
-private extensions are dropped and unsupported execution capability is rejected.
+and optional metadata events. Hash links also name the exact factory; changing
+the authority behind a registry alias clears its former selection.
+`model.ts` positively allowlists these facts; private extensions are dropped
+and unsupported execution capability is rejected.
Never put a bearer credential in a URL, iframe message or client environment.
A standalone deployment must supply its own authenticated same-origin JSON bridge
@@ -56,6 +59,12 @@ uses the same index and scoped selection. Rendering diagnostics report CPU
submission time separately from draw-call and frame counts; they do not prove
GPU timing or physical worker health.
+The Observatory design and first UI implementation were completed by Claude Opus
+through a normal local FLUJO subscription-agent conversation at Medium effort
+with Bash. The integration pass added camera fitting around visible instruments,
+responsive reframing, measured label collision bounds, live reduced-motion
+changes and a trailing diagnostics update when on-demand rendering settles.
+
Fresh observations do not establish worker activity. A cell needs an active
controller, ready lifecycle, recent heartbeat, an unexpired running lease and
no owned unknown outcome. Completed/cancelled operations do not animate as
@@ -72,3 +81,13 @@ positive allowlists and missing-source behavior. Copy the built `dist` artifact
to the consumer's ignored viewer directory for local integration; keep source
in this repository. Qualify desktop/mobile composition and 3D/2D navigation in
the browser against the built artifact before publishing the Brain image.
+
+The 2026-10-03 fixture qualification reached `cell-3333` in a 10,000-cell world
+in both modes and preserved its source/factory identity after actual WebGL
+context loss. The world had 10,003 indexed nodes; its overview drew 18 nodes and
+the selected neighborhood drew 10, with two GPU draw calls in 3D and no GL calls
+in 2D. Desktop, 615-pixel and 390-pixel layouts were checked. Reduced motion
+stopped an already-running renderer; 2D settled without an animation loop.
+These are fixture/navigation checks, not live FACTORY health evidence. The
+read-only observer was still offline at the last 2026-10-03 qualification check;
+its owner must restore and qualify it before a fresh live witness is recorded.
diff --git a/src/swarm/main.ts b/src/swarm/main.ts
index fcf074a..6d5ee1f 100644
--- a/src/swarm/main.ts
+++ b/src/swarm/main.ts
@@ -1,7 +1,8 @@
import './swarm.css';
import './observatory.css';
-import { activities, nodeKey, parseSwarm, SwarmIndex, type Node, type Selection, type Source, type Swarm } from './model';
-import { SwarmRenderer } from './renderer';
+import { activities, nodeKey, parseSwarm, selectionFragment, selectionFromFragment, SwarmIndex, type Node, type Selection, type Source, type Swarm } from './model';
+import { readSwarmJson } from './transport';
+import { SwarmRenderer, sourceColor } from './renderer';
import { swarmPreview } from './preview';
const $ = (id: string): HTMLElement => document.getElementById(id)!;
@@ -18,10 +19,7 @@ let swarm = empty(), index = new SwarmIndex(swarm), selection: Selection | null
let renderer: SwarmRenderer;
let mode: '3d' | '2d' = query.get('view') === '2d' || ((navigator.hardwareConcurrency ?? 8) <= 4) ? '2d' : '3d';
let lastActivity = '', follow = false, firstObservation = true;
-const selectionHash = (): Selection | null => {
- const hash = new URLSearchParams(location.hash.slice(1)), sourceId = hash.get('source'), kind = hash.get('kind'), id = hash.get('id');
- return sourceId && id && ['source', 'cell', 'task', 'effect'].includes(kind ?? '') ? { sourceId, kind: kind as Selection['kind'], id } : null;
-};
+const selectionHash = (): Selection | null => selectionFromFragment(swarm, location.hash);
function validSelection(value: Selection): boolean {
const source = index.sources.get(value.sourceId); if (!source) return false;
@@ -29,12 +27,16 @@ function validSelection(value: Selection): boolean {
}
function select(next: Selection | null, move = true): void {
if (next && !validSelection(next)) return;
+ const previousNode = focusNode()?.key;
selection = next; page = 0;
document.body.classList.toggle('has-selection', !!next);
- const hash = next ? new URLSearchParams({ source: next.sourceId, kind: next.kind, id: next.id }).toString() : '';
+ const source = next ? index.sources.get(next.sourceId)! : null;
+ // The selected authority tints the instruments with its deterministic hue.
+ document.body.style.setProperty('--accent', source ? sourceColor(source.id) : '#7fd1de');
+ const hash = next && source ? selectionFragment(source, next) : '';
history.replaceState(null, '', `${location.pathname}${location.search}${hash ? `#${hash}` : ''}`);
- redraw(); if (move) frameSelection();
- if (embedded) window.parent.postMessage({ protocol: 'brain-swarm/1', type: 'select', channel, selection: next }, location.origin);
+ redraw(); if (move && (!next || !['task', 'effect'].includes(next.kind) || focusNode()?.key !== previousNode)) frameSelection();
+ if (embedded) window.parent.postMessage({ protocol: 'brain-swarm/1', type: 'select', channel, selection: next && source ? { ...next, factoryId: source.factoryId } : null }, location.origin);
}
function sourceStatus(source: Source): string {
const age = source.snapshot ? Date.now() - Date.parse(source.snapshot.observedAt) : Infinity;
@@ -47,18 +49,26 @@ function frameSelection(): void {
else renderer.frame(node?.point);
}
+let statsTimer: ReturnType | undefined;
function createRenderer(): void {
+ clearTimeout(statsTimer);
+ lastStats = 0;
renderer?.dispose();
const canvas = element('canvas'); canvas.id = 'swarm-canvas'; canvas.tabIndex = 0; canvas.setAttribute('aria-label', 'Spatial swarm. Use navigation buttons to select cells; Home fits the whole swarm.');
$('swarm-canvas').replaceWith(canvas);
renderer = new SwarmRenderer(canvas, $('swarm-labels'), mode); mode = renderer.mode;
+ document.body.dataset.view = mode;
(document.getElementById('swarm-mode') as HTMLSelectElement).value = mode;
renderer.onPick = node => select(node.selection);
renderer.onStats = stats => {
const diagnostics = $('swarm-diagnostics');
Object.assign(diagnostics.dataset, { mode: stats.mode, visible: String(stats.visible), indexed: String(index.nodes.size), labels: String(stats.labels), draws: String(stats.draws), frames: String(stats.submissions), cpuMs: stats.cpuMs.toFixed(3) });
- if (performance.now() - lastStats < 500) return; lastStats = performance.now();
- diagnostics.textContent = `${stats.mode.toUpperCase()} · ${stats.visible} drawn / ${index.nodes.size} indexed nodes · ${stats.labels} labels · ${stats.draws} GL draw calls · ${stats.cpuMs.toFixed(2)} ms CPU submission · ${stats.submissions} frames`;
+ const text = `${stats.mode.toUpperCase()} · ${stats.visible} drawn / ${index.nodes.size} indexed nodes · ${stats.labels} labels · ${stats.draws} GL draw calls · ${stats.cpuMs.toFixed(2)} ms CPU submission · ${stats.submissions} frames`;
+ const publish = () => { statsTimer = undefined; lastStats = performance.now(); diagnostics.textContent = text; };
+ clearTimeout(statsTimer);
+ const remaining = 500 - (performance.now() - lastStats);
+ if (lastStats && remaining > 0) statsTimer = setTimeout(publish, remaining);
+ else publish();
};
redraw(); frameSelection();
}
@@ -67,16 +77,26 @@ function paintNavigation(): void {
const sourceList = $('source-list'); sourceList.replaceChildren();
for (const source of index.sources.values()) {
const item = button(source.label, () => select({ sourceId: source.id, kind: 'source', id: source.id }), selection?.sourceId === source.id);
- item.className = `source-record source-${sourceStatus(source)}`;
- item.append(element('small', `${sourceStatus(source)} · ${source.snapshot?.snapshot.cells.length ?? 0} cells`)); item.title = source.factoryId; sourceList.append(item);
+ const status = sourceStatus(source), cells = source.snapshot?.snapshot.cells ?? [];
+ const evidenced = cells.reduce((sum, cell) => sum + (index.nodes.get(nodeKey(source, 'cell', cell.id))?.activity === 'recent' ? 1 : 0), 0);
+ item.className = `source-record source-${status}`;
+ // Exactly the renderer's deterministic plate colour, never a list position.
+ item.style.setProperty('--accent', sourceColor(source.id));
+ item.dataset.status = status;
+ item.replaceChildren(element('span', undefined, 'source-dot'), element('strong', source.label, 'source-name'),
+ element('small', `${status} · ${cells.length} cells${evidenced ? ` · ${evidenced} evidenced` : ''}`));
+ item.title = source.factoryId; sourceList.append(item);
}
const focus = focusNode(), children = focus ? index.children.get(focus.key) ?? [] : [];
- $('branch-heading').textContent = focus ? `${focus.label} · ${children.length} children` : 'Select a source to explore its delegation';
+ $('branch-heading').textContent = focus ? `DELEGATION · ${focus.label} · ${children.length} children` : 'DELEGATION · select a source';
$('branch-list').replaceChildren(); $('branch-pages').replaceChildren();
for (const key of children.slice(page * 128, (page + 1) * 128)) {
const node = index.nodes.get(key)!;
const item = button(node.label, () => select(node.selection), key === focus?.key);
- item.append(element('small', `${node.cell?.role ?? 'source'} · ${node.cell?.status ?? sourceStatus(node.source)} · ${index.children.get(key)?.length ?? 0} children`));
+ item.className = `branch-record activity-${node.activity}`;
+ item.style.setProperty('--accent', sourceColor(node.source.id));
+ item.replaceChildren(element('span', undefined, 'branch-dot'), element('strong', node.label, 'branch-name'),
+ element('small', `${node.cell?.role ?? 'source'} · ${node.cell?.status ?? sourceStatus(node.source)} · ${index.children.get(key)?.length ?? 0} children`));
$('branch-list').append(item);
}
if (children.length > 128) {
@@ -106,9 +126,12 @@ function evidenceList(container: HTMLElement, pairs: Array<[string, string]>): v
}
function paintInspector(): void {
const container = $('swarm-inspect'); container.replaceChildren();
- if (!selection) { container.append(element('h2', 'A constellation of work.'), element('p', 'Move from registered authorities to their coordinators and delegated cells. Search reaches every recorded cell, task and external effect.'), element('p', 'This view follows observations. It cannot start, stop or resume workers.'), element('p', 'Select a source to inspect its provenance and recorded work.')); return; }
+ container.style.removeProperty('--accent');
+ if (!selection) { container.append(element('p', 'THE OBSERVATORY', 'evidence-kicker'), element('h2', 'A constellation of work.'), element('p', 'Move from registered authorities to their coordinators and delegated cells. Search reaches every recorded cell, task and external effect.'), element('p', 'This view follows observations. It cannot start, stop or resume workers.'), element('p', 'Select a source to inspect its provenance and recorded work.')); return; }
const source = index.sources.get(selection.sourceId)!;
- container.append(element('div', `${sourceStatus(source)} · ${source.label}`, 'source-state'), element('h2', selection.kind === 'source' ? source.label : selection.id));
+ container.style.setProperty('--accent', sourceColor(source.id));
+ container.append(element('p', selection.kind === 'source' ? 'REGISTERED AUTHORITY' : `RECORDED ${selection.kind.toUpperCase()}`, 'evidence-kicker'),
+ element('div', `${sourceStatus(source)} · ${source.label}`, 'source-state'), element('h2', selection.kind === 'source' ? source.label : selection.id));
evidenceList(container, [['Factory identity', source.factoryId], ['Registry source', source.id]]);
const envelope = source.snapshot;
if (!envelope) { container.append(element('p', 'This registered source has no available observation. No cells or execution state are inferred.', 'evidence-note')); return; }
@@ -167,6 +190,9 @@ function redraw(): void {
renderer?.setNodes(visible, focus?.key ?? null);
document.body.classList.toggle('has-selection', !!selection);
paintNavigation(); paintInspector(); paintTracking();
+ document.body.classList.toggle('field-empty', !swarm.sources.length);
+ const accented = selection ? index.sources.get(selection.sourceId) : null;
+ document.body.style.setProperty('--accent', accented ? sourceColor(accented.id) : '#7fd1de');
const cells = swarm.sources.reduce((sum, source) => sum + (source.snapshot?.snapshot.cells.length ?? 0), 0), tasks = swarm.sources.reduce((sum, source) => sum + (source.snapshot?.snapshot.tasks.length ?? 0), 0);
$('swarm-counts').textContent = `${swarm.sources.length} registered sources · ${cells.toLocaleString()} recorded cells · ${tasks.toLocaleString()} tasks`;
const stale = swarm.sources.filter(source => sourceStatus(source) !== 'observed').length;
@@ -184,7 +210,7 @@ function observe(value: unknown): void {
return source;
});
swarm = next; index = new SwarmIndex(next);
- if (selection && !validSelection(selection)) selection = null;
+ if (selection && (previous.get(selection.sourceId)?.factoryId !== index.sources.get(selection.sourceId)?.factoryId || !validSelection(selection))) select(null, false);
if (firstObservation) { const wanted = selectionHash(); if (wanted && validSelection(wanted)) selection = wanted; firstObservation = false; }
const active = [...index.nodes.values()].filter(node => node.activity === 'recent').map(node => node.key).sort().join('|');
const changedActivity = active && active !== lastActivity; lastActivity = active;
@@ -199,7 +225,7 @@ createRenderer();
($('swarm-follow') as HTMLInputElement).onchange = event => { follow = (event.target as HTMLInputElement).checked; };
$('swarm-home').onclick = () => select(null);
$('swarm-close').onclick = () => select(null, false);
-$('swarm-explore').onclick = () => { const hidden = document.body.classList.toggle('navigation-hidden'); $('swarm-explore').setAttribute('aria-expanded', String(!hidden)); };
+$('swarm-explore').onclick = () => { const hidden = document.body.classList.toggle('navigation-hidden'); $('swarm-explore').setAttribute('aria-expanded', String(!hidden)); frameSelection(); };
$('swarm-find').onclick = () => { document.body.classList.remove('navigation-hidden'); document.body.classList.add('search-open'); $('swarm-search').focus(); };
window.addEventListener('keydown', event => {
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement) return;
@@ -261,8 +287,8 @@ if (embedded) {
const response = await fetch(path.pathname, { cache: 'no-store', signal: controller.signal, credentials: 'same-origin' });
if ([401, 403, 404].includes(response.status)) { observe(empty()); throw new Error('Observation access unavailable'); }
if (!response.ok) throw new Error('Swarm bridge unavailable');
- const body = await response.text(); if (body.length > 32 * 1024 * 1024) throw new Error('Swarm observation exceeds its bound');
- if (!stopped) observe(JSON.parse(body));
+ const body = await readSwarmJson(response);
+ if (!stopped) observe(body);
} catch {
if (!stopped) { swarm.sources = swarm.sources.map(source => ({ ...source, status: source.snapshot ? 'stale' : 'unavailable' })); index = new SwarmIndex(swarm); redraw(); $('swarm-status').textContent = 'Observation unavailable · retained facts are stale'; }
} finally { clearTimeout(deadline); if (!stopped) timer = setTimeout(() => void poll(), 5000); }
@@ -271,4 +297,4 @@ if (embedded) {
}
window.addEventListener('pagehide', () => { stopped = true; controller?.abort(); clearTimeout(timer); });
}
-window.addEventListener('pagehide', () => { clearInterval(expiryTimer); renderer.dispose(); });
+window.addEventListener('pagehide', () => { clearInterval(expiryTimer); clearTimeout(statsTimer); renderer.dispose(); });
diff --git a/src/swarm/model.ts b/src/swarm/model.ts
index a2edccc..0639640 100644
--- a/src/swarm/model.ts
+++ b/src/swarm/model.ts
@@ -75,6 +75,18 @@ export function parseSwarm(value: unknown): Swarm {
export const nodeKey = (source: Pick, kind: string, localId: string) => JSON.stringify([source.id, source.factoryId, kind, localId]);
export const selectionKey = (source: Source, selection: Selection) => nodeKey(source, selection.kind, selection.id);
+export const selectionFragment = (source: Pick, selection: Selection): string => new URLSearchParams({ source: source.id, factory: source.factoryId, kind: selection.kind, id: selection.id }).toString();
+/** A saved link must name the authority, not just a reusable registry alias. */
+export function selectionFromFragment(swarm: Swarm, fragment: string): Selection | null {
+ const params = new URLSearchParams(fragment.replace(/^#/, ''));
+ if (['source', 'factory', 'kind', 'id'].some(key => params.getAll(key).length !== 1)) return null;
+ const source = swarm.sources.find(source => source.id === params.get('source'));
+ if (!source || source.factoryId !== params.get('factory')) return null;
+ const kind = params.get('kind'), localId = params.get('id')!;
+ const state = source.snapshot?.snapshot;
+ const valid = kind === 'source' ? localId === source.id : kind === 'cell' ? state?.cells.some(cell => cell.id === localId) : kind === 'task' ? state?.tasks.some(task => task.id === localId) : kind === 'effect' && state?.effects.some(effect => effect.key === localId);
+ return valid ? { sourceId: source.id, kind: kind as Selection['kind'], id: localId } : null;
+}
const hash = (value: string) => { let result = 2166136261; for (let i = 0; i < value.length; i++) result = Math.imul(result ^ value.charCodeAt(i), 16777619); return (result >>> 0) / 4294967296; };
/** O(cells + tasks + effects), including expiry independent of read time. */
@@ -146,13 +158,17 @@ export class SwarmIndex {
if (focus) {
for (const node of this.ancestors(focus).slice(-32)) keys.add(node.key);
const nearby = this.children.get(focus) ?? [];
- for (const key of nearby.slice(page * 128, (page + 1) * 128)) keys.add(key);
+ const pageKeys = nearby.slice(page * 128, (page + 1) * 128);
+ for (const key of pageKeys) keys.add(key);
+ if (!this.nodes.get(focus)?.cell) for (const root of pageKeys) for (const key of (this.children.get(root) ?? []).slice(0, 12)) keys.add(key);
const parent = this.nodes.get(focus)?.parent;
if (parent) for (const key of (this.children.get(parent) ?? []).slice(0, 128)) keys.add(key);
} else {
for (const source of this.sources.values()) {
- const root = nodeKey(source, 'cell', 'root'); if (this.nodes.has(root)) keys.add(root);
- for (const key of (this.children.get(root) ?? []).slice(0, 12)) keys.add(key);
+ for (const root of (this.children.get(nodeKey(source, 'source', source.id)) ?? []).slice(0, 128)) {
+ keys.add(root);
+ for (const key of (this.children.get(root) ?? []).slice(0, 12)) keys.add(key);
+ }
}
}
return [...keys].slice(0, limit).map(key => this.nodes.get(key)!).filter(Boolean);
diff --git a/src/swarm/observatory.css b/src/swarm/observatory.css
index f5c2125..4a6b438 100644
--- a/src/swarm/observatory.css
+++ b/src/swarm/observatory.css
@@ -1,126 +1,242 @@
-/* Observatory art direction: source plates and a wide field, with instruments
- * floating over it. All motion and semantic colouring still follow evidence. */
+/* THE OBSERVATORY — art direction for the FACTORY swarm viewer.
+ * A single deep field holds every authority; instruments float over it on
+ * hairline glass plates. Motion, colour and emphasis still follow evidence:
+ * source hues come from the renderer's deterministic palette, amber means
+ * uncertain, grey means retired/stale, and nothing animates without a lease. */
:root {
- --ink:#0a0e14; --panel:#10151d; --rule:#26313d; --text:#e8edf4;
- --muted:#a7b2c2; --quiet:#7d8798; --signal:#7fd1de;
+ --void:#04060b; --ink:#070a11; --deep:#0a0f18;
+ --plate:rgba(11,16,25,.82); --plate-solid:#0b1019;
+ --rule:rgba(150,180,210,.14); --rule-strong:rgba(160,196,226,.28);
+ --text:#e9eef7; --muted:#a6b3c7; --quiet:#73829a; --faint:#5a677b;
+ --signal:#7fd1de; --accent:#7fd1de; --warn:#e0a85c;
--serif:ui-serif,'Iowan Old Style','Palatino Linotype',Palatino,Georgia,serif;
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
- background:var(--ink); color:var(--text);
+ --kicker:9px/1.1 var(--mono);
+ --gap:22px;
+ background:var(--void); color:var(--text);
}
-body{background:var(--ink)}
-button,input,select{border-radius:2px}
-button:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--signal);outline-offset:2px}
-button{background:transparent;border-color:var(--rule)}
-button:hover{background:#172431;border-color:#506675}
-.swarm-top{height:64px;padding:0 24px;border-color:var(--rule);background:#0c1017;gap:20px}
-.swarm-brand{font-family:var(--serif);font-size:21px;letter-spacing:-.04em}
-.swarm-brand b{color:#b9d8e7;font-weight:400}
-.swarm-top button{font-size:11px;padding:6px 9px;color:var(--muted)}
-#swarm-status{font-family:var(--mono);font-size:10px;color:var(--muted);text-align:right}
-.read-only{font-family:var(--mono);font-size:9px;letter-spacing:.08em;color:var(--quiet);border-color:var(--rule);border-radius:2px}
-.swarm-shell{display:block;position:relative;height:calc(100vh - 108px);min-height:640px}
-.swarm-space{position:absolute;inset:0;background:radial-gradient(ellipse at 50% 48%,#1a273940,transparent 58%),radial-gradient(ellipse at 72% 55%,#252b4233,transparent 45%)}
-#swarm-stage{height:100%;min-height:0}
-.space-toolbar{position:absolute;top:18px;left:310px;right:24px;z-index:8;border:0;padding:0;gap:14px;align-items:flex-start}
-.has-selection .space-toolbar{right:382px}
-.space-controls{font-size:10px;gap:10px;color:var(--muted)}
-.space-controls button,.space-controls select{font-size:10px;padding:5px 7px}
-.follow-control{color:var(--quiet);font-size:10px}
-#swarm-path{font-family:var(--mono);font-size:10px}
-#swarm-path button{font-size:10px;padding:4px 7px;border-color:transparent;max-width:140px}
-#swarm-path button[aria-pressed=true]{color:var(--signal);background:transparent;border-bottom-color:var(--signal)}
-.swarm-navigation{position:absolute;z-index:10;left:20px;top:18px;bottom:76px;width:266px;border:1px solid var(--rule);padding:20px 17px;background:#10151de8;overflow:auto}
+body{margin:0;background:var(--void);color:var(--text);overflow-x:hidden}
+#swarm-app{position:relative;isolation:isolate}
+button,input,select{border-radius:3px;font-family:inherit}
+button:focus-visible,input:focus-visible,select:focus-visible,canvas:focus-visible,summary:focus-visible{outline:1px solid var(--accent);outline-offset:3px}
+button{background:transparent;border:1px solid var(--rule);color:var(--muted);transition:color .14s,border-color .14s,background .14s}
+button:hover{background:rgba(127,209,222,.07);border-color:var(--rule-strong);color:var(--text)}
+button[aria-pressed=true]{color:var(--text);border-color:var(--accent);background:rgba(127,209,222,.09)}
+kbd{font:9px var(--mono);border:1px solid var(--rule);border-radius:2px;padding:1px 3px;color:var(--faint);margin-left:5px}
+
+/* ── Masthead ───────────────────────────────────────────────────────────── */
+.swarm-top{position:relative;z-index:20;height:66px;padding:0 26px;gap:18px;align-items:center;
+ border-bottom:1px solid var(--rule);background:linear-gradient(#0a0e16,#070a11)}
+.swarm-brand{display:flex;align-items:center;gap:11px;font:400 21px/1 var(--serif);letter-spacing:-.035em;color:#e4ecf6;white-space:nowrap}
+.swarm-brand b{color:#b7d9e8;font-weight:400}
+.brand-mark{width:13px;height:13px;border-radius:50%;flex:0 0 auto;
+ background:radial-gradient(circle at 34% 32%,#eafaff,#7fd1de 46%,#2a5c6a 78%,#0a1016);
+ box-shadow:0 0 0 1px rgba(127,209,222,.4),0 0 16px rgba(127,209,222,.38)}
+.swarm-top button{font:10px var(--mono);letter-spacing:.1em;text-transform:uppercase;padding:7px 11px}
+#swarm-status{flex:1;font:10px/1.5 var(--mono);color:var(--muted);text-align:right;letter-spacing:.02em}
+.read-only{font:9px var(--mono);letter-spacing:.14em;color:var(--quiet);border:1px solid var(--rule);border-radius:2px;padding:4px 7px;white-space:nowrap}
+
+/* ── The field ──────────────────────────────────────────────────────────── */
+.swarm-shell{display:block;position:relative;height:calc(100vh - 112px);min-height:660px}
+.swarm-space{position:absolute;inset:0;overflow:hidden;background:
+ radial-gradient(130% 92% at 50% 128%,#12243390 0%,transparent 62%),
+ radial-gradient(90% 70% at 74% 18%,#1b1f3c66 0%,transparent 58%),
+ radial-gradient(80% 60% at 16% 30%,#10303a55 0%,transparent 60%),
+ linear-gradient(#060911,#04060b)}
+.field-backdrop{position:absolute;inset:0;z-index:0;pointer-events:none}
+.field-backdrop>span{position:absolute;inset:0;display:block}
+.field-nebula{background:
+ conic-gradient(from 210deg at 46% 54%,#0e3a4733,#1d2a5a2e,#3a1f452b,#0e3a4733);
+ filter:blur(70px);opacity:.55}
+.field-dust{opacity:.5;background:
+ radial-gradient(1px 1px at 12% 18%,#cfe9f2aa,transparent 60%),
+ radial-gradient(1px 1px at 72% 12%,#bcd6ff99,transparent 60%),
+ radial-gradient(1px 1px at 38% 74%,#e4d8ff88,transparent 60%),
+ radial-gradient(1px 1px at 86% 62%,#cfe9f299,transparent 60%),
+ repeating-radial-gradient(circle at 20% 30%,#9fc4d41a 0 1px,transparent 1px 90px),
+ repeating-radial-gradient(circle at 78% 68%,#aab8e018 0 1px,transparent 1px 124px)}
+.field-horizon{top:auto;bottom:16%;height:1px;background:linear-gradient(90deg,transparent,#5e8fa233 18%,#9fd3e155 50%,#5e8fa233 82%,transparent);opacity:.5}
+.field-vignette{background:radial-gradient(120% 100% at 50% 46%,transparent 38%,#04060bcc 100%)}
+#swarm-stage{position:absolute;inset:0;z-index:1;height:100%;min-height:0;overflow:hidden}
+#swarm-canvas{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;background:transparent}
+#swarm-labels{position:absolute;inset:0;pointer-events:none;overflow:hidden;z-index:2}
+
+/* ── Shared glass plate ─────────────────────────────────────────────────── */
+.swarm-navigation,.swarm-inspector,.swarm-timeline,.swarm-diagnostics,.space-toolbar>.space-controls,#swarm-path{
+ background:var(--plate);backdrop-filter:blur(14px) saturate(1.1);-webkit-backdrop-filter:blur(14px) saturate(1.1);
+ border:1px solid var(--rule);box-shadow:0 24px 60px -30px #000c,inset 0 1px 0 rgba(190,220,240,.07)}
+.section-title{font:var(--kicker);letter-spacing:.17em;color:var(--quiet);text-transform:uppercase}
+
+/* ── Atlas rail ─────────────────────────────────────────────────────────── */
+.swarm-navigation{position:absolute;z-index:12;left:22px;top:22px;bottom:86px;width:294px;padding:22px 20px;overflow:auto;scrollbar-width:thin}
.navigation-hidden .swarm-navigation{display:none}
-.navigation-hidden .space-toolbar{left:24px}
-.navigation-hidden .space-caption{left:32px}
-.section-title{font:9px var(--mono);letter-spacing:.15em;color:var(--quiet);margin-bottom:18px}
-.search-label{font-size:10px;color:var(--muted)}
-input[type=search]{font-size:11px;background:#0b1119;border-color:#2c3645;padding:10px;color:var(--text)}
-#source-list{margin:22px 0}
-#source-list button{font:15px var(--serif);padding:11px 8px 10px 12px;border:0;border-left:2px solid #7fd1de;border-bottom:1px solid var(--rule);margin:0;border-radius:0;background:transparent}
-#source-list button:nth-child(2){border-left-color:#9bb8e8}
-#source-list button:nth-child(3){border-left-color:#c2a7e8}
-#source-list button:nth-child(4){border-left-color:#e8a7b8}
-#source-list button:nth-child(5){border-left-color:#e8c98a}
-#source-list button small{font:10px var(--mono);color:var(--quiet);margin-top:5px}
-#source-list button[aria-pressed=true]{background:#1a2936;color:#e8f7fb}
-#source-list button.source-stale,#source-list button.source-unavailable{background:repeating-linear-gradient(45deg,transparent 0 4px,#a7b2c208 4px 5px);color:var(--muted)}
-#branch-list{max-height:36vh}
-#branch-list button{font-size:11px;border:0;border-bottom:1px solid #202b35;border-radius:0;background:transparent;padding:9px 6px;margin:0}
-#branch-list button small{font:9px var(--mono);color:var(--quiet)}
-#branch-pages{font:10px var(--mono);color:var(--quiet)}
-#branch-pages button{font-size:10px;margin-top:8px}
-#search-results button{border:0;border-bottom:1px solid var(--rule);font-size:11px;border-radius:0;padding:8px 4px}
-.scope-note{font-size:10px;color:var(--quiet);border-top:1px dotted #33414c;padding-top:14px;line-height:1.6}
-.space-caption{left:320px;bottom:92px;color:var(--muted);font:11px var(--mono)}
-.observatory-title{font:38px var(--serif);letter-spacing:-.025em;color:#dee8f2;margin-bottom:10px}
-.has-selection .observatory-title{font-size:27px}
-.space-caption p{font:10px var(--mono);color:var(--quiet);margin-top:8px}
-.swarm-label{font:10px var(--mono);color:#97adbb;text-shadow:0 1px 4px #050a10}
-.swarm-label.source-label{font:17px var(--serif);letter-spacing:.015em;color:#c7e0e7}
-.swarm-label.selected{color:#eafcff}
-.swarm-inspector{display:none;position:absolute;z-index:12;right:20px;top:76px;bottom:76px;width:340px;padding:22px;background:#10151df2;border:1px solid var(--rule);overflow:auto}
+.swarm-navigation>.section-title:first-child{display:block;margin-bottom:16px;color:var(--muted)}
+.search-label{display:grid;gap:8px;font:10px var(--mono);letter-spacing:.08em;color:var(--quiet);text-transform:uppercase}
+input[type=search]{width:100%;font:12px var(--mono);background:#060a11;border:1px solid var(--rule);padding:10px 11px;color:var(--text)}
+input[type=search]::placeholder{color:#4e5b6d}
+#search-results{max-height:30vh;overflow:auto;margin-top:12px}
+#search-results p{font:10px/1.6 var(--mono);color:var(--quiet);margin:0 0 6px}
+#search-results button{display:block;width:100%;text-align:left;border:0;border-bottom:1px solid rgba(150,180,210,.08);border-radius:0;font:11px var(--mono);padding:9px 2px;margin:0;color:var(--muted);overflow-wrap:anywhere}
+.rail-heading{display:flex;align-items:center;gap:10px;margin:26px 0 12px}
+.rail-heading .section-title{margin:0}
+.rail-heading::after{content:'';flex:1;height:1px;background:linear-gradient(90deg,var(--rule),transparent)}
+#source-list{margin:0 0 6px;display:grid}
+#source-list button{display:grid;grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto auto;column-gap:11px;align-items:center;
+ width:100%;text-align:left;margin:0;padding:13px 10px 12px 12px;border:0;border-bottom:1px solid rgba(150,180,210,.08);border-radius:0;background:transparent;position:relative}
+#source-list button::before{content:'';position:absolute;left:0;top:0;bottom:0;width:2px;background:var(--accent);opacity:.55}
+#source-list button:hover::before,#source-list button[aria-pressed=true]::before{opacity:1;box-shadow:0 0 12px var(--accent)}
+#source-list button[aria-pressed=true]{background:linear-gradient(90deg,rgba(127,209,222,.1),transparent 72%)}
+.source-dot{grid-row:1/3;width:9px;height:9px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 1px #04060b,0 0 10px var(--accent)}
+.source-name{font:400 16px/1.25 var(--serif);letter-spacing:-.015em;color:#e6eef7;overflow-wrap:anywhere}
+#source-list button small{grid-column:2;font:9px var(--mono);letter-spacing:.08em;color:var(--quiet);margin-top:4px;text-transform:uppercase}
+#source-list button.source-stale .source-dot,#source-list button.source-unavailable .source-dot{background:#6b7789;box-shadow:none}
+#source-list button.source-stale,#source-list button.source-unavailable{color:var(--muted)}
+#source-list button.source-stale .source-name,#source-list button.source-unavailable .source-name{color:#b0bccb}
+#source-list button.source-unavailable::before{background:repeating-linear-gradient(180deg,#6b7789 0 3px,transparent 3px 7px);opacity:.8}
+#branch-heading{margin:22px 0 10px;color:var(--muted)}
+#branch-list{max-height:30vh;overflow:auto;display:grid}
+#branch-list button{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:9px;align-items:baseline;width:100%;text-align:left;
+ border:0;border-bottom:1px solid rgba(150,180,210,.07);border-radius:0;background:transparent;padding:9px 4px;margin:0}
+.branch-dot{width:5px;height:5px;border-radius:50%;background:#55637a;transform:translateY(-2px)}
+.activity-recent .branch-dot{background:var(--accent);box-shadow:0 0 8px var(--accent)}
+.activity-uncertain .branch-dot{background:var(--warn);box-shadow:0 0 8px rgba(224,168,92,.5)}
+.branch-name{font:11px var(--mono);color:#cdd9e8;overflow-wrap:anywhere}
+#branch-list button small{grid-column:2;display:block;font:9px var(--mono);color:var(--quiet);margin-top:4px}
+#branch-list button[aria-pressed=true] .branch-name{color:#eafcff}
+#branch-pages{font:9px/1.7 var(--mono);color:var(--quiet);margin-top:10px}
+#branch-pages button{font:9px var(--mono);letter-spacing:.08em;padding:6px 8px;margin:6px 6px 0 0;text-transform:uppercase}
+#branch-pages p{margin:8px 0 0}
+.scope-note{font:10px/1.75 var(--mono);color:var(--faint);border-top:1px solid var(--rule);padding-top:15px;margin-top:20px}
+
+/* ── Sextant bar: path + view controls ──────────────────────────────────── */
+.space-toolbar{position:absolute;z-index:14;top:22px;left:338px;right:26px;border:0;padding:0;display:flex;gap:14px;align-items:flex-start;justify-content:space-between;pointer-events:none}
+.has-selection .space-toolbar{right:422px}
+.space-toolbar>*{pointer-events:auto}
+#swarm-path{display:flex;flex-wrap:wrap;align-items:center;gap:3px;font:10px var(--mono);padding:5px 9px;border-radius:999px;max-width:min(560px,56%)}
+#swarm-path button{font:10px var(--mono);padding:4px 8px;border:0;border-radius:999px;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--quiet)}
+#swarm-path button:hover{color:var(--text);background:rgba(127,209,222,.08)}
+#swarm-path button[aria-pressed=true]{color:#eafcff;background:rgba(127,209,222,.13);border:0}
+#swarm-path span{color:#45505f;font-size:9px;padding:0 1px}
+.space-controls{display:flex;align-items:center;gap:8px;padding:6px 9px;border-radius:999px;font:10px var(--mono);color:var(--muted)}
+.space-controls button{font:9px var(--mono);letter-spacing:.1em;text-transform:uppercase;padding:6px 9px;border-radius:999px}
+.control-field,.follow-control{display:flex;align-items:center;gap:6px;font:9px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--quiet)}
+.space-controls select{font:10px var(--mono);padding:5px 6px;background:#070b12;border:1px solid var(--rule);color:var(--muted)}
+.follow-control input{accent-color:var(--accent);width:12px;height:12px}
+
+/* ── Caption: the display voice of the field ────────────────────────────── */
+.space-caption{position:absolute;z-index:6;left:340px;bottom:96px;max-width:min(430px,38vw);pointer-events:none;color:var(--muted)}
+.navigation-hidden .space-caption{left:34px}
+.navigation-hidden .space-toolbar{left:26px}
+.caption-kicker{font:var(--kicker);letter-spacing:.24em;color:var(--faint);margin:0 0 12px;text-transform:uppercase}
+.observatory-title{font:400 clamp(30px,3.6vw,50px)/1.02 var(--serif);letter-spacing:-.035em;color:#eaf2fa;margin:0 0 12px;
+ text-shadow:0 2px 34px #04060b,0 0 70px rgba(127,209,222,.12)}
+.has-selection .observatory-title{font-size:clamp(24px,2.3vw,32px)}
+#swarm-counts{display:block;font:10px/1.7 var(--mono);letter-spacing:.06em;color:#9fb0c5}
+.space-caption p{font:10px/1.75 var(--mono);color:var(--faint);margin:8px 0 0;max-width:36ch}
+.field-legend{display:flex;flex-wrap:wrap;gap:6px 16px;list-style:none;margin:16px 0 0;padding:14px 0 0;border-top:1px solid var(--rule);font:9px var(--mono);letter-spacing:.07em;color:var(--quiet);text-transform:uppercase}
+.field-legend li{display:flex;align-items:center;gap:7px}
+.field-legend i{width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 9px var(--accent)}
+.legend-uncertain i{background:var(--warn);box-shadow:0 0 9px rgba(224,168,92,.55)}
+.legend-quiet i{background:#6b7789;box-shadow:none}
+.legend-plate i{background:transparent;border:1px solid var(--accent);box-shadow:0 0 9px rgba(127,209,222,.25)}
+.has-selection .field-legend{display:none}
+
+/* ── Field labels ───────────────────────────────────────────────────────── */
+.swarm-label{position:absolute;top:0;left:0;font:10px var(--mono);letter-spacing:.04em;color:#a2b6c7;white-space:nowrap;max-width:168px;overflow:hidden;text-overflow:ellipsis;
+ padding:2px 6px;border-radius:2px;background:rgba(5,8,14,.42);text-shadow:0 1px 6px #04060b}
+.swarm-label.active{color:#d6f4fb;box-shadow:inset 0 0 0 1px rgba(127,209,222,.22)}
+.swarm-label.source-label{display:flex;flex-direction:column;align-items:center;gap:2px;font:400 18px/1.1 var(--serif);letter-spacing:-.01em;color:#dcecf4;background:transparent;padding:0;max-width:220px}
+.swarm-label.source-label i{font:9px var(--mono);font-style:normal;letter-spacing:.2em;text-transform:uppercase;color:var(--accent);opacity:.75}
+.swarm-label.selected{color:#f1fdff;background:rgba(127,209,222,.12);box-shadow:inset 0 0 0 1px rgba(127,209,222,.4)}
+
+/* ── Evidence sheet ─────────────────────────────────────────────────────── */
+.swarm-inspector{display:none;position:absolute;z-index:16;right:22px;top:22px;bottom:86px;width:380px;padding:24px 24px 30px;overflow:auto;scrollbar-width:thin}
+.swarm-inspector::before{content:'';position:absolute;left:0;top:0;bottom:0;width:2px;background:linear-gradient(180deg,var(--accent),transparent 72%);opacity:.8}
.has-selection .swarm-inspector{display:block}
-.swarm-inspector>.section-title{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--rule);padding-bottom:14px}
-#swarm-close{font-size:18px;padding:0 5px;color:var(--quiet);border:0}
-.swarm-inspector h2{font:26px/1.15 var(--serif);letter-spacing:-.025em;margin:18px 0}
-.swarm-inspector h3{font:15px var(--serif);color:#d3e5e9;border-top:1px solid var(--rule);padding-top:18px;margin-top:26px}
-.swarm-inspector p{font-size:12px;color:var(--muted);line-height:1.65}
-.swarm-inspector dl{display:block;font:11px/1.5 var(--mono)}
-.swarm-inspector dt{color:var(--quiet);padding-top:12px;margin:0;border-top:1px solid #25303a}
-.swarm-inspector dd{margin:3px 0 12px;color:#d0dae5}
-.swarm-inspector button:not(#swarm-close){display:block;width:100%;text-align:left;font-size:11px;border:0;border-bottom:1px solid var(--rule);border-radius:0;padding:10px 0;margin:0;color:#bbd9df}
-.source-state{font:9px var(--mono);letter-spacing:.1em;color:var(--signal);line-height:1.6}
-.evidence-note{border:0!important;border-left:2px solid #d7a86b!important;background:#2b251d60!important;border-radius:0;padding:11px 13px;color:#d4bc96!important}
-.swarm-timeline{position:absolute;bottom:20px;left:310px;z-index:14;width:min(490px,calc(100% - 710px));padding:14px 16px;border:1px solid var(--rule);background:#10151dec}
-.swarm-timeline summary{margin:0;cursor:pointer;font:9px var(--mono);letter-spacing:.12em;color:var(--muted)}
-.swarm-timeline summary span{font-size:9px;color:var(--quiet);letter-spacing:0}
-#tracking-list{font-family:var(--mono);font-size:10px;max-height:180px;padding-top:10px}
-.swarm-measurements{position:absolute;right:20px;bottom:27px;z-index:16;font:9px var(--mono);color:var(--quiet);max-width:300px;text-align:right}
-.swarm-measurements summary{cursor:pointer}
-.swarm-diagnostics{font:9px/1.8 var(--mono);border:1px solid var(--rule);background:#10151df2;margin-top:10px;padding:10px;color:var(--muted);text-align:left}
-.sample-notice{color:#dfba80!important}
-footer{height:44px;position:relative;border-color:var(--rule);font:9px/1.65 var(--mono);color:var(--quiet);padding:14px 24px;background:#0c1017}
+.swarm-inspector>.section-title{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--rule);padding-bottom:14px;margin-bottom:20px;color:var(--muted)}
+#swarm-close{font-size:17px;line-height:1;padding:2px 6px;color:var(--quiet);border:0}
+.evidence-kicker{font:var(--kicker)!important;letter-spacing:.22em!important;color:var(--accent)!important;margin:0 0 6px!important;text-transform:uppercase}
+.source-state{font:9px var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--quiet);margin-bottom:4px}
+.swarm-inspector h2{font:400 clamp(21px,2vw,28px)/1.14 var(--serif);letter-spacing:-.03em;margin:6px 0 18px;color:#eaf2fa;overflow-wrap:anywhere}
+.swarm-inspector h3{font:400 15px var(--serif);color:#d4e4ea;border-top:1px solid var(--rule);padding-top:17px;margin:28px 0 6px}
+.swarm-inspector p{font:11.5px/1.7 var(--mono);color:var(--muted);overflow-wrap:anywhere}
+.swarm-inspector dl{display:block;font:11px/1.55 var(--mono);margin:14px 0 0}
+.swarm-inspector dt{color:var(--quiet);padding-top:11px;margin:0;border-top:1px solid rgba(150,180,210,.09);font-size:9px;letter-spacing:.11em;text-transform:uppercase}
+.swarm-inspector dd{margin:4px 0 11px;color:#d6e1ee;overflow-wrap:anywhere}
+.swarm-inspector button:not(#swarm-close){display:block;width:100%;text-align:left;font:10.5px var(--mono);border:0;border-bottom:1px solid rgba(150,180,210,.09);border-radius:0;padding:10px 2px;margin:0;color:#bcd8e0;overflow-wrap:anywhere}
+.swarm-inspector button:not(#swarm-close):hover{color:#eafcff;background:rgba(127,209,222,.06)}
+.evidence-note{border:0!important;border-left:2px solid var(--warn)!important;background:rgba(224,168,92,.07)!important;border-radius:0;padding:11px 14px!important;color:#dcc49d!important;margin:14px 0}
+
+/* ── Tracking ledger and instruments ────────────────────────────────────── */
+.swarm-timeline{position:absolute;z-index:15;bottom:24px;left:340px;width:min(470px,calc(100% - 800px));padding:13px 16px;border-radius:3px}
+.swarm-timeline summary{display:flex;gap:10px;align-items:baseline;cursor:pointer;margin:0;font:var(--kicker);letter-spacing:.16em;color:var(--muted);text-transform:uppercase}
+.swarm-timeline summary::marker{color:var(--faint)}
+.swarm-timeline summary span{font:9px var(--mono);letter-spacing:.04em;color:var(--faint);text-transform:none}
+#tracking-list{font:10px/1.6 var(--mono);max-height:190px;overflow:auto;padding-top:12px}
+#tracking-list p{color:var(--faint);margin:0}
+.tracking-row{display:grid;grid-template-columns:auto auto minmax(0,1fr);gap:12px;padding:7px 0;border-bottom:1px solid rgba(150,180,210,.07);color:#c3cfdd}
+.tracking-row time{color:var(--faint);font-size:9px}
+.swarm-measurements{position:absolute;z-index:17;right:24px;bottom:28px;max-width:320px;text-align:right;font:9px var(--mono);color:var(--faint)}
+.swarm-measurements summary{cursor:pointer;letter-spacing:.14em;text-transform:uppercase}
+.swarm-diagnostics{font:9px/1.85 var(--mono);margin-top:10px;padding:11px 12px;color:var(--muted);text-align:left;overflow-wrap:anywhere}
+.sample-notice{color:#e3bf85!important}
+footer{position:relative;z-index:20;height:46px;display:flex;flex-wrap:wrap;align-items:center;gap:7px 18px;
+ border-top:1px solid var(--rule);font:9px/1.6 var(--mono);letter-spacing:.07em;color:var(--faint);padding:0 26px;background:#070a11;text-transform:uppercase}
+footer b{color:var(--warn);font-weight:400}
+.field-empty .observatory-title{opacity:.75}
+
+/* ── Responsive framing ─────────────────────────────────────────────────── */
+@media(max-width:1320px){
+ .swarm-timeline{width:min(420px,calc(100% - 760px))}
+ .swarm-inspector{width:340px}
+ .has-selection .space-toolbar{right:378px}
+}
@media(max-width:1120px){
- .swarm-navigation{width:220px;left:14px;padding:18px 14px}
- .space-toolbar{left:252px;right:18px}
- .has-selection .space-toolbar{right:316px}
- .swarm-inspector{width:278px;right:14px;padding:18px;top:100px}
- .space-caption{left:256px;bottom:100px}.observatory-title{font-size:32px}
- .swarm-timeline{left:252px;width:calc(100% - 568px)}
+ .swarm-navigation{width:252px;left:16px;top:16px;padding:18px 16px}
+ .space-toolbar{left:282px;right:18px;top:16px}
+ .has-selection .space-toolbar{right:312px}
+ .swarm-inspector{width:286px;right:16px;top:74px;padding:20px}
+ .space-caption{left:284px;bottom:104px;max-width:min(360px,44vw)}
+ .swarm-timeline{left:282px;width:calc(100% - 620px)}
+ .field-legend{gap:5px 12px}
+}
+@media(max-width:900px){
+ .swarm-timeline{display:none}
+ .space-caption{bottom:38px}
}
@media(max-width:800px){
- .swarm-top{height:auto;min-height:76px;gap:12px;padding:12px 16px;position:sticky;top:0;z-index:30}
+ .swarm-top{height:auto;min-height:72px;flex-wrap:wrap;gap:10px;padding:12px 16px;position:sticky;top:0}
+ .swarm-brand{font-size:19px}
#swarm-status{order:4;flex-basis:100%;text-align:left;font-size:9px}
- .swarm-brand{font-size:19px}.read-only{margin-left:auto}.swarm-top button{font-size:10px}
+ .read-only{margin-left:auto}
.swarm-shell{height:auto;min-height:0;display:block}
- .swarm-space{position:relative;height:600px}
- #swarm-stage{height:100%}
- .space-toolbar,.has-selection .space-toolbar{top:14px;left:14px;right:14px;justify-content:space-between;gap:10px}
- .space-controls{gap:6px;font-size:9px}.space-controls label{font-size:9px}
- .swarm-navigation{left:14px;right:14px;top:92px;bottom:auto;width:auto;max-height:220px;padding:10px 12px;background:#10151ddb}
- .swarm-navigation>.section-title:first-child,.search-label,#search-results,.scope-note{display:none}
- .search-open .search-label,.search-open #search-results{display:block}
- .search-open .search-label input{margin-top:7px}
- .search-open .swarm-navigation{max-height:360px}
- #source-list{margin:0;display:flex;gap:10px;overflow:auto}
- #source-list button{min-width:150px;width:auto;padding:8px 10px;font-size:14px;border-bottom:0}
- #source-list button small{font-size:9px}
- #branch-heading{font-size:8px;margin:10px 0 6px}
- #branch-list{max-height:98px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px}
- #branch-list button{font-size:10px;padding:7px 4px}
- #branch-list button small{font-size:8px}
- .space-caption,.navigation-hidden .space-caption{left:20px;bottom:95px;font-size:9px}.observatory-title{font-size:32px}.has-selection .observatory-title{font-size:27px}
- .swarm-timeline{left:14px;right:14px;bottom:20px;width:auto;padding:12px;max-width:calc(100% - 28px)}
- .swarm-measurements{right:14px;bottom:67px;font-size:8px}
- .swarm-inspector{position:relative;right:auto;left:auto;top:auto;bottom:auto;width:auto;margin:0 14px 20px;padding:22px;max-height:none}
- .swarm-inspector dl{display:block}.swarm-inspector dd{margin:4px 0 13px}
+ .swarm-space{position:relative;height:clamp(420px,62vh,620px)}
+ .space-toolbar,.has-selection .space-toolbar{top:12px;left:12px;right:12px;flex-wrap:wrap;gap:8px}
+ #swarm-path{max-width:100%}
+ .swarm-navigation{position:relative;left:auto;top:auto;bottom:auto;width:auto;margin:16px 12px 0;max-height:none;padding:18px 16px;backdrop-filter:none;background:var(--plate-solid)}
+ .navigation-hidden .swarm-navigation{display:none}
+ #search-results{max-height:none}
+ #source-list{grid-auto-flow:column;grid-auto-columns:minmax(186px,1fr);overflow:auto;gap:10px;padding-bottom:6px}
+ #source-list button{border-bottom:0;border:1px solid var(--rule);padding:12px}
+ #branch-list{max-height:220px}
+ .space-caption,.navigation-hidden .space-caption{left:18px;bottom:24px;max-width:calc(100% - 36px)}
+ .observatory-title,.has-selection .observatory-title{font-size:clamp(26px,7vw,36px)}
+ .field-legend,.has-selection .field-legend{display:flex}
+ .swarm-measurements{right:14px;bottom:14px;max-width:62%}
+ .swarm-inspector,.has-selection .swarm-inspector{display:block;position:relative;right:auto;left:auto;top:auto;bottom:auto;width:auto;margin:16px 12px;padding:22px;backdrop-filter:none;background:var(--plate-solid)}
footer{height:auto;padding:14px 16px}
}
@media(max-width:520px){
- .swarm-brand{font-size:18px}.swarm-top{gap:9px}#swarm-find{margin-left:auto}.read-only{display:none}
- .swarm-space{height:540px}.space-toolbar{align-items:flex-start}.space-controls{flex-basis:100%}
- .swarm-navigation{top:104px}.space-caption{max-width:calc(100% - 40px)}
- .space-caption p{font-size:9px}.observatory-title{font-size:30px}
- .swarm-inspector h2{font-size:27px}
+ .swarm-brand{font-size:18px}
+ #swarm-find{margin-left:auto}
+ .read-only{display:none}
+ .swarm-space{height:clamp(500px,62vh,600px)}
+ .space-controls{flex-wrap:wrap}
+ .field-legend li:nth-child(n+3){display:none}
+ .space-caption p{display:none}
+ .swarm-inspector h2{font-size:22px}
+}
+@media(prefers-reduced-motion:reduce){
+ *{transition:none!important;animation:none!important}
+ .field-nebula{filter:none;opacity:.35}
}
diff --git a/src/swarm/preview.ts b/src/swarm/preview.ts
index 4014341..a7cfdbf 100644
--- a/src/swarm/preview.ts
+++ b/src/swarm/preview.ts
@@ -11,7 +11,7 @@ export function swarmPreview(totalCells = 54): Swarm {
depth: index === 0 ? 0 : Math.floor(Math.log(index * 3 + 1) / Math.log(4)), role: index === 0 ? 'coordinator' : index % 5 === 0 ? 'verifier' : index % 7 === 0 ? 'watcher' : 'developer',
status: index % 17 === 16 ? 'retired' : 'ready', heartbeat: now,
purpose: index === 0 ? 'Coordinate the recorded local delegation.' : `Inspect the work assigned to ${label} cell ${index}.`,
- allocationCents: index === 0 ? 10000 : 500, budgetBasis: 'logical-allocation',
+ allocationCents: Math.floor(10000 / 4 ** (index === 0 ? 0 : Math.floor(Math.log(index * 3 + 1) / Math.log(4)))), budgetBasis: 'logical-allocation',
}));
const tasks = cells.length > 1 ? [{ id: 'implementation', projectId: 'flujo', branch: 'codex/sample', status: 'running' as const, owner: 'cell-1', attempt: 1, leaseExpiry: future, specDigest: digest, candidate: null, review: null },
{ id: 'retire-operation', projectId: 'flujo', branch: 'codex/sample-retire', status: 'completed' as const, owner: cells.length > 2 ? 'cell-2' : 'root', attempt: 1, leaseExpiry: null, specDigest: digest, candidate: null, review: null },
diff --git a/src/swarm/renderer.ts b/src/swarm/renderer.ts
index ef305bb..b2c2af7 100644
--- a/src/swarm/renderer.ts
+++ b/src/swarm/renderer.ts
@@ -2,13 +2,44 @@ import { BufferGeometry, Color, Float32BufferAttribute, LineBasicMaterial, LineS
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import type { Node, Point } from './model';
+/** Deterministic authority palette. The navigation indicators read the same
+ * function, so a plate in the field and its index entry cannot disagree. */
const hues = ['#7fd1de', '#9bb8e8', '#c2a7e8', '#e8a7b8', '#e8c98a', '#8fd9b6'];
-const sourceHue = (id: string) => hues[[...id].reduce((sum, char) => (sum * 31 + char.charCodeAt(0)) >>> 0, 0) % hues.length]!;
-const tint = (node: Node): string => node.source.status !== 'observed' ? '#7d8798' : node.activity === 'uncertain' ? '#e0a85c' : node.cell?.status === 'retired' ? '#7d8798' : sourceHue(node.source.id);
-const vertexShader = `attribute float aSize; attribute float aActive; attribute float aFill; varying vec3 vColor; varying float vActive; varying float vFill; uniform float uTime;
-void main(){vColor=color; vActive=aActive; vFill=aFill; vec4 p=modelViewMatrix*vec4(position,1.); float pulse=1.+aActive*.08*sin(uTime*2.); gl_PointSize=clamp(aSize*pulse*180./max(1.,-p.z),7.,56.); gl_Position=projectionMatrix*p;}`;
-const fragmentShader = `varying vec3 vColor; varying float vActive; varying float vFill;
-void main(){float d=length(gl_PointCoord-.5)*2.; if(d>1.)discard; float ring=smoothstep(.7,.8,d)*(1.-smoothstep(.86,1.,d)); float core=(1.-smoothstep(.2,.54,d))*vFill; float glow=exp(-d*d*5.)*.12*vFill; gl_FragColor=vec4(mix(vColor,vec3(.92,.98,1.),core*.6),ring*.8+core*.7+glow);}`;
+export const sourceHue = (id: string) => hues[[...id].reduce((sum, char) => (sum * 31 + char.charCodeAt(0)) >>> 0, 0) % hues.length]!;
+/** Shared with the atlas rail; identical input gives an identical colour. */
+export const sourceColor = sourceHue;
+export const uncertainHue = '#e0a85c';
+export const retiredHue = '#7d8798';
+const tint = (node: Node): string => node.source.status !== 'observed' ? retiredHue : node.activity === 'uncertain' ? uncertainHue : node.cell?.status === 'retired' ? retiredHue : sourceHue(node.source.id);
+
+/** One batched point program: engraved plates, cell rings, cores and haloes.
+ * Depth attenuation keeps a deep field legible without a second pass. */
+const vertexShader = `attribute float aSize; attribute float aActive; attribute float aFill; attribute float aPlate; attribute float aSelected;
+varying vec3 vColor; varying float vActive; varying float vFill; varying float vPlate; varying float vSelected; varying float vFade; uniform float uTime;
+void main(){
+ vColor=color; vActive=aActive; vFill=aFill; vPlate=aPlate; vSelected=aSelected;
+ vec4 p=modelViewMatrix*vec4(position,1.);
+ float depth=max(1.,-p.z);
+ vFade=clamp(1.25-depth/5200.,.34,1.);
+ float breath=1.+aActive*.09*sin(uTime*2.+position.x*.12);
+ gl_PointSize=clamp(aSize*breath*(1.+aSelected*.26)*200./depth,7.,78.);
+ gl_Position=projectionMatrix*p;
+}`;
+const fragmentShader = `varying vec3 vColor; varying float vActive; varying float vFill; varying float vPlate; varying float vSelected; varying float vFade;
+void main(){
+ vec2 uv=gl_PointCoord-.5; float d=length(uv)*2.;
+ if(d>1.)discard;
+ float core=(1.-smoothstep(.0,.40,d))*vFill;
+ float ring=smoothstep(.49,.58,d)*(1.-smoothstep(.70,.80,d));
+ float plate=vPlate*smoothstep(.86,.91,d)*(1.-smoothstep(.97,1.,d));
+ float halo=exp(-d*d*3.6)*(.10+.12*vFill);
+ vec2 q=abs(uv)*2.;
+ float spike=vSelected*max(exp(-q.x*22.),exp(-q.y*22.))*(1.-smoothstep(.2,1.,d))*.55;
+ float select=vSelected*smoothstep(.80,.88,d)*(1.-smoothstep(.95,1.,d));
+ vec3 lit=mix(vColor,vec3(.93,.99,1.),clamp(core*.55+spike*.7+select*.8,0.,1.));
+ float alpha=(ring*.9+core*.85+halo+plate*.55+spike*.6+select*.95)*vFade;
+ gl_FragColor=vec4(lit,clamp(alpha,0.,1.));
+}`;
/** Two semantic draw batches, bounded labels, no postprocessing or idle orbit.
* Rendering is demand-driven; evidenced activity pulses at no more than 30fps. */
@@ -16,7 +47,7 @@ export class SwarmRenderer {
private gl: WebGLRenderer | null = null;
private ctx: CanvasRenderingContext2D | null = null;
private scene = new Scene();
- private camera = new PerspectiveCamera(50, 1, .1, 20000);
+ private camera = new PerspectiveCamera(46, 1, .1, 20000);
private controls: OrbitControls | null = null;
private points: Points | null = null;
private lines: LineSegments | null = null;
@@ -34,10 +65,13 @@ export class SwarmRenderer {
private observer: ResizeObserver;
private disposeInput: () => void;
private disposed = false;
+ private motion = matchMedia('(prefers-reduced-motion: reduce)');
private labelElements: HTMLSpanElement[] = [];
+ private labelSizes = new Map();
private submissions = 0;
private lastSubmissionMs = 0;
private flight: { start: number; fromPosition: Vector3; fromTarget: Vector3; toPosition: Vector3; toTarget: Vector3 } | null = null;
+ private lastFit: { point?: Point; sourceId?: string } | null = null;
readonly mode: '3d' | '2d';
onPick: (node: Node) => void = () => undefined;
onStats: (stats: { mode: string; visible: number; labels: number; draws: number; submissions: number; cpuMs: number }) => void = () => undefined;
@@ -47,13 +81,14 @@ export class SwarmRenderer {
try {
this.gl = new WebGLRenderer({ canvas, antialias: false, powerPreference: 'low-power', alpha: true });
this.gl.setPixelRatio(Math.min(devicePixelRatio || 1, 1.25));
- this.gl.setClearColor(0x081216, 0);
- this.camera.position.set(40, 120, 180);
+ this.gl.setClearColor(0x05070c, 0);
+ this.camera.position.set(52, 128, 196);
this.controls = new OrbitControls(this.camera, canvas);
this.controls.enableDamping = false;
this.controls.enablePan = true;
this.controls.minDistance = 8;
this.controls.maxDistance = 10000;
+ this.controls.maxPolarAngle = Math.PI * .92;
this.controls.addEventListener('change', this.request);
canvas.addEventListener('webglcontextlost', this.contextLost);
} catch { this.gl = null; }
@@ -69,73 +104,142 @@ export class SwarmRenderer {
this.observer = new ResizeObserver(this.resize);
this.observer.observe(this.canvas.parentElement!);
document.addEventListener('visibilitychange', this.visibility);
+ this.motion.addEventListener('change', this.motionChanged);
this.disposeInput = this.wireInput();
this.resize();
}
private contextLost = (event: Event) => { event.preventDefault(); this.active = false; this.canvas.dispatchEvent(new CustomEvent('swarm-renderer-lost', { bubbles: true })); };
private visibility = () => { if (document.hidden) { cancelAnimationFrame(this.frameId); this.frameId = 0; } else this.request(); };
+ private motionChanged = () => {
+ if (this.motion.matches) {
+ this.flight = null;
+ cancelAnimationFrame(this.frameId); this.frameId = 0;
+ }
+ this.setNodes(this.nodes, this.selected);
+ };
private resize = () => {
const rect = this.canvas.parentElement!.getBoundingClientRect();
this.width = Math.max(1, rect.width); this.height = Math.max(1, rect.height);
if (this.gl) { this.gl.setSize(this.width, this.height, false); this.camera.aspect = this.width / this.height; this.camera.updateProjectionMatrix(); }
else { const ratio = Math.min(devicePixelRatio || 1, 1.25); this.canvas.width = this.width * ratio; this.canvas.height = this.height * ratio; this.ctx!.setTransform(ratio, 0, 0, ratio, 0, 0); }
+ this.viewport();
+ if (this.lastFit) this.frame(this.lastFit.point, this.lastFit.sourceId);
this.request();
};
+ /** Fit observations into the area left by visible instruments. The backdrop
+ * remains full bleed, while atlas/evidence/caption plates do not hide focus. */
+ private viewport(): { x: number; y: number; width: number; height: number } {
+ const stage = this.canvas.parentElement!.getBoundingClientRect();
+ let left = 20, right = this.width - 20, top = 24, bottom = this.height - 24;
+ const overlaps = (rect: DOMRect) => rect.width > 0 && rect.height > 0 && rect.top < stage.bottom && rect.bottom > stage.top;
+ const atlas = document.querySelector('.swarm-navigation')?.getBoundingClientRect();
+ if (atlas && overlaps(atlas) && atlas.left < stage.left + this.width / 2) left = Math.max(left, atlas.right - stage.left + 20);
+ const evidence = document.querySelector('.swarm-inspector')?.getBoundingClientRect();
+ if (evidence && overlaps(evidence) && evidence.right > stage.left + this.width / 2) right = Math.min(right, evidence.left - stage.left - 20);
+ const toolbar = document.querySelector('.space-toolbar')?.getBoundingClientRect();
+ if (toolbar && overlaps(toolbar)) top = Math.max(top, toolbar.bottom - stage.top + 20);
+ const caption = document.querySelector('.space-caption')?.getBoundingClientRect();
+ if (caption && overlaps(caption)) bottom = Math.min(bottom, caption.top - stage.top - 20);
+ const width = Math.max(120, right - left), height = Math.max(120, bottom - top);
+ const x = left + width / 2, y = top + height / 2;
+ if (this.gl) this.camera.setViewOffset(this.width, this.height, this.width / 2 - x, this.height / 2 - y, this.width, this.height);
+ return { x, y, width, height };
+ }
setNodes(nodes: Node[], selected: string | null): void {
this.nodes = nodes; this.selected = selected;
- this.active = !matchMedia('(prefers-reduced-motion: reduce)').matches && nodes.some(node => node.activity === 'recent');
+ this.viewport();
+ // The plan view has no animated shader; it only needs observation/input redraws.
+ this.active = !!this.gl && !this.motion.matches && nodes.some(node => node.activity === 'recent');
if (this.gl) {
- const pos: number[] = [], colors: number[] = [], sizes: number[] = [], active: number[] = [], fill: number[] = [], linkPos: number[] = [], linkColors: number[] = [];
+ const pos: number[] = [], colors: number[] = [], sizes: number[] = [], active: number[] = [], fill: number[] = [], plate: number[] = [], chosen: number[] = [];
+ const linkPos: number[] = [], linkColors: number[] = [];
const byKey = new Map(nodes.map(node => [node.key, node]));
- // Static engraved source plates share the delegation line batch. They
- // are reference rings, not edges or activity. No cross-authority lines.
+ // Static engraved source plates share the delegation line batch. They are
+ // decorative reference rings, not edges, authority or activity, and no
+ // cross-authority line is ever emitted.
for (const source of nodes.filter(node => !node.cell)) {
- const color = new Color(sourceHue(source.source.id));
- for (const radius of [26, 49, 72]) for (let i = 0; i < 64; i++) for (const t of [i / 64, (i + 1) / 64]) {
- const angle = t * Math.PI * 2;
- linkPos.push(source.point.x + Math.cos(angle) * radius, 0, source.point.z + Math.sin(angle) * radius);
- linkColors.push(color.r * .07, color.g * .07, color.b * .07);
+ const color = new Color(sourceHue(source.source.id)), base = source.point.y;
+ const faint = source.source.status !== 'observed' ? .035 : .085;
+ for (const [ri, radius] of [30, 56, 84].entries()) {
+ const segments = 80, weight = faint * (ri === 1 ? 1.35 : 1);
+ for (let i = 0; i < segments; i++) for (const t of [i / segments, (i + 1) / segments]) {
+ const angle = t * Math.PI * 2;
+ linkPos.push(source.point.x + Math.cos(angle) * radius, base, source.point.z + Math.sin(angle) * radius);
+ linkColors.push(color.r * weight, color.g * weight, color.b * weight);
+ }
+ }
+ // Graduated ticks on the outer ring read as an instrument bezel.
+ for (let i = 0; i < 24; i++) {
+ const angle = i / 24 * Math.PI * 2, inner = i % 6 === 0 ? 78 : 81.5, weight = faint * 1.9;
+ linkPos.push(source.point.x + Math.cos(angle) * inner, base, source.point.z + Math.sin(angle) * inner);
+ linkColors.push(color.r * weight, color.g * weight, color.b * weight);
+ linkPos.push(source.point.x + Math.cos(angle) * 88, base, source.point.z + Math.sin(angle) * 88);
+ linkColors.push(color.r * weight * .25, color.g * weight * .25, color.b * weight * .25);
}
}
for (const node of nodes) {
pos.push(node.point.x, node.point.y, node.point.z);
- const color = new Color(node.key === selected ? '#e1fff5' : tint(node)); colors.push(color.r, color.g, color.b);
- sizes.push(!node.cell ? 18 : node.key === selected ? 17 : node.cell.role === 'coordinator' ? 13 : 9); active.push(node.activity === 'recent' ? 1 : 0);
+ const own = new Color(tint(node)), color = new Color(node.key === selected ? '#e6fff8' : tint(node));
+ colors.push(color.r, color.g, color.b);
+ sizes.push(!node.cell ? 21 : node.key === selected ? 17 : node.cell.role === 'coordinator' ? 13.5 : node.cell.role === 'verifier' ? 10 : 8.5);
+ active.push(!this.motion.matches && node.activity === 'recent' ? 1 : 0);
fill.push(node.source.status === 'observed' && node.cell?.status !== 'retired' && node.cell?.status !== 'reserved' && node.activity !== 'uncertain' ? 1 : 0);
+ plate.push(node.cell ? 0 : 1); chosen.push(node.key === selected ? 1 : 0);
const parent = node.parent ? byKey.get(node.parent) : undefined;
if (parent?.cell && node.cell?.parentId) {
- // Short curved delegation links, one shared line batch.
- for (let i = 0; i < 8; i++) {
- for (const t of [i / 8, (i + 1) / 8]) {
- linkPos.push(parent.point.x + (node.point.x - parent.point.x) * t, parent.point.y + (node.point.y - parent.point.y) * t + Math.sin(t * Math.PI) * 3, parent.point.z + (node.point.z - parent.point.z) * t);
- const intensity = node.key === selected || parent.key === selected ? .65 : .2; linkColors.push(color.r * intensity, color.g * intensity, color.b * intensity);
- }
+ // Lifted delegation ribbons fading parent to child, one line batch.
+ const from = new Color(tint(parent)), lit = node.key === selected || parent.key === selected;
+ for (let i = 0; i < 10; i++) for (const t of [i / 10, (i + 1) / 10]) {
+ const arc = Math.sin(t * Math.PI), intensity = (lit ? .72 : .19) * (.45 + .55 * arc);
+ linkPos.push(parent.point.x + (node.point.x - parent.point.x) * t, parent.point.y + (node.point.y - parent.point.y) * t + arc * 3.4, parent.point.z + (node.point.z - parent.point.z) * t);
+ linkColors.push((from.r + (own.r - from.r) * t) * intensity, (from.g + (own.g - from.g) * t) * intensity, (from.b + (own.b - from.b) * t) * intensity);
}
}
}
- const geometry = new BufferGeometry(); geometry.setAttribute('position', new Float32BufferAttribute(pos, 3)); geometry.setAttribute('color', new Float32BufferAttribute(colors, 3)); geometry.setAttribute('aSize', new Float32BufferAttribute(sizes, 1)); geometry.setAttribute('aActive', new Float32BufferAttribute(active, 1)); geometry.setAttribute('aFill', new Float32BufferAttribute(fill, 1));
+ const geometry = new BufferGeometry();
+ geometry.setAttribute('position', new Float32BufferAttribute(pos, 3)); geometry.setAttribute('color', new Float32BufferAttribute(colors, 3));
+ geometry.setAttribute('aSize', new Float32BufferAttribute(sizes, 1)); geometry.setAttribute('aActive', new Float32BufferAttribute(active, 1));
+ geometry.setAttribute('aFill', new Float32BufferAttribute(fill, 1)); geometry.setAttribute('aPlate', new Float32BufferAttribute(plate, 1));
+ geometry.setAttribute('aSelected', new Float32BufferAttribute(chosen, 1));
if (this.points) { this.points.geometry.dispose(); this.points.geometry = geometry; }
else { this.points = new Points(geometry, new ShaderMaterial({ vertexShader, fragmentShader, vertexColors: true, transparent: true, depthWrite: false, uniforms: { uTime: { value: 0 } } })); this.scene.add(this.points); }
const links = new BufferGeometry(); links.setAttribute('position', new Float32BufferAttribute(linkPos, 3)); links.setAttribute('color', new Float32BufferAttribute(linkColors, 3));
if (this.lines) { this.lines.geometry.dispose(); this.lines.geometry = links; }
- else { this.lines = new LineSegments(links, new LineBasicMaterial({ vertexColors: true, transparent: true, opacity: .9, depthWrite: false })); this.scene.add(this.lines); }
+ else { this.lines = new LineSegments(links, new LineBasicMaterial({ vertexColors: true, transparent: true, opacity: .92, depthWrite: false })); this.scene.add(this.lines); }
}
- const priority = nodes.filter(node => node.key === selected).concat(nodes.filter(node => !node.cell || node.cell.role === 'coordinator'), nodes.filter(node => node.cell && node.cell.role !== 'coordinator').sort((a, b) => a.cell!.depth - b.cell!.depth));
+ const priority = nodes.filter(node => node.key === selected).concat(nodes.filter(node => !node.cell || node.cell.role === 'coordinator'), nodes.filter(node => node.cell && node.activity === 'recent'), nodes.filter(node => node.cell && node.cell.role !== 'coordinator').sort((a, b) => a.cell!.depth - b.cell!.depth));
const labelled = [...new Map(priority.map(node => [node.key, node])).values()].slice(0, 24);
this.labels.replaceChildren(); this.labelElements = [];
- for (const node of labelled) { const span = document.createElement('span'); span.className = `swarm-label${!node.cell ? ' source-label' : ''}${node.key === selected ? ' selected' : ''}`; span.textContent = node.label.length > 24 ? `${node.label.slice(0, 22)}…` : node.label; span.title = `${node.source.factoryId} / ${node.label}`; span.dataset.key = node.key; this.labels.append(span); this.labelElements.push(span); }
+ for (const node of labelled) {
+ const span = document.createElement('span');
+ span.className = `swarm-label${!node.cell ? ' source-label' : ''}${node.key === selected ? ' selected' : ''}${node.activity === 'recent' ? ' active' : ''}`;
+ span.textContent = node.label.length > 24 ? `${node.label.slice(0, 22)}…` : node.label;
+ span.title = `${node.source.factoryId} / ${node.label}`;
+ span.dataset.key = node.key;
+ span.style.setProperty('--accent', sourceHue(node.source.id));
+ if (!node.cell) { const kicker = document.createElement('i'); kicker.textContent = node.source.status === 'observed' ? 'authority' : node.source.status; span.append(kicker); }
+ this.labels.append(span); this.labelElements.push(span);
+ }
+ // Measure once per observation, rather than reading layout on every frame.
+ this.labelSizes = new Map(this.labelElements.map(label => {
+ const rect = label.getBoundingClientRect();
+ return [label, { width: rect.width, height: rect.height }];
+ }));
this.request();
}
frame(point?: Point, sourceId?: string): void {
+ this.lastFit = { point, sourceId };
+ const viewport = this.viewport();
const nodes = sourceId ? this.nodes.filter(node => node.source.id === sourceId) : this.nodes;
const center = point ?? nodes.reduce((sum, node) => ({ x: sum.x + node.point.x / Math.max(1, nodes.length), y: sum.y + node.point.y / Math.max(1, nodes.length), z: sum.z + node.point.z / Math.max(1, nodes.length) }), { x: 0, y: 0, z: 0 });
- const radius = point ? 35 : Math.max(sourceId ? 78 : 40, ...nodes.map(node => Math.hypot(node.point.x - center.x, node.point.z - center.z)));
+ const radius = point ? 35 : Math.max(sourceId ? 92 : 48, ...nodes.map(node => Math.hypot(node.point.x - center.x, node.point.z - center.z)));
if (this.gl) {
- const distance = radius * (this.width < this.height ? 3.8 : sourceId ? 2.5 : 1.8);
- const target = new Vector3(center.x, center.y, center.z), position = new Vector3(center.x + distance * .16, center.y + distance * .65, center.z + distance);
- if (matchMedia('(prefers-reduced-motion: reduce)').matches) { this.controls!.target.copy(target); this.camera.position.copy(position); this.controls!.update(); }
+ const distance = radius * Math.max((point ? 2.7 : sourceId ? 1.45 : 1.1) * this.height / viewport.height, 2.4 * this.height / viewport.width);
+ const target = new Vector3(center.x, center.y + (point ? 0 : 6), center.z);
+ const position = new Vector3(center.x + distance * .34, center.y + distance * (point ? .42 : .52), center.z + distance * .86);
+ if (this.motion.matches) { this.controls!.target.copy(target); this.camera.position.copy(position); this.controls!.update(); }
else this.flight = { start: performance.now(), fromPosition: this.camera.position.clone(), fromTarget: this.controls!.target.clone(), toPosition: position, toTarget: target };
- } else { this.zoom = Math.min(this.width, this.height) / (radius * 2.8); this.pan = { x: -center.x * this.zoom, y: -center.z * this.zoom }; }
+ } else { this.zoom = Math.min(viewport.width, viewport.height) / (radius * 2.8); this.pan = { x: viewport.x - this.width / 2 - center.x * this.zoom, y: viewport.y - this.height / 2 - center.z * this.zoom }; }
this.request();
}
request = (): void => { if (!this.disposed && !document.hidden && !this.frameId) this.frameId = requestAnimationFrame(this.draw); };
@@ -147,43 +251,70 @@ export class SwarmRenderer {
private draw = (time: number): void => {
this.frameId = 0;
if (this.disposed || document.hidden) return;
- if ((this.active || this.flight) && time - this.lastFrame < 33) { this.request(); return; }
+ if ((this.active || this.flight) && time - this.lastFrame < 1000 / 30) { this.request(); return; }
this.lastFrame = time; const start = performance.now(); this.projected.clear();
if (this.flight) {
- const progress = Math.min(1, (time - this.flight.start) / 620), eased = 1 - (1 - progress) ** 3;
+ const progress = Math.min(1, (time - this.flight.start) / 760), eased = 1 - (1 - progress) ** 3;
this.camera.position.lerpVectors(this.flight.fromPosition, this.flight.toPosition, eased); this.controls!.target.lerpVectors(this.flight.fromTarget, this.flight.toTarget, eased); this.controls!.update();
if (progress >= 1) this.flight = null;
}
for (const node of this.nodes) this.projected.set(node.key, this.project(node.point));
if (this.gl) { if (this.points) this.points.material.uniforms.uTime!.value = time / 1000; this.gl.render(this.scene, this.camera); }
- else {
- const context = this.ctx!; context.clearRect(0, 0, this.width, this.height);
- for (const source of this.nodes.filter(node => !node.cell)) { const point = this.projected.get(source.key)!; context.strokeStyle = `${sourceHue(source.source.id)}18`; context.lineWidth = 1; for (const radius of [26, 49, 72]) { context.beginPath(); context.arc(point.x, point.y, radius * this.zoom, 0, Math.PI * 2); context.stroke(); } }
- for (const node of this.nodes) {
- const parent = node.parent && this.projected.get(node.parent), point = this.projected.get(node.key)!;
- if (!parent || !node.cell?.parentId) continue;
- context.beginPath(); context.moveTo(parent.x, parent.y); context.quadraticCurveTo((parent.x + point.x) / 2 + 7, (parent.y + point.y) / 2 - 12, point.x, point.y); context.strokeStyle = node.key === this.selected ? '#7cdcbda0' : '#739c9e36'; context.lineWidth = 1; context.stroke();
- }
- for (const node of this.nodes) {
- const point = this.projected.get(node.key)!, radius = !node.cell ? 13 : node.key === this.selected ? 10 : node.cell.role === 'coordinator' ? 9 : 5;
- const gradient = context.createRadialGradient(point.x, point.y, 0, point.x, point.y, radius * 3); gradient.addColorStop(0, `${tint(node)}d0`); gradient.addColorStop(.3, `${tint(node)}60`); gradient.addColorStop(1, `${tint(node)}00`);
- context.fillStyle = gradient; context.beginPath(); context.arc(point.x, point.y, radius * 3, 0, Math.PI * 2); context.fill();
- context.strokeStyle = tint(node); context.beginPath(); context.arc(point.x, point.y, radius * .75, 0, Math.PI * 2); context.stroke();
- if (node.source.status === 'observed' && node.cell?.status !== 'retired' && node.activity !== 'uncertain') { context.fillStyle = tint(node); context.beginPath(); context.arc(point.x, point.y, radius * .35, 0, Math.PI * 2); context.fill(); }
- if (node.key === this.selected) { context.strokeStyle = '#c6ffec'; context.beginPath(); context.arc(point.x, point.y, radius + 4, 0, Math.PI * 2); context.stroke(); }
- }
- }
- const occupied = new Set();
+ else this.paint2d();
+ const occupied: Array<{ left: number; right: number; top: number; bottom: number }> = [];
for (const label of this.labelElements) {
const point = this.projected.get(label.dataset.key!); label.hidden = !point?.size || point.x < 0 || point.x > this.width || point.y < 0 || point.y > this.height;
if (point && point.y > this.height - 145) label.hidden = true;
- if (point && !label.hidden) { const slot = `${Math.floor(point.x / 110)}:${Math.floor(point.y / 26)}`; if (occupied.has(slot)) label.hidden = true; else occupied.add(slot); }
- if (point) label.style.transform = `translate(${point.x}px,${point.y + 17}px) translateX(-50%)`;
+ if (point && !label.hidden) {
+ const size = this.labelSizes.get(label)!;
+ const rect = { left: point.x - size.width / 2 - 4, right: point.x + size.width / 2 + 4, top: point.y + 16, bottom: point.y + 20 + size.height };
+ if (occupied.some(other => rect.left < other.right && rect.right > other.left && rect.top < other.bottom && rect.bottom > other.top)) label.hidden = true;
+ else occupied.push(rect);
+ }
+ if (point) label.style.transform = `translate(${point.x}px,${point.y + 18}px) translateX(-50%)`;
}
this.submissions++; this.lastSubmissionMs = performance.now() - start;
this.onStats({ mode: this.mode, visible: this.nodes.length, labels: this.labelElements.length, draws: this.gl?.info.render.calls ?? 0, submissions: this.submissions, cpuMs: this.lastSubmissionMs });
if (this.active || this.flight) this.request();
};
+ /** 2D fallback: the same index, the same deterministic colours, plan view. */
+ private paint2d(): void {
+ const context = this.ctx!;
+ context.clearRect(0, 0, this.width, this.height);
+ for (const source of this.nodes.filter(node => !node.cell)) {
+ const point = this.projected.get(source.key)!, hue = sourceHue(source.source.id);
+ context.lineWidth = 1;
+ for (const radius of [30, 56, 84]) { context.strokeStyle = `${hue}${radius === 56 ? '22' : '16'}`; context.beginPath(); context.arc(point.x, point.y, radius * this.zoom, 0, Math.PI * 2); context.stroke(); }
+ context.strokeStyle = `${hue}2c`;
+ for (let i = 0; i < 24; i++) {
+ const angle = i / 24 * Math.PI * 2, inner = (i % 6 === 0 ? 78 : 81.5) * this.zoom, outer = 88 * this.zoom;
+ context.beginPath(); context.moveTo(point.x + Math.cos(angle) * inner, point.y + Math.sin(angle) * inner); context.lineTo(point.x + Math.cos(angle) * outer, point.y + Math.sin(angle) * outer); context.stroke();
+ }
+ }
+ for (const node of this.nodes) {
+ const parent = node.parent && this.projected.get(node.parent), point = this.projected.get(node.key)!;
+ if (!parent || !node.cell?.parentId) continue;
+ const lit = node.key === this.selected || node.parent === this.selected;
+ context.beginPath(); context.moveTo(parent.x, parent.y);
+ context.quadraticCurveTo((parent.x + point.x) / 2 + 7, (parent.y + point.y) / 2 - 13, point.x, point.y);
+ context.strokeStyle = lit ? `${tint(node)}c0` : `${tint(node)}30`; context.lineWidth = lit ? 1.4 : 1; context.stroke();
+ }
+ for (const node of this.nodes) {
+ const point = this.projected.get(node.key)!, hue = tint(node);
+ const radius = !node.cell ? 14 : node.key === this.selected ? 10 : node.cell.role === 'coordinator' ? 9 : node.cell.role === 'verifier' ? 6 : 5;
+ const gradient = context.createRadialGradient(point.x, point.y, 0, point.x, point.y, radius * 3.2);
+ gradient.addColorStop(0, `${hue}cc`); gradient.addColorStop(.32, `${hue}55`); gradient.addColorStop(1, `${hue}00`);
+ context.fillStyle = gradient; context.beginPath(); context.arc(point.x, point.y, radius * 3.2, 0, Math.PI * 2); context.fill();
+ context.strokeStyle = hue; context.lineWidth = node.cell ? 1 : 1.4; context.beginPath(); context.arc(point.x, point.y, radius * .78, 0, Math.PI * 2); context.stroke();
+ if (!node.cell) { context.strokeStyle = `${hue}66`; context.beginPath(); context.arc(point.x, point.y, radius * 1.5, 0, Math.PI * 2); context.stroke(); }
+ if (node.source.status === 'observed' && node.cell?.status !== 'retired' && node.cell?.status !== 'reserved' && node.activity !== 'uncertain') { context.fillStyle = hue; context.beginPath(); context.arc(point.x, point.y, radius * .34, 0, Math.PI * 2); context.fill(); }
+ if (node.key === this.selected) {
+ context.strokeStyle = '#d2fff0'; context.lineWidth = 1.2;
+ context.beginPath(); context.arc(point.x, point.y, radius + 5, 0, Math.PI * 2); context.stroke();
+ context.beginPath(); context.moveTo(point.x - radius - 11, point.y); context.lineTo(point.x - radius - 3, point.y); context.moveTo(point.x + radius + 3, point.y); context.lineTo(point.x + radius + 11, point.y); context.stroke();
+ }
+ }
+ }
private wireInput(): () => void {
const down = (event: PointerEvent) => { this.flight = null; this.drag = { x: event.clientX, y: event.clientY, startX: event.clientX, startY: event.clientY, moved: false }; if (!this.gl) this.canvas.setPointerCapture(event.pointerId); };
const move = (event: PointerEvent) => {
@@ -207,7 +338,8 @@ export class SwarmRenderer {
}
dispose(): void {
this.disposed = true; cancelAnimationFrame(this.frameId); this.observer.disconnect(); this.disposeInput(); document.removeEventListener('visibilitychange', this.visibility);
+ this.motion.removeEventListener('change', this.motionChanged);
this.canvas.removeEventListener('webglcontextlost', this.contextLost); this.controls?.removeEventListener('change', this.request); this.controls?.dispose();
- this.points?.geometry.dispose(); this.points?.material.dispose(); this.lines?.geometry.dispose(); this.lines?.material.dispose(); this.gl?.dispose(); this.labels.replaceChildren();
+ this.points?.geometry.dispose(); this.points?.material.dispose(); this.lines?.geometry.dispose(); this.lines?.material.dispose(); this.gl?.dispose(); this.labels.replaceChildren(); this.labelSizes.clear();
}
}
diff --git a/src/swarm/transport.ts b/src/swarm/transport.ts
new file mode 100644
index 0000000..b984bbb
--- /dev/null
+++ b/src/swarm/transport.ts
@@ -0,0 +1,15 @@
+/** Bound streamed bytes before JSON decoding, including multibyte UTF-8. */
+export async function readSwarmJson(response: Response, maxBytes = 32 * 1024 * 1024): Promise {
+ if (!response.body) throw new Error('Swarm observation has no body');
+ const reader = response.body.getReader(), decoder = new TextDecoder('utf-8', { fatal: true });
+ const chunks: string[] = []; let bytes = 0;
+ try {
+ while (true) {
+ const { done, value } = await reader.read(); if (done) break;
+ bytes += value.byteLength;
+ if (bytes > maxBytes) throw new Error('Swarm observation exceeds its bound');
+ chunks.push(decoder.decode(value, { stream: true }));
+ }
+ return JSON.parse(chunks.join('') + decoder.decode());
+ } finally { await reader.cancel().catch(() => undefined); }
+}
diff --git a/swarm.html b/swarm.html
index 57e691a..b7be622 100644
--- a/swarm.html
+++ b/swarm.html
@@ -1,15 +1,59 @@
-Brain · swarm
+Brain · swarm
-
+
+ brain observatory
+ Atlas
+ Find /
+ Waiting for an authorized observation
+ READ ONLY
+
-
-
A wider mind.
Orbit the recorded world. Enter a source. Follow its work.
LIVE TRACKING
Rendering details
+
+ EXPLORE THE SWARM
+ Find a cell, task or effect
+
+ REGISTERED AUTHORITIES
+
+ DELEGATION
+
+
+ Registration groups sources. Lines within a source show recorded delegation. Reference rings are decorative; they are not authority edges.
+
+
+
+
+
+
+
+
+
RECORDED FACTORY SPACE
+
A wider mind.
+
+
Orbit the recorded world. Enter a source. Follow its work.
+
+ unexpired lease · recent heartbeat
+ uncertain evidence
+ retired · reserved · stale
+ authority plate
+
+
+
+ LIVE TRACKING
+ Rendering details
+
-
FACTORY owns execution and recovery. Recorded activity does not establish host health. Worker quiescence: unverified.
+
FACTORY owns execution and recovery. Recorded activity does not establish host health. Worker quiescence: unverified .
diff --git a/test/swarm.test.mjs b/test/swarm.test.mjs
index c223066..724ea32 100644
--- a/test/swarm.test.mjs
+++ b/test/swarm.test.mjs
@@ -5,12 +5,34 @@ import { performance } from 'node:perf_hooks';
import ts from 'typescript';
const generated = new URL('../.vite/swarm-tests/', import.meta.url);
await mkdir(generated, { recursive: true });
-for (const name of ['model', 'preview']) {
+for (const name of ['model', 'preview', 'transport']) {
const source = await readFile(new URL(`../src/swarm/${name}.ts`, import.meta.url), 'utf8');
await writeFile(new URL(`${name}.mjs`, generated), ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText);
}
-const { parseSwarm, SwarmIndex, nodeKey, activities } = await import(new URL('model.mjs', generated));
+const { parseSwarm, SwarmIndex, nodeKey, activities, selectionFragment, selectionFromFragment } = await import(new URL('model.mjs', generated));
const { swarmPreview } = await import(new URL('preview.mjs', generated));
+const { readSwarmJson } = await import(new URL('transport.mjs', generated));
+
+test('deep links bind a reused registry alias to its exact factory and reject ambiguous/missing authority', () => {
+ const swarm = swarmPreview(), source = swarm.sources[0], selected = { sourceId: source.id, kind: 'cell', id: 'root' };
+ const fragment = selectionFragment(source, selected);
+ assert.deepEqual(selectionFromFragment(swarm, fragment), selected);
+ assert.equal(selectionFromFragment(swarm, fragment + '&factory=another'), null);
+ assert.equal(selectionFromFragment(swarm, `source=${source.id}&kind=cell&id=root`), null);
+ source.factoryId = 'replacement-authority'; source.snapshot.factoryId = source.factoryId;
+ assert.equal(selectionFromFragment(swarm, fragment), null);
+ assert.deepEqual(selectionFromFragment(swarm, selectionFragment(source, selected)), selected);
+});
+
+test('consumer bounds streamed bytes before parsing and handles split UTF-8 without corruption', async () => {
+ const value = new TextEncoder().encode('{"label":"ø"}');
+ const response = new Response(new ReadableStream({ start(controller) { controller.enqueue(value.slice(0, 11)); controller.enqueue(value.slice(11)); controller.close(); } }));
+ assert.deepEqual(await readSwarmJson(response, value.length), { label: 'ø' });
+ let cancelled = false;
+ const oversized = new Response(new ReadableStream({ pull(controller) { controller.enqueue(new Uint8Array(64)); }, cancel() { cancelled = true; } }));
+ await assert.rejects(readSwarmJson(oversized, 32), /exceeds its bound/);
+ assert.equal(cancelled, true);
+});
test('same bare root/task/effect identities remain distinct across authorities and registration is separate', () => {
const swarm = parseSwarm(swarmPreview());
@@ -60,6 +82,20 @@ test('spatial positions survive heartbeat and recorded-status updates', () => {
const after = new SwarmIndex(swarm);
for (const [key, node] of before.nodes) assert.deepEqual(after.nodes.get(key).point, node.point);
});
+
+test('large source overviews use recorded roots even when their ID is not root, and source focus shows its delegation context', () => {
+ const swarm = swarmPreview(3000), source = swarm.sources[0]; swarm.sources = [source];
+ source.snapshot.snapshot.cells[0].id = 'coordinator-alpha';
+ for (const cell of source.snapshot.snapshot.cells) if (cell.parentId === 'root') cell.parentId = 'coordinator-alpha';
+ for (const task of source.snapshot.snapshot.tasks) if (task.owner === 'root') task.owner = 'coordinator-alpha';
+ const index = new SwarmIndex(parseSwarm(swarm)), root = nodeKey(source, 'cell', 'coordinator-alpha');
+ assert.ok(index.visible(null).some(node => node.key === root));
+ assert.ok(index.visible(null).some(node => node.cell?.id === 'cell-1'));
+ const focused = index.visible(nodeKey(source, 'source', source.id));
+ assert.ok(focused.some(node => node.key === root));
+ assert.ok(focused.some(node => node.cell?.parentId === 'coordinator-alpha'));
+ assert.ok(focused.length <= 600);
+});
test('observation freshness never renews worker evidence, and terminal states do not activate it', () => {
const swarm = parseSwarm(swarmPreview()), source = swarm.sources[0], now = Date.parse(source.snapshot.observedAt);
assert.equal(activities(source, now).get('cell-1'), 'recent');
From 7390063e63f9fe161cfcd451f6e17e860362df6f Mon Sep 17 00:00:00 2001
From: flujo-app <300233937+flujo-app@users.noreply.github.com>
Date: Sat, 3 Oct 2026 16:02:45 -0500
Subject: [PATCH 3/9] Preserve unknown effect uncertainty after task completion
---
docs/SWARM.md | 4 +++-
src/swarm/model.ts | 3 ++-
test/swarm.test.mjs | 14 ++++++++++++++
3 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/docs/SWARM.md b/docs/SWARM.md
index 05582b8..bc3aed0 100644
--- a/docs/SWARM.md
+++ b/docs/SWARM.md
@@ -68,7 +68,9 @@ changes and a trailing diagnostics update when on-demand rendering settles.
Fresh observations do not establish worker activity. A cell needs an active
controller, ready lifecycle, recent heartbeat, an unexpired running lease and
no owned unknown outcome. Completed/cancelled operations do not animate as
-running work. Pause, effect drain and worker quiescence remain separate facts.
+running work. Unknown external outcomes keep their cell's uncertainty marker
+after task completion or retirement, until an observed resolution replaces them.
+Pause, effect drain and worker quiescence remain separate facts.
The selected inspector preserves task, candidate and review digests and states
that quiescence is unverified.
diff --git a/src/swarm/model.ts b/src/swarm/model.ts
index 0639640..afc5ba1 100644
--- a/src/swarm/model.ts
+++ b/src/swarm/model.ts
@@ -99,7 +99,8 @@ export function activities(source: Source, now: number, sample = false): Map 15000 || observedAge < -5000)) || state.control.status !== 'active' || cell.status !== 'ready' || heartbeatAge < 0 || heartbeatAge > 60000 || expiry <= now || unknown.has(cell.id) ? 'uncertain' : 'recent');
+ // Task completion does not resolve an external operation's unknown outcome.
+ result.set(cell.id, unknown.has(cell.id) ? 'uncertain' : expiry === undefined ? 'idle' : source.status !== 'observed' || (!sample && (observedAge > 15000 || observedAge < -5000)) || state.control.status !== 'active' || cell.status !== 'ready' || heartbeatAge < 0 || heartbeatAge > 60000 || expiry <= now ? 'uncertain' : 'recent');
}
return result;
}
diff --git a/test/swarm.test.mjs b/test/swarm.test.mjs
index 724ea32..ccc779b 100644
--- a/test/swarm.test.mjs
+++ b/test/swarm.test.mjs
@@ -111,6 +111,20 @@ test('observation freshness never renews worker evidence, and terminal states do
source.status = 'observed'; source.snapshot.snapshot.control.status = 'paused';
assert.equal(activities(source, now).get('cell-1'), 'uncertain');
});
+test('an unknown external outcome remains uncertain after operational completion, retirement and pause', () => {
+ const source = parseSwarm(swarmPreview()).sources[0], state = source.snapshot.snapshot;
+ const now = Date.parse(source.snapshot.observedAt), task = state.tasks[0];
+ task.status = 'completed';
+ state.cells.find(cell => cell.id === task.owner).status = 'retired';
+ state.control.status = 'paused';
+ state.effects.push({ key: 'retire-unknown', kind: 'retire', state: 'unknown', owner: task.owner,
+ taskId: task.id, scope: 'cleanup', scopeId: task.owner, ownerEpoch: 1, controlEpoch: 1,
+ createdAt: source.snapshot.observedAt, updatedAt: source.snapshot.observedAt, requestDigest: 'b'.repeat(64) });
+ assert.equal(activities(source, now).get(task.owner), 'uncertain');
+ assert.equal(state.workerQuiescence, 'unverified');
+ state.effects[0].state = 'succeeded';
+ assert.equal(activities(source, now).get(task.owner), 'idle');
+});
test('consumer drops private extensions and fails closed on capabilities/identity/duplicate records', () => {
const input = swarmPreview(); input.token = 'private'; input.sources[0].origin = 'private'; input.sources[0].snapshot.snapshot.tasks[2].candidate.path = 'private'; input.sources[0].snapshot.snapshot.paidBudget = { token: 'private' };
assert.equal(JSON.stringify(parseSwarm(input)).includes('private'), false);
From a865f67a54c6c2d363f106c8b694801bd5022dc6 Mon Sep 17 00:00:00 2001
From: flujo-app <300233937+flujo-app@users.noreply.github.com>
Date: Sat, 3 Oct 2026 16:17:15 -0500
Subject: [PATCH 4/9] Distinguish recorded swarm states without animation
---
docs/SWARM.md | 25 +++++--
src/swarm/main.ts | 41 ++++++++---
src/swarm/observatory.css | 54 +++++++++++---
src/swarm/renderer.ts | 147 ++++++++++++++++++++++++++++++--------
swarm.html | 14 ++--
test/swarm.test.mjs | 34 ++++++++-
6 files changed, 254 insertions(+), 61 deletions(-)
diff --git a/docs/SWARM.md b/docs/SWARM.md
index bc3aed0..91c46d4 100644
--- a/docs/SWARM.md
+++ b/docs/SWARM.md
@@ -65,6 +65,13 @@ with Bash. The integration pass added camera fitting around visible instruments,
responsive reframing, measured label collision bounds, live reduced-motion
changes and a trailing diagnostics update when on-demand rendering settles.
+Static shapes also preserve status under reduced motion: authority plates use
+a hollow keystone, observed idle cells an open ring, recent qualifying leased
+work a bullseye, uncertain evidence an amber barred ring, and retained records
+a broken grey ring. Unconfirmed running-work evidence is distinct from an
+owned unknown external outcome in the inspector. Only an owned unknown outcome
+outranks retirement or source staleness; it remains unresolved by completion.
+
Fresh observations do not establish worker activity. A cell needs an active
controller, ready lifecycle, recent heartbeat, an unexpired running lease and
no owned unknown outcome. Completed/cancelled operations do not animate as
@@ -76,8 +83,8 @@ that quiescence is unverified.
## Development and verification
-Run `npm ci`, `npm run build` and `npm run test:swarm`. Tests use the actual model
-and preview modules and cover duplicate record IDs across authorities, complete
+Run `npm ci`, `npm run build` and `npm run test:swarm`. Tests use the actual model,
+marker, transport and preview modules and cover duplicate record IDs across authorities, complete
10,000-deep/wide navigation, stable layout, stale/lease/terminal activity gates,
positive allowlists and missing-source behavior. Copy the built `dist` artifact
to the consumer's ignored viewer directory for local integration; keep source
@@ -90,6 +97,14 @@ context loss. The world had 10,003 indexed nodes; its overview drew 18 nodes and
the selected neighborhood drew 10, with two GPU draw calls in 3D and no GL calls
in 2D. Desktop, 615-pixel and 390-pixel layouts were checked. Reduced motion
stopped an already-running renderer; 2D settled without an animation loop.
-These are fixture/navigation checks, not live FACTORY health evidence. The
-read-only observer was still offline at the last 2026-10-03 qualification check;
-its owner must restore and qualify it before a fresh live witness is recorded.
+These are fixture/navigation checks, not live FACTORY health evidence.
+
+After FACTORY's owner restored and qualified its observer, the actual Brain
+Online BFF passed a fresh read at 2026-10-03T21:06:54Z: loaded build
+`d9d87c6ebbd05b6c82bdbf0f7c537e76eb4b9a66`, paused admission at revision 92,
+five cells, three tasks and six effects. Staff reads returned 200, anonymous
+reads 401 and customer reads 404; snapshot/event responses were no-store and
+contained no server credential. The actual host component and upstream artifact
+also displayed these live records in a local qualification harness with a
+development staff principal and five-second refreshes. This does not qualify
+a production Supabase login or prove provider freshness or worker quiescence.
diff --git a/src/swarm/main.ts b/src/swarm/main.ts
index 6d5ee1f..b9feb87 100644
--- a/src/swarm/main.ts
+++ b/src/swarm/main.ts
@@ -2,7 +2,7 @@ import './swarm.css';
import './observatory.css';
import { activities, nodeKey, parseSwarm, selectionFragment, selectionFromFragment, SwarmIndex, type Node, type Selection, type Source, type Swarm } from './model';
import { readSwarmJson } from './transport';
-import { SwarmRenderer, sourceColor } from './renderer';
+import { activityEvidence, markerLegend, markerOf, markerTitle, SwarmRenderer, sourceColor, type Marker } from './renderer';
import { swarmPreview } from './preview';
const $ = (id: string): HTMLElement => document.getElementById(id)!;
@@ -10,6 +10,12 @@ const element = (tag: T, text?: string, c
const result = document.createElement(tag); if (text !== undefined) result.textContent = text; if (className) result.className = className; return result;
};
const button = (text: string, action: () => void, pressed = false): HTMLButtonElement => { const result = element('button', text); result.type = 'button'; result.setAttribute('aria-pressed', String(pressed)); result.onclick = action; return result; };
+const markerChip = (marker: Marker, className: string): HTMLSpanElement => {
+ // The rail, the legend and the field all read the same marker vocabulary.
+ const chip = element('span', undefined, `marker ${className}`);
+ chip.dataset.marker = marker; chip.title = `${markerTitle[marker]} — ${markerLegend[marker]}`;
+ return chip;
+};
const timestamp = (value: string): string => new Intl.DateTimeFormat(undefined, { timeZone: 'America/Bogota', dateStyle: 'medium', timeStyle: 'medium' }).format(new Date(value));
const taskLabel = (status: string) => status === 'completed' ? 'operation completed' : status;
const empty = (): Swarm => ({ schemaVersion: 1, scope: 'operator-source-registry', observedAt: new Date().toISOString(), commands: false, sources: [] });
@@ -83,9 +89,11 @@ function paintNavigation(): void {
// Exactly the renderer's deterministic plate colour, never a list position.
item.style.setProperty('--accent', sourceColor(source.id));
item.dataset.status = status;
- item.replaceChildren(element('span', undefined, 'source-dot'), element('strong', source.label, 'source-name'),
- element('small', `${status} · ${cells.length} cells${evidenced ? ` · ${evidenced} evidenced` : ''}`));
- item.title = source.factoryId; sourceList.append(item);
+ const marker: Marker = status === 'observed' ? 'plate' : 'quiet';
+ item.dataset.marker = marker;
+ item.replaceChildren(markerChip(marker, 'source-dot'), element('strong', source.label, 'source-name'),
+ element('small', `${status} · ${cells.length} recorded cells${evidenced ? ` · ${evidenced} with a running lease` : ''}`));
+ item.title = `${source.factoryId} · ${markerLegend[marker]}`; sourceList.append(item);
}
const focus = focusNode(), children = focus ? index.children.get(focus.key) ?? [] : [];
$('branch-heading').textContent = focus ? `DELEGATION · ${focus.label} · ${children.length} children` : 'DELEGATION · select a source';
@@ -93,10 +101,13 @@ function paintNavigation(): void {
for (const key of children.slice(page * 128, (page + 1) * 128)) {
const node = index.nodes.get(key)!;
const item = button(node.label, () => select(node.selection), key === focus?.key);
+ const marker = markerOf(node);
item.className = `branch-record activity-${node.activity}`;
+ item.dataset.marker = marker;
item.style.setProperty('--accent', sourceColor(node.source.id));
- item.replaceChildren(element('span', undefined, 'branch-dot'), element('strong', node.label, 'branch-name'),
- element('small', `${node.cell?.role ?? 'source'} · ${node.cell?.status ?? sourceStatus(node.source)} · ${index.children.get(key)?.length ?? 0} children`));
+ item.title = `${node.source.factoryId} / ${node.label} · ${markerLegend[marker]}`;
+ item.replaceChildren(markerChip(marker, 'branch-dot'), element('strong', node.label, 'branch-name'),
+ element('small', `${node.cell?.role ?? 'source'} · ${node.cell?.status ?? sourceStatus(node.source)} · ${markerTitle[marker].toLowerCase()} · ${index.children.get(key)?.length ?? 0} children`));
$('branch-list').append(item);
}
if (children.length > 128) {
@@ -127,7 +138,20 @@ function evidenceList(container: HTMLElement, pairs: Array<[string, string]>): v
function paintInspector(): void {
const container = $('swarm-inspect'); container.replaceChildren();
container.style.removeProperty('--accent');
- if (!selection) { container.append(element('p', 'THE OBSERVATORY', 'evidence-kicker'), element('h2', 'A constellation of work.'), element('p', 'Move from registered authorities to their coordinators and delegated cells. Search reaches every recorded cell, task and external effect.'), element('p', 'This view follows observations. It cannot start, stop or resume workers.'), element('p', 'Select a source to inspect its provenance and recorded work.')); return; }
+ if (!selection) {
+ container.append(element('p', 'THE OBSERVATORY', 'evidence-kicker'), element('h2', 'A constellation of work.'),
+ element('p', 'Move from registered authorities to their coordinators and delegated cells. Search reaches every recorded cell, task and external effect.'),
+ element('p', 'This view follows observations. It cannot start, stop or resume workers.'),
+ element('h3', 'Reading the markers'));
+ const guide = element('ul', undefined, 'marker-guide');
+ for (const marker of ['plate', 'idle', 'recent', 'uncertain', 'quiet'] as Marker[]) {
+ const row = element('li');
+ row.append(markerChip(marker, 'guide-dot'), element('strong', markerTitle[marker]), element('small', markerLegend[marker]));
+ guide.append(row);
+ }
+ container.append(guide, element('p', 'Markers describe recorded observations only. They never establish host health, physical worker activity or quiescence, and shape rather than animation carries each state.', 'evidence-note'));
+ return;
+ }
const source = index.sources.get(selection.sourceId)!;
container.style.setProperty('--accent', sourceColor(source.id));
container.append(element('p', selection.kind === 'source' ? 'REGISTERED AUTHORITY' : `RECORDED ${selection.kind.toUpperCase()}`, 'evidence-kicker'),
@@ -146,7 +170,8 @@ function paintInspector(): void {
} else if (selection.kind === 'cell') {
const cell = state.cells.find(cell => cell.id === selection!.id)!;
container.append(element('p', cell.purpose));
- evidenceList(container, [['Role / lifecycle', `${cell.role} / ${cell.status}`], ['Recorded parent', cell.parentId ?? 'Local root'], ['Heartbeat', timestamp(cell.heartbeat)], ['Activity evidence', activities(source, Date.now(), swarm.sample).get(cell.id) ?? 'idle'], ['Logical allocation', `$${(cell.allocationCents / 100).toFixed(2)} · logical-allocation`], ['Worker quiescence', 'unverified']]);
+ const node = index.nodeFor(selection)!;
+ evidenceList(container, [['Role / lifecycle', `${cell.role} / ${cell.status}`], ['Recorded parent', cell.parentId ?? 'Local root'], ['Heartbeat', timestamp(cell.heartbeat)], ['Activity evidence', activityEvidence({ ...node, activity: activities(source, Date.now(), swarm.sample).get(cell.id) ?? 'idle' })], ['Logical allocation', `$${(cell.allocationCents / 100).toFixed(2)} · logical-allocation`], ['Worker quiescence', 'unverified']]);
container.append(element('h3', 'Owned tasks')); appendTasks(container, source, state.tasks.filter(task => task.owner === cell.id));
container.append(element('h3', 'Owned external effects')); appendEffects(container, source, state.effects.filter(effect => effect.owner === cell.id));
if (cell.parentId && !state.cells.some(parent => parent.id === cell.parentId)) container.append(element('p', 'The parent is absent from this observation. Its position does not prove a delegation link.', 'evidence-note'));
diff --git a/src/swarm/observatory.css b/src/swarm/observatory.css
index 4a6b438..7faaa18 100644
--- a/src/swarm/observatory.css
+++ b/src/swarm/observatory.css
@@ -86,10 +86,10 @@ input[type=search]::placeholder{color:#4e5b6d}
#source-list button::before{content:'';position:absolute;left:0;top:0;bottom:0;width:2px;background:var(--accent);opacity:.55}
#source-list button:hover::before,#source-list button[aria-pressed=true]::before{opacity:1;box-shadow:0 0 12px var(--accent)}
#source-list button[aria-pressed=true]{background:linear-gradient(90deg,rgba(127,209,222,.1),transparent 72%)}
-.source-dot{grid-row:1/3;width:9px;height:9px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 1px #04060b,0 0 10px var(--accent)}
+.source-dot{grid-row:1/3}
.source-name{font:400 16px/1.25 var(--serif);letter-spacing:-.015em;color:#e6eef7;overflow-wrap:anywhere}
#source-list button small{grid-column:2;font:9px var(--mono);letter-spacing:.08em;color:var(--quiet);margin-top:4px;text-transform:uppercase}
-#source-list button.source-stale .source-dot,#source-list button.source-unavailable .source-dot{background:#6b7789;box-shadow:none}
+#source-list button.source-stale,#source-list button.source-unavailable{--accent:#7d8798}
#source-list button.source-stale,#source-list button.source-unavailable{color:var(--muted)}
#source-list button.source-stale .source-name,#source-list button.source-unavailable .source-name{color:#b0bccb}
#source-list button.source-unavailable::before{background:repeating-linear-gradient(180deg,#6b7789 0 3px,transparent 3px 7px);opacity:.8}
@@ -97,9 +97,7 @@ input[type=search]::placeholder{color:#4e5b6d}
#branch-list{max-height:30vh;overflow:auto;display:grid}
#branch-list button{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:9px;align-items:baseline;width:100%;text-align:left;
border:0;border-bottom:1px solid rgba(150,180,210,.07);border-radius:0;background:transparent;padding:9px 4px;margin:0}
-.branch-dot{width:5px;height:5px;border-radius:50%;background:#55637a;transform:translateY(-2px)}
-.activity-recent .branch-dot{background:var(--accent);box-shadow:0 0 8px var(--accent)}
-.activity-uncertain .branch-dot{background:var(--warn);box-shadow:0 0 8px rgba(224,168,92,.5)}
+.branch-dot{width:9px;height:9px;transform:translateY(-1px)}
.branch-name{font:11px var(--mono);color:#cdd9e8;overflow-wrap:anywhere}
#branch-list button small{grid-column:2;display:block;font:9px var(--mono);color:var(--quiet);margin-top:4px}
#branch-list button[aria-pressed=true] .branch-name{color:#eafcff}
@@ -135,16 +133,42 @@ input[type=search]::placeholder{color:#4e5b6d}
.space-caption p{font:10px/1.75 var(--mono);color:var(--faint);margin:8px 0 0;max-width:36ch}
.field-legend{display:flex;flex-wrap:wrap;gap:6px 16px;list-style:none;margin:16px 0 0;padding:14px 0 0;border-top:1px solid var(--rule);font:9px var(--mono);letter-spacing:.07em;color:var(--quiet);text-transform:uppercase}
.field-legend li{display:flex;align-items:center;gap:7px}
-.field-legend i{width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 9px var(--accent)}
-.legend-uncertain i{background:var(--warn);box-shadow:0 0 9px rgba(224,168,92,.55)}
-.legend-quiet i{background:#6b7789;box-shadow:none}
-.legend-plate i{background:transparent;border:1px solid var(--accent);box-shadow:0 0 9px rgba(127,209,222,.25)}
-.has-selection .field-legend{display:none}
+.legend-note{font:9px/1.6 var(--mono)!important;letter-spacing:.07em;color:var(--faint);margin:9px 0 0!important;text-transform:uppercase}
+.has-selection .field-legend,.has-selection .legend-note{display:none}
+
+/* ── Static status markers ──────────────────────────────────────────────────
+ * The same five engraved forms the GL/2D field draws, rebuilt in pure CSS for
+ * the legend, the atlas rail and the reading guide. Shape alone identifies a
+ * state: nothing here animates, and nothing depends on glow to be read.
+ * A plate is an authority, never evidence of a working host. */
+.marker{position:relative;box-sizing:border-box;display:inline-block;flex:none;width:10px;height:10px;border-radius:50%;background:none;box-shadow:none}
+.marker::after,.marker::before{content:'';position:absolute;box-sizing:border-box}
+/* plate: bezel ring and a hollow keystone, deliberately no filled core */
+.marker[data-marker=plate]{border:1px solid var(--accent);opacity:.95}
+.marker[data-marker=plate]::after{inset:2.5px;border:1px solid var(--accent);border-radius:1px;transform:rotate(45deg);opacity:.72}
+/* idle: one open hairline ring — observed, no running lease */
+.marker[data-marker=idle]{border:1px solid var(--accent);opacity:.62}
+/* recent: bullseye — ring, gap, separated core */
+.marker[data-marker=recent]{border:1px solid var(--accent)}
+.marker[data-marker=recent]::after{inset:2.5px;border-radius:50%;background:var(--accent)}
+/* uncertain: amber ring struck through */
+.marker[data-marker=uncertain]{border:1px solid var(--warn)}
+.marker[data-marker=uncertain]::after{left:1.5px;right:1.5px;top:50%;height:1px;margin-top:-.5px;background:var(--warn)}
+/* quiet: a broken ring, retained but not live */
+.marker[data-marker=quiet]{border:1px dashed #7d8798;opacity:.9}
+.field-legend .marker{width:9px;height:9px}
/* ── Field labels ───────────────────────────────────────────────────────── */
.swarm-label{position:absolute;top:0;left:0;font:10px var(--mono);letter-spacing:.04em;color:#a2b6c7;white-space:nowrap;max-width:168px;overflow:hidden;text-overflow:ellipsis;
padding:2px 6px;border-radius:2px;background:rgba(5,8,14,.42);text-shadow:0 1px 6px #04060b}
+.swarm-label:not(.source-label)[data-marker]::before{content:'';display:inline-block;box-sizing:border-box;width:7px;height:7px;margin-right:6px;border-radius:50%;vertical-align:.5px}
+.swarm-label[data-marker=idle]::before{border:1px solid var(--accent);opacity:.6}
+.swarm-label[data-marker=recent]::before{border:1px solid var(--accent);box-shadow:inset 0 0 0 1.6px #060a11,inset 0 0 0 4px var(--accent)}
+.swarm-label[data-marker=uncertain]::before{border:1px solid var(--warn);background:linear-gradient(var(--warn),var(--warn)) center/4px 1px no-repeat}
+.swarm-label[data-marker=quiet]::before{border:1px dashed #7d8798}
.swarm-label.active{color:#d6f4fb;box-shadow:inset 0 0 0 1px rgba(127,209,222,.22)}
+.swarm-label[data-marker=uncertain]{color:#e3c79c}
+.swarm-label[data-marker=quiet]{color:#8c99ac}
.swarm-label.source-label{display:flex;flex-direction:column;align-items:center;gap:2px;font:400 18px/1.1 var(--serif);letter-spacing:-.01em;color:#dcecf4;background:transparent;padding:0;max-width:220px}
.swarm-label.source-label i{font:9px var(--mono);font-style:normal;letter-spacing:.2em;text-transform:uppercase;color:var(--accent);opacity:.75}
.swarm-label.selected{color:#f1fdff;background:rgba(127,209,222,.12);box-shadow:inset 0 0 0 1px rgba(127,209,222,.4)}
@@ -167,6 +191,12 @@ input[type=search]::placeholder{color:#4e5b6d}
.swarm-inspector button:not(#swarm-close):hover{color:#eafcff;background:rgba(127,209,222,.06)}
.evidence-note{border:0!important;border-left:2px solid var(--warn)!important;background:rgba(224,168,92,.07)!important;border-radius:0;padding:11px 14px!important;color:#dcc49d!important;margin:14px 0}
+.marker-guide{list-style:none;margin:14px 0 4px;padding:0;display:grid;gap:1px}
+.marker-guide li{display:grid;grid-template-columns:12px minmax(0,1fr);gap:4px 11px;align-items:center;padding:11px 0;border-top:1px solid rgba(150,180,210,.09)}
+.marker-guide .guide-dot{grid-row:1/3}
+.marker-guide strong{font:9px var(--mono);font-weight:400;letter-spacing:.13em;text-transform:uppercase;color:#dce7f2}
+.marker-guide small{font:10px/1.5 var(--mono);color:var(--quiet)}
+
/* ── Tracking ledger and instruments ────────────────────────────────────── */
.swarm-timeline{position:absolute;z-index:15;bottom:24px;left:340px;width:min(470px,calc(100% - 800px));padding:13px 16px;border-radius:3px}
.swarm-timeline summary{display:flex;gap:10px;align-items:baseline;cursor:pointer;margin:0;font:var(--kicker);letter-spacing:.16em;color:var(--muted);text-transform:uppercase}
@@ -222,6 +252,7 @@ footer b{color:var(--warn);font-weight:400}
.space-caption,.navigation-hidden .space-caption{left:18px;bottom:24px;max-width:calc(100% - 36px)}
.observatory-title,.has-selection .observatory-title{font-size:clamp(26px,7vw,36px)}
.field-legend,.has-selection .field-legend{display:flex}
+ .legend-note,.has-selection .legend-note{display:block}
.swarm-measurements{right:14px;bottom:14px;max-width:62%}
.swarm-inspector,.has-selection .swarm-inspector{display:block;position:relative;right:auto;left:auto;top:auto;bottom:auto;width:auto;margin:16px 12px;padding:22px;backdrop-filter:none;background:var(--plate-solid)}
footer{height:auto;padding:14px 16px}
@@ -232,7 +263,8 @@ footer b{color:var(--warn);font-weight:400}
.read-only{display:none}
.swarm-space{height:clamp(500px,62vh,600px)}
.space-controls{flex-wrap:wrap}
- .field-legend li:nth-child(n+3){display:none}
+ .field-legend{gap:4px 10px;font-size:8.5px;letter-spacing:.04em}
+ .legend-note{display:none}
.space-caption p{display:none}
.swarm-inspector h2{font-size:22px}
}
diff --git a/src/swarm/renderer.ts b/src/swarm/renderer.ts
index b2c2af7..0b2b8cc 100644
--- a/src/swarm/renderer.ts
+++ b/src/swarm/renderer.ts
@@ -1,6 +1,6 @@
import { BufferGeometry, Color, Float32BufferAttribute, LineBasicMaterial, LineSegments, PerspectiveCamera, Points, Scene, ShaderMaterial, Vector3, WebGLRenderer } from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
-import type { Node, Point } from './model';
+import type { Node, Point, Source } from './model';
/** Deterministic authority palette. The navigation indicators read the same
* function, so a plate in the field and its index entry cannot disagree. */
@@ -10,34 +10,105 @@ export const sourceHue = (id: string) => hues[[...id].reduce((sum, char) => (sum
export const sourceColor = sourceHue;
export const uncertainHue = '#e0a85c';
export const retiredHue = '#7d8798';
-const tint = (node: Node): string => node.source.status !== 'observed' ? retiredHue : node.activity === 'uncertain' ? uncertainHue : node.cell?.status === 'retired' ? retiredHue : sourceHue(node.source.id);
+
+/** The five static status forms. Shape, not motion or brightness alone,
+ * carries the meaning, so every state stays recognisable with reduced motion:
+ * plate - a registered authority's engraved disc. Its centre is a hollow
+ * keystone, never a filled core: a plate is not worker activity.
+ * idle - observed record, no running lease: a single open hairline ring.
+ * recent - unexpired running lease on a fresh heartbeat: ring plus a
+ * separated inner core (bullseye). Observation, not host health.
+ * uncertain - unconfirmed running-work evidence or an owned unknown outcome:
+ * amber ring struck through.
+ * quiet - retained record (retired, reserved, stale or unavailable):
+ * a broken dashed ring with no core. */
+export type Marker = 'plate' | 'idle' | 'recent' | 'uncertain' | 'quiet';
+// Canonical envelopes are replaced on observation; clock ticks only update
+// source availability/activity. Cache once per envelope, with no retained owner
+// after that observation is collected and no per-cell scan of all effects.
+const unknownOwners = new WeakMap, Set>();
+export function hasUnknownOutcome(source: Source, cellId: string): boolean {
+ const envelope = source.snapshot; if (!envelope) return false;
+ let owners = unknownOwners.get(envelope);
+ if (!owners) { owners = new Set(envelope.snapshot.effects.filter(effect => effect.state === 'unknown').map(effect => effect.owner)); unknownOwners.set(envelope, owners); }
+ return owners.has(cellId);
+}
+export const markerOf = (node: Node): Marker => !node.cell ? node.source.status === 'observed' ? 'plate' : 'quiet'
+ // An unknown owned effect outranks completion, retirement and pause.
+ : hasUnknownOutcome(node.source, node.cell.id) ? 'uncertain'
+ : node.source.status !== 'observed' || node.cell.status === 'retired' || node.cell.status === 'reserved' ? 'quiet'
+ : node.activity === 'uncertain' ? 'uncertain'
+ : node.activity === 'recent' ? 'recent' : 'idle';
+export const markerForm: Record = { plate: 0, idle: 1, recent: 2, uncertain: 3, quiet: 4 };
+/** Wording is deliberately about the record, never about physical health. */
+export const markerLegend: Record = {
+ plate: 'registered authority · not worker activity',
+ idle: 'observed record · no running lease',
+ recent: 'running lease · recent heartbeat recorded',
+ uncertain: 'running-work evidence unconfirmed or owned outcome unknown',
+ quiet: 'retained record · retired, reserved or stale',
+};
+export const markerTitle: Record = {
+ plate: 'Authority plate', idle: 'Observed idle', recent: 'Recent leased work', uncertain: 'Uncertain', quiet: 'Retained record',
+};
+export function activityEvidence(node: Node): string {
+ if (node.cell && hasUnknownOutcome(node.source, node.cell.id)) return 'owned external outcome unknown · unresolved by completion or retirement';
+ if (node.source.status !== 'observed') return 'retained observation · current activity unconfirmed';
+ if (node.cell?.status === 'retired' || node.cell?.status === 'reserved') return `${node.cell.status} record · worker activity unverified`;
+ return node.activity === 'uncertain' ? 'running-work evidence unconfirmed · inspect controller, lease and heartbeat'
+ : node.activity === 'recent' ? 'running lease unexpired · heartbeat recent in this observation'
+ : 'observed record · no running lease';
+}
+export const markerColor = (node: Node): string => {
+ const marker = markerOf(node);
+ if (marker === 'uncertain') return uncertainHue;
+ if (marker === 'quiet' || node.source.status !== 'observed') return retiredHue;
+ return sourceHue(node.source.id);
+};
+const tint = markerColor;
/** One batched point program: engraved plates, cell rings, cores and haloes.
* Depth attenuation keeps a deep field legible without a second pass. */
-const vertexShader = `attribute float aSize; attribute float aActive; attribute float aFill; attribute float aPlate; attribute float aSelected;
-varying vec3 vColor; varying float vActive; varying float vFill; varying float vPlate; varying float vSelected; varying float vFade; uniform float uTime;
+const vertexShader = `attribute float aSize; attribute float aActive; attribute float aForm; attribute float aSelected;
+varying vec3 vColor; varying float vForm; varying float vSelected; varying float vFade; uniform float uTime;
void main(){
- vColor=color; vActive=aActive; vFill=aFill; vPlate=aPlate; vSelected=aSelected;
+ vColor=color; vForm=aForm; vSelected=aSelected;
vec4 p=modelViewMatrix*vec4(position,1.);
float depth=max(1.,-p.z);
vFade=clamp(1.25-depth/5200.,.34,1.);
- float breath=1.+aActive*.09*sin(uTime*2.+position.x*.12);
+ // Optional breathing, a courtesy only: aActive is zero under reduced motion
+ // and recent work is already distinguished by its static bullseye form.
+ float breath=1.+aActive*.07*sin(uTime*2.+position.x*.12);
gl_PointSize=clamp(aSize*breath*(1.+aSelected*.26)*200./depth,7.,78.);
gl_Position=projectionMatrix*p;
}`;
-const fragmentShader = `varying vec3 vColor; varying float vActive; varying float vFill; varying float vPlate; varying float vSelected; varying float vFade;
+const fragmentShader = `varying vec3 vColor; varying float vForm; varying float vSelected; varying float vFade;
void main(){
vec2 uv=gl_PointCoord-.5; float d=length(uv)*2.;
if(d>1.)discard;
- float core=(1.-smoothstep(.0,.40,d))*vFill;
- float ring=smoothstep(.49,.58,d)*(1.-smoothstep(.70,.80,d));
- float plate=vPlate*smoothstep(.86,.91,d)*(1.-smoothstep(.97,1.,d));
- float halo=exp(-d*d*3.6)*(.10+.12*vFill);
+ float plate=step(vForm,.5);
+ float idle=step(.5,vForm)*step(vForm,1.5);
+ float recent=step(1.5,vForm)*step(vForm,2.5);
+ float uncertain=step(2.5,vForm)*step(vForm,3.5);
+ float quiet=step(3.5,vForm);
+ // One shared engraved outline; quiet records break it into nine dashes.
+ float ring=smoothstep(.47,.55,d)*(1.-smoothstep(.65,.73,d));
+ float dash=step(.42,fract(atan(uv.y,uv.x)*1.43239));
+ float outline=ring*(1.-quiet*dash);
+ // Recent: a separated core inside the ring reads as a bullseye at any size.
+ float core=recent*(1.-smoothstep(.25,.33,d));
+ // Uncertain: the ring is struck through, a caution glyph rather than a glow.
+ float bar=uncertain*(1.-smoothstep(.06,.10,abs(uv.y)*2.))*(1.-smoothstep(.30,.42,abs(uv.x)*2.));
+ // Authority plate: wide bezel and a hollow keystone; deliberately no core.
+ float bezel=plate*smoothstep(.84,.90,d)*(1.-smoothstep(.96,1.,d));
+ float keystone=plate*(1.-smoothstep(.03,.06,abs((abs(uv.x)+abs(uv.y))*2.-.36)));
+ float halo=exp(-d*d*3.4)*(.05+.13*recent+.04*plate);
vec2 q=abs(uv)*2.;
- float spike=vSelected*max(exp(-q.x*22.),exp(-q.y*22.))*(1.-smoothstep(.2,1.,d))*.55;
+ float spike=vSelected*max(exp(-q.x*22.),exp(-q.y*22.))*(1.-smoothstep(.2,1.,d))*.5;
float select=vSelected*smoothstep(.80,.88,d)*(1.-smoothstep(.95,1.,d));
- vec3 lit=mix(vColor,vec3(.93,.99,1.),clamp(core*.55+spike*.7+select*.8,0.,1.));
- float alpha=(ring*.9+core*.85+halo+plate*.55+spike*.6+select*.95)*vFade;
+ float weight=outline*(recent+idle*.62+uncertain*.85+quiet*.5+plate*.78);
+ vec3 lit=mix(vColor,vec3(.93,.99,1.),clamp(core*.5+spike*.7+select*.8,0.,1.));
+ float alpha=(weight+core*.95+bar*.9+bezel*.6+keystone*.5+halo+spike*.6+select*.95)*vFade;
gl_FragColor=vec4(lit,clamp(alpha,0.,1.));
}`;
@@ -151,7 +222,7 @@ export class SwarmRenderer {
// The plan view has no animated shader; it only needs observation/input redraws.
this.active = !!this.gl && !this.motion.matches && nodes.some(node => node.activity === 'recent');
if (this.gl) {
- const pos: number[] = [], colors: number[] = [], sizes: number[] = [], active: number[] = [], fill: number[] = [], plate: number[] = [], chosen: number[] = [];
+ const pos: number[] = [], colors: number[] = [], sizes: number[] = [], active: number[] = [], form: number[] = [], chosen: number[] = [];
const linkPos: number[] = [], linkColors: number[] = [];
const byKey = new Map(nodes.map(node => [node.key, node]));
// Static engraved source plates share the delegation line batch. They are
@@ -181,10 +252,13 @@ export class SwarmRenderer {
pos.push(node.point.x, node.point.y, node.point.z);
const own = new Color(tint(node)), color = new Color(node.key === selected ? '#e6fff8' : tint(node));
colors.push(color.r, color.g, color.b);
- sizes.push(!node.cell ? 21 : node.key === selected ? 17 : node.cell.role === 'coordinator' ? 13.5 : node.cell.role === 'verifier' ? 10 : 8.5);
- active.push(!this.motion.matches && node.activity === 'recent' ? 1 : 0);
- fill.push(node.source.status === 'observed' && node.cell?.status !== 'retired' && node.cell?.status !== 'reserved' && node.activity !== 'uncertain' ? 1 : 0);
- plate.push(node.cell ? 0 : 1); chosen.push(node.key === selected ? 1 : 0);
+ const marker = markerOf(node);
+ // Evidenced and uncertain records are also drawn slightly larger, so
+ // the distinction survives a small point without relying on motion.
+ const emphasis = marker === 'recent' ? 1.3 : marker === 'uncertain' ? 1.16 : marker === 'quiet' ? .92 : 1;
+ sizes.push((!node.cell ? 21 : node.key === selected ? 17 : node.cell.role === 'coordinator' ? 13.5 : node.cell.role === 'verifier' ? 10 : 8.5) * (node.cell ? emphasis : 1));
+ active.push(!this.motion.matches && marker === 'recent' ? 1 : 0);
+ form.push(markerForm[marker]); chosen.push(node.key === selected ? 1 : 0);
const parent = node.parent ? byKey.get(node.parent) : undefined;
if (parent?.cell && node.cell?.parentId) {
// Lifted delegation ribbons fading parent to child, one line batch.
@@ -199,7 +273,7 @@ export class SwarmRenderer {
const geometry = new BufferGeometry();
geometry.setAttribute('position', new Float32BufferAttribute(pos, 3)); geometry.setAttribute('color', new Float32BufferAttribute(colors, 3));
geometry.setAttribute('aSize', new Float32BufferAttribute(sizes, 1)); geometry.setAttribute('aActive', new Float32BufferAttribute(active, 1));
- geometry.setAttribute('aFill', new Float32BufferAttribute(fill, 1)); geometry.setAttribute('aPlate', new Float32BufferAttribute(plate, 1));
+ geometry.setAttribute('aForm', new Float32BufferAttribute(form, 1));
geometry.setAttribute('aSelected', new Float32BufferAttribute(chosen, 1));
if (this.points) { this.points.geometry.dispose(); this.points.geometry = geometry; }
else { this.points = new Points(geometry, new ShaderMaterial({ vertexShader, fragmentShader, vertexColors: true, transparent: true, depthWrite: false, uniforms: { uTime: { value: 0 } } })); this.scene.add(this.points); }
@@ -212,9 +286,11 @@ export class SwarmRenderer {
this.labels.replaceChildren(); this.labelElements = [];
for (const node of labelled) {
const span = document.createElement('span');
- span.className = `swarm-label${!node.cell ? ' source-label' : ''}${node.key === selected ? ' selected' : ''}${node.activity === 'recent' ? ' active' : ''}`;
+ const marker = markerOf(node);
+ span.className = `swarm-label${!node.cell ? ' source-label' : ''}${node.key === selected ? ' selected' : ''}${marker === 'recent' ? ' active' : ''}`;
+ span.dataset.marker = marker;
span.textContent = node.label.length > 24 ? `${node.label.slice(0, 22)}…` : node.label;
- span.title = `${node.source.factoryId} / ${node.label}`;
+ span.title = `${node.source.factoryId} / ${node.label} · ${markerLegend[marker]}`;
span.dataset.key = node.key;
span.style.setProperty('--accent', sourceHue(node.source.id));
if (!node.cell) { const kicker = document.createElement('i'); kicker.textContent = node.source.status === 'observed' ? 'authority' : node.source.status; span.append(kicker); }
@@ -300,14 +376,25 @@ export class SwarmRenderer {
context.strokeStyle = lit ? `${tint(node)}c0` : `${tint(node)}30`; context.lineWidth = lit ? 1.4 : 1; context.stroke();
}
for (const node of this.nodes) {
- const point = this.projected.get(node.key)!, hue = tint(node);
- const radius = !node.cell ? 14 : node.key === this.selected ? 10 : node.cell.role === 'coordinator' ? 9 : node.cell.role === 'verifier' ? 6 : 5;
- const gradient = context.createRadialGradient(point.x, point.y, 0, point.x, point.y, radius * 3.2);
- gradient.addColorStop(0, `${hue}cc`); gradient.addColorStop(.32, `${hue}55`); gradient.addColorStop(1, `${hue}00`);
- context.fillStyle = gradient; context.beginPath(); context.arc(point.x, point.y, radius * 3.2, 0, Math.PI * 2); context.fill();
- context.strokeStyle = hue; context.lineWidth = node.cell ? 1 : 1.4; context.beginPath(); context.arc(point.x, point.y, radius * .78, 0, Math.PI * 2); context.stroke();
- if (!node.cell) { context.strokeStyle = `${hue}66`; context.beginPath(); context.arc(point.x, point.y, radius * 1.5, 0, Math.PI * 2); context.stroke(); }
- if (node.source.status === 'observed' && node.cell?.status !== 'retired' && node.cell?.status !== 'reserved' && node.activity !== 'uncertain') { context.fillStyle = hue; context.beginPath(); context.arc(point.x, point.y, radius * .34, 0, Math.PI * 2); context.fill(); }
+ const point = this.projected.get(node.key)!, hue = tint(node), marker = markerOf(node);
+ const scale = marker === 'recent' ? 1.3 : marker === 'uncertain' ? 1.16 : marker === 'quiet' ? .92 : 1;
+ const radius = (!node.cell ? 14 : node.key === this.selected ? 10 : node.cell.role === 'coordinator' ? 9 : node.cell.role === 'verifier' ? 6 : 5) * (node.cell ? scale : 1);
+ const bloom = marker === 'recent' ? 2.6 : marker === 'plate' ? 3.2 : 1.9;
+ const gradient = context.createRadialGradient(point.x, point.y, 0, point.x, point.y, radius * bloom);
+ gradient.addColorStop(0, `${hue}${marker === 'idle' || marker === 'quiet' ? '44' : '88'}`); gradient.addColorStop(.34, `${hue}33`); gradient.addColorStop(1, `${hue}00`);
+ context.fillStyle = gradient; context.beginPath(); context.arc(point.x, point.y, radius * bloom, 0, Math.PI * 2); context.fill();
+ context.setLineDash(marker === 'quiet' ? [2.4, 2.4] : []);
+ context.strokeStyle = marker === 'idle' ? `${hue}b0` : marker === 'quiet' ? `${hue}a0` : hue;
+ context.lineWidth = marker === 'plate' ? 1.4 : marker === 'recent' ? 1.3 : 1;
+ context.beginPath(); context.arc(point.x, point.y, radius * .78, 0, Math.PI * 2); context.stroke();
+ context.setLineDash([]);
+ if (marker === 'plate') {
+ // Bezel plus a hollow keystone: an authority, never evidenced activity.
+ context.strokeStyle = `${hue}66`; context.beginPath(); context.arc(point.x, point.y, radius * 1.5, 0, Math.PI * 2); context.stroke();
+ context.beginPath(); context.moveTo(point.x, point.y - radius * .32); context.lineTo(point.x + radius * .32, point.y); context.lineTo(point.x, point.y + radius * .32); context.lineTo(point.x - radius * .32, point.y); context.closePath(); context.stroke();
+ }
+ if (marker === 'recent') { context.fillStyle = hue; context.beginPath(); context.arc(point.x, point.y, radius * .34, 0, Math.PI * 2); context.fill(); }
+ if (marker === 'uncertain') { context.strokeStyle = hue; context.lineWidth = 1.2; context.beginPath(); context.moveTo(point.x - radius * .46, point.y); context.lineTo(point.x + radius * .46, point.y); context.stroke(); }
if (node.key === this.selected) {
context.strokeStyle = '#d2fff0'; context.lineWidth = 1.2;
context.beginPath(); context.arc(point.x, point.y, radius + 5, 0, Math.PI * 2); context.stroke();
diff --git a/swarm.html b/swarm.html
index b7be622..0f92322 100644
--- a/swarm.html
+++ b/swarm.html
@@ -40,12 +40,14 @@
A wider mind.
Orbit the recorded world. Enter a source. Follow its work.
-
- unexpired lease · recent heartbeat
- uncertain evidence
- retired · reserved · stale
- authority plate
+
+ authority plate · not worker activity
+ observed · no running lease
+ running lease · recent heartbeat
+ work unconfirmed · or outcome unknown
+ retained · retired, reserved or stale
+ Shapes, not motion: recorded observations only, never host or worker health.
LIVE TRACKING
@@ -53,7 +55,7 @@
- FACTORY owns execution and recovery. Recorded activity does not establish host health. Worker quiescence: unverified .
+ FACTORY owns execution and recovery. Recorded activity does not establish host health. Worker quiescence: unverified . Markers report records, not host health.