Files
aiturk-hermes-ide/apps/desktop/src/app/starmap/share-code.ts
T

197 lines
6.6 KiB
TypeScript

import type { EN_SURFACES } from '@/i18n/en-surfaces'
import { type BitReader, type BitWriter, createLoadout, Dict, idxOf, indexBits, LoadoutError } from '@/lib/loadout'
import type { StarmapEdge, StarmapGraph, StarmapNode } from '@/types/hermes'
// ── Star-map share code ───────────────────────────────────────────────────────
//
// The body schema for a star map, riding the generic loadout codec (@/lib/loadout
// owns the bitstream, DEFLATE, version+checksum frame, and base64url). We encode
// what the map RENDERS — each node's kind, its time POSITION (12-bit quantized,
// not an absolute epoch), radius inputs (useCount/state/pinned), and an interned
// label + category — plus edges as fixed-width node indices. Memory prose is
// dropped; labels are trimmed. DEFLATE then makes the repetitive label/category
// text almost free. A 60-skill map is a few hundred chars.
const VERSION = 3
const PREFIX = 'HML' // "Hermes Memory Loadout" — namespaces our codes like WoW's leading bytes.
const MAX_LABEL = 64 // trim runaway memory titles so one card can't bloat the code.
const trim = (s: string): string => (s.length > MAX_LABEL ? s.slice(0, MAX_LABEL) : s)
const KINDS = ['skill', 'memory'] as const
const STATES = ['active', 'archived', 'disabled', 'draft'] as const
const MEM_SOURCES = ['none', 'memory', 'profile'] as const
const CREATED_BY = ['none', 'agent', 'user'] as const
const REC_BITS = 12 // time position resolution: 1/4096 of the span — sub-pixel here.
const REC_MAX = (1 << REC_BITS) - 1
const finiteTs = (v?: null | number): null | number =>
typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.round(v)) : null
function writeNode(w: BitWriter, n: StarmapNode, dict: Dict, minTs: number, span: number): void {
w.uint(idxOf(KINDS, n.kind), 1)
w.varint(dict.id(trim(n.label || '')))
w.varint(dict.id(n.category || ''))
w.varint(Math.max(0, n.useCount | 0))
w.uint(idxOf(STATES, n.state), 2)
w.uint(idxOf(MEM_SOURCES, n.memorySource ?? 'none'), 2)
w.uint(idxOf(CREATED_BY, n.createdBy ?? 'none'), 2)
w.bit(n.pinned)
// Time as a 12-bit POSITION within [minTs, maxTs] — not an absolute epoch.
const ts = finiteTs(n.timestamp)
if (ts === null) {
w.bit(0)
} else {
w.bit(1)
w.uint(span > 0 ? Math.round(((ts - minTs) / span) * REC_MAX) : 0, REC_BITS)
}
}
function readNode(r: BitReader, dict: string[], i: number, minTs: number, span: number): StarmapNode {
const kind = KINDS[r.uint(1)] ?? 'skill'
const label = dict[r.varint()] ?? ''
const category = dict[r.varint()] ?? ''
const useCount = r.varint()
const state = STATES[r.uint(2)] ?? 'active'
const memSrc = MEM_SOURCES[r.uint(2)] ?? 'none'
const createdBy = CREATED_BY[r.uint(2)] ?? 'none'
const pinned = r.bit() === 1
const timestamp = r.bit() === 1 ? minTs + (span > 0 ? Math.round((r.uint(REC_BITS) / REC_MAX) * span) : 0) : null
// Ids are synthesized (they're never displayed); memory ids mirror the scan's
// `memory:<source>:<index>` shape so the rest of the UI is none the wiser.
const isMemory = kind === 'memory'
const source = memSrc === 'none' ? 'memory' : memSrc
return {
category,
createdBy: createdBy === 'none' ? null : createdBy,
id: isMemory ? `memory:${source}:${i}` : `s${i}`,
kind,
label,
memorySource: isMemory ? source : undefined,
pinned,
state,
timestamp,
useCount
}
}
function writeGraph(w: BitWriter, graph: StarmapGraph): void {
const dict = new Dict()
// Intern labels + categories; deflate later squeezes the inevitable repetition.
for (const n of graph.nodes) {
dict.id(trim(n.label || ''))
dict.id(n.category || '')
}
const stamps = graph.nodes.map(n => finiteTs(n.timestamp)).filter((v): v is number => v !== null)
const minTs = stamps.length ? Math.min(...stamps) : 0
const maxTs = stamps.length ? Math.max(...stamps) : 0
const span = maxTs - minTs
w.varint(minTs)
w.varint(maxTs)
w.varint(dict.list.length)
for (const s of dict.list) {
w.str(s)
}
w.varint(graph.nodes.length)
for (const n of graph.nodes) {
writeNode(w, n, dict, minTs, span)
}
// Edges reference nodes by position; drop any whose endpoints aren't both nodes.
const order = new Map(graph.nodes.map((n, i) => [n.id, i]))
const edges = graph.edges.filter(e => order.has(e.source) && order.has(e.target))
const bits = indexBits(graph.nodes.length)
w.varint(edges.length)
for (const e of edges) {
w.uint(order.get(e.source)!, bits)
w.uint(order.get(e.target)!, bits)
}
}
function readGraph(r: BitReader): StarmapGraph {
const minTs = r.varint()
const maxTs = r.varint()
const span = maxTs - minTs
const dictLen = r.varint()
const dict: string[] = []
for (let i = 0; i < dictLen; i += 1) {
dict.push(r.str())
}
const nodeCount = r.varint()
const nodes: StarmapNode[] = []
for (let i = 0; i < nodeCount; i += 1) {
nodes.push(readNode(r, dict, i, minTs, span))
}
const bits = indexBits(nodeCount)
const edgeCount = r.varint()
const edges: StarmapEdge[] = []
for (let i = 0; i < edgeCount; i += 1) {
const src = nodes[r.uint(bits)]
const dst = nodes[r.uint(bits)]
if (src && dst) {
edges.push({ source: src.id, target: dst.id })
}
}
const counts = new Map<string, number>()
for (const n of nodes) {
counts.set(n.category, (counts.get(n.category) ?? 0) + 1)
}
const clusters = [...counts.entries()]
.map(([category, count]) => ({ category, count }))
.sort((a, b) => b.count - a.count)
// Memory cards are dropped (viz-only); a marker lets the UI tell a decoded map
// apart from a freshly-scanned one.
return { clusters, edges, memory: [], nodes, stats: { imported: true } }
}
export class ShareCodeError extends LoadoutError {}
export function shareCodeErrorText(error: unknown, messages: typeof EN_SURFACES.mapErrors): string {
if (!(error instanceof ShareCodeError) || !error.info) {return messages.unknown}
const info = error.info
return info.kind === 'version' ? messages.version(info.actual, info.expected) : messages[info.kind]
}
const codec = createLoadout<StarmapGraph>({
error: ShareCodeError,
noun: 'map code',
prefix: PREFIX,
read: readGraph,
version: VERSION,
write: writeGraph
})
// Serialize a star-map graph to a short, opaque, clipboard-safe loadout string.
export function encodeShareCode(graph: StarmapGraph): string {
return codec.encode(graph)
}
// Parse a loadout string back into a (viz-complete, text-synthesized) graph.
export function decodeShareCode(code: string): StarmapGraph {
return codec.decode(code)
}