Skip to content

15. Frontend

Context and Problem Statement

The Altar gives a browser form to conversation, selected-Run evidence, capability transitions, and Pattern inspection. It is a disposable projection: Litestar and the Phylactery retain validation, policy, lifecycle, and durable mutation. Static output removes a production server; it does not make browser state authoritative or the loopback surface safe for hostile remote use.

Requirements

  • Litestar owns validation, caller scope, consent, persistence, workflow movement, and every durable mutation. The retired Jinja, HTMX, Alpine, and island paths are not compatibility APIs.
  • Browser contracts derive from Altar-controller OpenAPI. Framework-neutral TypeScript owns generated types, transport, runtime validation, and event reduction.
  • SvelteKit supplies static routes and layouts only: no SSR, server loads/routes, form actions, remote functions, or JavaScript production server.
  • A snapshot plus event cursor must reconstruct browser state. Connection, animation, and canvas layout never prove execution.
  • Bridge, Orb, Nexus, and Loom are separate routes with explicit server fallbacks. Generated UI is closed descriptors, never model- or extension-supplied HTML, JavaScript, Svelte, or imports.
  • Circle is the Invocation-shaped focus within Bridge, not a fifth authority surface or a claim that the current run strip already composes every owning projection.
  • Native CSS includes keyboard access, visible focus, non-colour cues, reduced motion, named regions, and visible unknown/error states.
  • The Altar ships one canonical built-in appearance, LychD Dark. Its semantic colour boundary must remain closed enough for a future validated operator palette override without promising a second built-in theme, arbitrary CSS, or extension-owned UI.
  • The Altar keeps a replaceable localization boundary for interface copy, accessibility labels, pluralization, locale-sensitive formatting, and text direction. Interface locale never rewrites Intent, model output, artifacts, or evidence.
  • Every executable software dependency in the supported client, build, and renderer path is locally operable FOSS under OSI-approved terms; non-code data may use reviewed public-domain-equivalent terms such as CC0. Proprietary services and source-available-only packages are not admitted. Dependency closure, licence compatibility, and notices are reviewed with Packaging before admission.

Considered Options

Option Decision Why
Server-rendered hypermedia with Svelte islands Rejected It preserves two projection contracts; historic hypermedia is evidence, not a supported surface.
React 19 with Vite Rejected under current requirements; eligible only through the reopening gate Its explicit external-store and renderer-change seams are useful, but they do not by themselves establish domain intent, lifecycle safety, static routing, or lower whole-system maintenance.
Plain Svelte SPA Rejected Components alone do not establish the selected route, layout, fallback, deep-link, and static-build conventions.
Svelte 5 with static SvelteKit Selected and reaffirmed Svelte supplies the view language; SvelteKit and adapter-static supply the client router and static artifact while framework-neutral TypeScript retains authority.

Decision Outcome

The canonical Altar is a Svelte 5 static SPA. clients/web/src/routes/+layout.ts sets ssr = false and prerender = false; adapter-static writes index.html and assets to src/lychd/public. Litestar redirects / to /bridge, admits the instrument/deep-link shells, and mounts hashed assets at /_app. New Svelte uses runes, current event attributes, snippets, and native CSS. +page.server.*, +layout.server.*, +server.*, private server modules, form actions, remote functions, and a SvelteKit production server are forbidden. Node 24.18 and npm 11.16 are build pins; Litestar/Granian alone serves production requests. Installed tools, lockfile, generated output, and focused tests are evidence; State owns delivery.

Decision lock and reopening gate

The frontend stack was re-examined as a greenfield choice against React 19 with Vite, Node versus Bun, native CSS versus Tailwind, one versus separate Loom and Orb renderers, and a Rust/Wasm frontend. Popularity, hiring pool, model-training volume, community enthusiasm, source-line count, bundle size, and isolated microbenchmarks do not decide this Covenant. The review reaffirmed one coherent boundary:

  • Svelte 5 with static SvelteKit remains the Altar framework. React's more explicit effect diagnostics and React Flow's tagged renderer changes are real advantages, but renderer changes are not domain commands. They did not establish that Svelte cannot preserve the same authority, recovery, accessibility, route, and release contracts with less total maintenance.
  • Node 24.18 with npm 11.16 remains the only verified JavaScript build grammar. Bun is not a supported installer, runner, or runtime: adding it beside Node/npm would create a second runtime, lock, and command meaning without replacing the accepted path. Reopening requires one complete matched repository receipt that replaces or passes every Vite, Vitest, browser, notice, and release gate—not install speed.
  • Native semantic CSS remains the only styling vocabulary. A large stylesheet is repaired by token, base, shared-chrome, graph-seam, and instrument ownership—not by adding Tailwind's scanner, utility grammar, or a parallel cascade truth.
  • Loom and Orb share one DOM renderer. A second renderer buys a second adapter, semantic twin, teardown audit, licence review, and browser-quirk tail; no measured workload requires that cost. They share immutable TypeScript contracts, identities, commands, selection/camera protocols, telemetry, folding, and a model-derived semantic twin. They do not share instrument view models, and shared code never lets one instrument's authority reach the other.
  • Nexus remains a native semantic control board. Its cards, preview, ticket, and inspector are primary. The first trial for a future body map is a read-only Svelte DOM/SVG projection; it does not need an editor or dense-graph engine without a measured topology requirement.

This choice is closed to preference-driven reconsideration. Articles, polls, hiring arguments, framework enthusiasm, toy examples, and an isolated speed or size result are discovery leads, not reopening evidence. Reopen the Svelte decision only when an executable product-shaped receipt shows that the accepted stack cannot meet a required security, packaging, accessibility, deep-link, recovery, lifecycle, or measured workload contract and a named replacement passes that same contract. A replacement must also preserve every existing Litestar route, generated API, focus-reset, asset-mount, API-404, notice, and release-artifact gate.

The XYFlow line is admitted for Loom under a corrected gate. Its shared pan/zoom implementation creates a ResizeObserver that its destroy path does not disconnect, and upstream declares that omission deliberate: destroy() also runs to pause zooming during a user selection, so disconnecting there would leave the extent cache stale. Under the observer-lifetime rule an observer dies only when it holds no scripting reference and observes no target, so an unreferenced observer that still observes is retained by the specification alone. Collection therefore depends on the engine holding its target weakly, which Blink does; Gecko and WebKit are unverified. Counting live observers still measures upstream intent rather than retained memory, but the specification does not by itself clear this path. The gate is measured retained heap: repeated mount, replacement, settlement, and HMR cycles followed by forced collection must show no growth on every supported engine, and reasoning does not substitute for that measurement. A candidate must also prove, through public APIs and without a maintained fork, private-store access, whole-flow remount, or renderer-state authority, that drag, keyboard movement, resize, measurement, selection, connect, reconnect, delete, rejection, and resync preserve the authoritative semantic twin.

Loom is an editor, not a dense field. Its registered Scrolls place five and three stations today, over six and two permitted edges, and stay in the tens to low hundreds under any admitted Suite grammar. Orb is bounded by folding rather than by volume. Neither workload justifies a dense engine, and a dense engine supplies neither Loom's drag, connect, reconnect, and handle grammar nor a DOM-native accessible twin.

Loom's editing admission is separately gated on contract, not on renderer. The served semantic score carries no position, and the Scroll grammar declares no layout document, so no drag may be admitted before authority owns layout and mutation intent. Until then Loom is a read-only projection over a computed layout. XYFlow also has no keyboard path to create a connection; its connection handles are not focusable. Loom therefore either owns that path above the renderer or admits editing without it, and admitting editing without it would break the keyboard twin this Covenant requires.

Folding, not scale

A rendered field is bounded by folding at every camera scale. Folding is a legibility rule first: a field no reader can read is not evidence, and no engine repairs that. The bounded rendered set is its consequence, and it is what keeps a DOM renderer sufficient.

Folding follows structure the authority already declares—for Orb, Run, then station subject, then occurrence, then event, with delegated jobs as their own declared groups. A group states its membership count, carries its declared name, and remains selectable and expandable through the same typed identities. Geometric or computed clustering is not admitted: proximity would assert a relation no authority declared, and a group must never be readable as evidence LychD did not record.

Folding is presentation only. It never merges, omits, or reorders retained sequence truth, and an unexpanded group whose members were never served is an explicit unknown, exactly like a sequence gap. Bounded expansion remains a Vessel contract: authority serves what was asked for, and the browser holds only what it was served. Folding belongs to Loom and Orb through their shared supply; Nexus folds by disclosure in its own board and inherits no renderer.

Deferred dense renderers

Sigma with Graphology and cosmos.gl are mapped, not admitted, and are reopened only by a measured field that folding cannot bound—not by node count alone. Both are FOSS: Sigma's closure is MIT throughout; cosmos.gl became admissible only after moving to the OpenJS Foundation under MIT, its predecessor having carried non-commercial terms that failed this Covenant outright.

The mapping is recorded so a later review starts from evidence rather than from preference. Neither ships zoom-driven aggregation, and neither does any commercial alternative surveyed; aggregation is LychD-owned under every candidate, which is why it is specified above as a supply rule instead of a selection criterion. Sigma re-indexes its whole graph when a node is dropped, and eviction is the operation bounded expansion is made of. cosmos.gl has no removal path for links, so an edge-touching patch rebuilds the whole link set, and its identity-keyed streaming API is published only under the non-commercial tier its maintainers also sell. Neither handles WebGL context loss, and neither carries any accessibility surface, so the semantic twin would be built twice and kept in sync forever. G6 was evaluated and rejected outright: its own published maximum-scale demonstration fails, its combo collapse is a pointer-triggered action over hand-declared groups rather than zoom-driven aggregation, and its teardown discards its own context while asynchronous work still holds it.

Admission would still require realistic scale, incremental patch, camera/selection recovery, WebGL-context loss, teardown, complete semantic-twin, licence, and notice receipts, and any admitted graph structure would remain a disposable projection cache, never Run truth. Custom WebGPU or a narrow Rust/Wasm worker is an escalation only after profiling proves that this path cannot meet the same contract; it is not a route to rewriting the browser shell.

The four instruments

Instrument Browser contract Current limit
Bridge Session selection, single-active-Run text admission, reconstruction, semantic stream, consent, and closed fragments Partial: text only; live reconstruction and tokens are process-local.
Orb One selected Run's ordered, paged structural evidence, gaps, capture label, and links Partial: no index, tail, complete trace store, annotation, or multi-Run field.
Nexus Timestamped capability observations, non-binding plan preview, typed request, and ticket stream Partial: observations and tickets are process-local and not restart-complete.
Loom Immutable Scroll/Pattern-revision reader, station/permission outline, optional Mermaid lens Partial: fixed registry reader without independent Spell identities, editing, teaching, or publication.

The shell owns navigation, source identity, pending-consent count, and transient notices; an instrument owns route-local presentation. /scrying, /reliquary, /bindings, and ambiguous unversioned Loom detail are deliberately not routes. Scrying is Orb's act, not a fifth instrument. Artifact references and configuration observations are contextual projections, not browser custody or configuration authority.

Circle is likewise not a fifth instrument or route in the present contract. One Bridge séance may contain many Invocation/Run projections; a future Circle focus may compose authorized Loom, Nexus, and Orb lenses around exactly one of them without importing their authority into browser state. The current run strip is only its delivered seed.

Projection law

The Vessel emits validated snapshots, semantic events, and admitted mutation intents. The Altar projects them; it does not settle system truth.

Loom and Orb share one framework-neutral graph-projection supply in ordinary immutable TypeScript: stable server identities and revisions, validated snapshots and ordered deltas, semantic nodes and relations, declared fold groups and their membership counts, presentation-local selection and viewport intents, and revision-fenced mutation intents. Folding and the semantic twin derive from this supply, so both survive a renderer change and neither is owned by a dependency. Each instrument derives its own view model over one shared renderer adapter. The supply contains no runes, reactive proxies, XYFlow, Sigma, or Graphology objects, renderer geometry, or durable policy. Mutating Loom gestures become explicit server commands; authority replaces the structural projection after acceptance, rejection, gap recovery, or resync, while local camera, selection, and focus are restored only while their identity and generation remain valid. A complete keyboard and screen-reader twin derives from the same supply. This reuse is not a universal Graph or domain model.

The client has three replaceable layers: validated snapshot; ordered deltas reduced against its cursor; and URL, selection, layout, draft, focus, and related presentation. Refresh, remount, numeric gap, or resync replaces projection from a snapshot. An animation or open EventSource is never commitment evidence.

Use $state only for template, $derived, or external-synchronization reactivity; use $state.raw for large unit-replaced API objects, $derived/$derived.by for computation, and $effect for DOM, browser, or network synchronization—not state propagation. Props change, collections key by stable identity, and runes stay in components or presentation-owned .svelte.ts. Snapshot or unwrap reactive values before Fetch, SSE, structured clone, IndexedDB, worker, or extension boundaries. Generated contracts, Zod, Fetch wrappers, reducers, and domain decisions remain ordinary TypeScript; a guard rejects runes in framework-neutral .ts.

Typed JSON and semantic SSE

scripts/export_openapi.py builds a small schema application from production Altar controllers and dependencies. Runtime and exporter share the same deterministic JSON-only OpenAPI configuration; generated controller errors use Litestar's JSON status_code, detail, and optional extra fields. The exporter does not run create_app() or its middleware, exception handlers, lifespan, database, SAQ, or security configuration. The chain is:

Altar controllers → clients/web/openapi.json → openapi-typescript
  → clients/web/src/lib/api/openapi.d.ts → aliases + openapi-fetch

Controllers own operation identifiers. Release regeneration and clean-source preflight expose schema drift; a local command may rewrite tracked output, whose diff still needs review. SSE stays outside openapi-fetch: Zod schemas, constrained to generated types, validate version, identifiers, sequence, kind, and envelope. Kind-specific interpretation remains explicit because run payload is currently a broad string-keyed record, not a generated discriminated schema. Operations that raise a real not-found response explicitly publish the shared FrameworkError schema; generated clients never infer success-only behavior for those paths.

Bridge is text JSON only: no voice upload, file/media admission, audio output, or streaming voice. Its named JSON SSE envelope carries schema_version, run_id, producer-stable event_id, seq, kind, occurred_at, and payload; kinds are token, status, node, dispatch, transition, fragment, consent, log, done, and resync. The server observes browser-managed Last-Event-ID, sends keepalives, and projects its process-local bus. Terminal runs receive resync, not invented token replay.

The client seeds from the snapshot cursor, serially validates and reduces events, ignores applied sequence numbers, and refetches on gap or resync. Its initial cursor is reducer state: the current EventSource constructor does not send an explicit cursor. A run or ticket identity mismatch, invalid data, or failed authoritative refetch permanently closes that channel. Bridge and Nexus immediately mark the projection stale and attempt one bounded authoritative recovery; a second failure remains visibly stale rather than animated as live. Each Bridge Run projection retains its applied cursor and a browser-local authority generation; a delayed recovery may replace that Run only while both still match the request it began from. Ordinary transport errors remain transient while EventSource reconnects. Durable terminal Run status overrides a lagging process-local channel, and only a retained agent turn retires that terminal projection from the selected session. Token deltas and channels are neither durable nor cross-process; retained structural Step evidence is best-effort. Nexus uses its own versioned transition envelope with the same retention boundary and completion-driven single-flight polling. Closing a channel fences already-queued callbacks, so an event from a superseded ticket cannot overwrite the replacement identity. Orb pagination likewise merges only when both the requested and current snapshot still name the same Run; its Loom link additionally requires the entire valid pinned manifest to equal the registered revision, as worker replay does.

Component destruction advances the same local authority boundary. A request that settles after teardown cannot attach a stream, mutate component state, schedule another poll, or navigate. Loom uses its load generation for this rule; Orb aborts owned snapshot and pagination reads as it advances that generation; Bridge and Nexus additionally fence stream callbacks and timers.

Closed rendering, readable form

The Vessel validates FragmentCalls through a closed Pydantic registry and emits inert descriptors. Svelte maps admitted kinds to compiled renderers and exposes unknown descriptors explicitly. {@html} is forbidden and statically guarded. Invalid or unknown model fragments are dropped and logged before settlement; client fallback merely contains malformed or newer descriptors. Settled turns retain the complete validated descriptor, including props, so terminal refresh reconstructs the same compiled component rather than preserving only its key. Legacy rows that predate descriptor retention are normalized to inert schema-zero key-only descriptors with empty props; they remain readable through an explicit inert fallback and never enter a current-version component renderer.

Consent appears in its Bridge context and the shell count. The client submits one typed approve/deny intent with the configured CSRF header; the Vessel rechecks identity, state, scope, and idempotency before resume. Snapshot application versions selected-Bridge consent authority, so an older decision response cannot overwrite a newer snapshot; after Run cancellation the selected Bridge immediately revokes its visible consent cards and count, then refetches its snapshot. Root route cancellation uses the selected snapshot session as that authority rather than a route prop. A failed refetch cannot restore the revoked local authority. Instrument attention events are invalidation hints only: the shell always re-reads the cross-session status endpoint and request-version fences overlapping reads, so an arriving local count never becomes global truth. Bridge message submission likewise retains one client UUID across an ambiguous response, and durable Run admission maps that identity to exactly one canonical Run. A replay repairs an unresolved held turn-retention gate before publication. Within one Bridge session, process-local admission permits that exact replay but refuses a different message while any prior Run remains nonterminal; terminal ledger truth admits the next turn without a separate active marker. The fixed visible Magus Sigil is local bootstrap context, not an authenticated person. Applying a refreshed root snapshot for the same canonically selected session preserves the unsent draft; only an actual selected-session identity change clears it.

Nexus retains an ambiguous transition request UUID per target, so inspecting another target cannot discard the only safe retry identity. A lost-ticket conflict retains that UUID and refuses a fresh physical launch; only a definitive non-conflict client rejection clears that target. Authoritative refresh rebinds the inspector to the exact request id so a settled ticket cannot leave stale pre-refresh transition detail selected. Board refresh remains single-flight, but a refresh requested while one is in flight marks a dirty trailing pass; the settling read cannot erase a newer invalidation.

Extensions have no UI source, template, script, import, or third-party sandbox surface. @xyflow/svelte is admitted for Loom but not installed; State owns delivery. Loom's optional locally bundled Mermaid diagram runs in strict security mode; its textual station/permission score remains visible and authoritative on rendering failure. Plain-text source lives below /api/v1/loom/source/workflows/{workflow} and /api/v1/loom/source/patterns/{pattern_id}/{revision} so every legal two-segment exact Pattern route remains addressable. Mermaid source is not Pattern data. A later renderer must isolate DTO identity from renderer coordinates, treat geometry and motion as disposable, deny publication/execution from a read-only view, and keep a keyboard-operable outline, list, table, or timeline.

Native CSS uses custom properties, cascade layers, media queries, semantic classes, and state attributes. Type, label, icon, shape, and copy carry meaning without colour; the stylesheet has a skip link, visible focus, hidden labels, narrow layout, and prefers-reduced-motion. Inspectors restore their live opener after closing when one exists; deep links use router focus reset. Tailwind, Sass, project-owned PostCSS, and a parallel styling vocabulary are forbidden. Transitive Vite packages in the lockfile are not a styling API.

Canonical appearance and bounded palette configuration

The canonical Altar has one complete built-in appearance: LychD Dark. Dark is the product default and the only appearance Core currently designs, releases, and supports. The document and native controls use a dark colour scheme; Core does not follow prefers-color-scheme, expose a light/system selector, or create presentation state merely to imply choice. “Readable by day and night” remains an acceptance target requiring contrast plus bright- and dim-environment browser receipts, not a conclusion inferred from dark tokens.

The styling boundary nevertheless stays configurable in one deliberately narrow direction. A future operator-local palette may provide a partial map over an allowlisted set of semantic colour roles—for example page and panel surfaces, primary and secondary text, focus/rune, ready, active, warning, refusal, borders, and decorative accent. Resolution starts from the complete pinned LychD Dark palette, applies valid overrides, and falls back per missing value. The public configuration names semantic roles rather than CSS variable or selector identities; Frontend owns their meaning, while Configuration must admit any future typed source and precedence before it is delivered.

An override value uses a restricted, resource-free colour grammar and cannot contribute selectors, declarations, functions that fetch resources, url(), fonts, layout, visibility, opacity, animation, HTML, Svelte, JavaScript, routes, or assets. Validation preserves required contrast, visible focus, forced-colour behavior, non-colour status distinctions, and the legibility of consent, refusal, fault, unknown, and evidence states. Missing roles inherit their canonical values; an explicitly invalid map is refused before application rather than producing a half-themed Altar.

Components, diagrams, and overridable artwork consume the resolved semantic palette or declare themselves fixed LychD brand assets outside it. Mermaid, browser chrome, native controls, static art, and generated surfaces cannot keep an undisclosed second palette. A future named palette package may serialize this same finite map only after Extensions and Packaging admit its provenance, licence, static build, compatibility, and accessibility receipts; it never opens a runtime CSS plug-in surface.

Palette choice is local presentation configuration. It never changes a Run, Pattern, event, artifact, consent, authority, status, or retained evidence, and it cannot remove their textual and structural cues. The current implementation has only the fixed LychD Dark palette and no operator override source; State owns that boundary.

Interface locale and content language

The Altar localizes its own chrome through ordinary framework-neutral message catalogues and locale-aware formatters. Canonical route, API, Pattern, Spell, Run, event, status, and field identities remain stable and untranslated. Components consume message identities and typed parameters; a translator or model never supplies HTML, Svelte, executable templates, or domain keys. Catalogues are reviewed static client assets under the same FOSS, notice, CSP, and release boundary as the rest of the Altar, not a runtime translation service.

The FOSS reference Altar deliberately targets English as its sole required interface locale. This keeps the reference implementation, review surface, release evidence, and contributor obligation small; it does not permit English grammar, word order, or left-to-right layout assumptions to enter canonical identities or application contracts. A multilingual interface remains permitted future work rather than a current Product goal. Another locale may enter later as an optional, reviewed static catalogue and formatter contribution with exact compatibility, provenance, licence, and accessibility evidence. It is not a skin, arbitrary client-code plug-in, or support claim merely because a community translation exists.

Locale resolution is explicit Altar choice, then an admitted Principal preference, then a browser language hint, then an admitted operator default, then English. The current loopback magus:* bootstrap is not an authenticated Principal and may retain only a presentation-local choice. Applying a locale sets document lang and direction and governs dates, times, numbers, pluralization, visible copy, and accessibility text. Every admitted locale owes fallback, missing-message, overflow, keyboard, screen-reader, and bidirectional-layout receipts; a catalogue claim alone does not establish support.

Interface locale is separate from content language. A Magus may use a Slovak Altar while reading an English prompt, original Japanese evidence, or another language selected for speech, image, video, or music. Changing locale never translates retained content. Translation is an explicit, attributed semantic transformation that preserves source text, target language, implementation revision, declared loss, and authority. Persona language or a model's detected language never silently changes the Magus's interface. Speech input and output may support additional languages while the Altar chrome remains English; speech capability does not imply an installed or supported interface catalogue.

The delivered Altar remains English-only: it has no message catalogue, locale selector, Principal-preference binding, or right-to-left receipt. Browser-native formatting that happens to follow a device locale is not this contract; State owns the exact material boundary.

Build boundary, security, and reopening

Development runs Vite on 127.0.0.1:5173, proxying /api and /schema to the loopback Vessel. Production serves static Bridge, Orb, Nexus, and Loom shells; unknown APIs and retired paths stay 404. Every operation that raises a runtime not-found across those verticals declares the shared JSON FrameworkError contract. npm ci, OpenAPI and notice generation, compilation, and Python build share a release source. Audit verifies source identity, compiled index.html, and archive notices—not a real browser or running image. Only src/lychd/public/_app is broadly mounted; the two known root artifacts, /altar-lightning.svg and /THIRD_PARTY_NOTICES.txt, have narrow typed Litestar handlers.

The Altar is loopback-only. Defaults use same-origin CORS, accept only explicit loopback Origin exceptions, constrain Host to literal loopback authorities, and expose schema JSON without remote documentation assets. CSRF remains an unsafe-method layer, not authentication; ordinary requests still receive the fixed bootstrap Sigil. The foreground launcher can expose configuration the app cannot observe, security-header and production-browser receipts are absent, and no remote principal exists. Remote, proxied, tunneled, direct-image-public, and untrusted-browser use remains unsupported.

Focused checks cover Svelte/TypeScript; frontend API, cursor, remount, stream, focus-return, and GenUI tests; Python routes, controllers, consent, SSE, fixed root assets, and four instruments; guards for unsafe HTML, server modules, styling, and misplaced runes; plus static build and archive audit. They do not establish Playwright against create_app(), full keyboard/a11y behavior, hostile-browser security, performance budgets, Node-free production image, or durable cross-process events. The decision lock is the exhaustive reopening rule. A build, security, or renderer task applies that gate rather than starting another framework comparison.

Consequences

Positive

One generated protocol and Svelte projection replace the hybrid stack; snapshots, cursors, stable identities, and visible gaps keep loss explicit.

Negative

The SPA needs JavaScript, process-local streams cannot recover token history, and browser accessibility, lifecycle, root-asset, performance, and hostile-browser receipts remain open.